Newer
Older
dub_jkp / source / dub / package_.d
/**
	Stuff with dependencies.

	Copyright: © 2012 Matthias Dondorff
	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
	Authors: Matthias Dondorff
*/
module dub.package_;

import dub.compilers.compiler;
import dub.dependency;
import dub.utils;

import std.algorithm;
import std.array;
import std.conv;
import std.exception;
import std.file;
import std.range;
import std.string;
import vibecompat.core.log;
import vibecompat.core.file;
import vibecompat.data.json;
import vibecompat.inet.url;

enum PackageJsonFilename = "package.json";


/// Indicates where a package has been or should be installed to.
enum InstallLocation {
	/// Packages installed with 'local' will be placed in the current folder 
	/// using the package name as destination.
	local,
	/// Packages with 'projectLocal' will be placed in a folder managed by
	/// dub (i.e. inside the .dub subfolder). 
	projectLocal,
	/// Packages with 'userWide' will be placed in a folder accessible by
	/// all of the applications from the current user.
	userWide,
	/// Packages installed with 'systemWide' will be placed in a shared folder,
	/// which can be accessed by all users of the system.
	systemWide
}

/// Representing an installed package, usually constructed from a json object.
/// 
/// Json file example:
/// {
/// 		"name": "MetalCollection",
/// 		"author": "VariousArtists",
/// 		"version": "1.0.0",
///		"url": "https://github.org/...",
///		"keywords": "a,b,c",
///		"category": "music.best",
/// 		"dependencies": {
/// 			"black-sabbath": ">=1.0.0",
/// 			"CowboysFromHell": "<1.0.0",
/// 			"BeneathTheRemains": {"version": "0.4.1", "path": "./beneath-0.4.1"}
/// 		}
///		"licenses": {
///			...
///		}
///		"configurations": [
// TODO: what and how?
///		]
// TODO: plain like this or packed together?
///			"
///			"dflags-X"
///			"lflags-X"
///			"libs-X"
///			"files-X"
///			"copyFiles-X"
///			"versions-X"
///			"importPaths-X"
///			"stringImportPaths-X"	
///			"sourcePath"
/// 	}
///	}
///
/// TODO: explain configurations
class Package {
	static struct LocalPackageDef { string name; Version version_; Path path; }

	private {
		InstallLocation m_location;
		Path m_path;
		PackageInfo m_info;
	}

	this(InstallLocation location, Path root)
	{
		this(jsonFromFile(root ~ PackageJsonFilename), location, root);
	}

	this(Json packageInfo, InstallLocation location = InstallLocation.local, Path root = Path())
	{
		m_location = location;
		m_path = root;

		// check for default string import folders
		foreach(defvf; ["views"]){
			auto p = m_path ~ defvf;
			if( existsFile(p) )
				m_info.buildSettings.stringImportPaths[""] ~= defvf;
		}

		string[] app_files;
		auto pkg_name = packageInfo.name.get!string();

		// check for default source folders
		foreach(defsf; ["source", "src"]){
			auto p = m_path ~ defsf;
			if( existsFile(p) ){
				m_info.buildSettings.sourcePaths[""] ~= defsf;
				m_info.buildSettings.importPaths[""] ~= defsf;
				if( existsFile(p ~ "app.d") ) app_files ~= Path(defsf ~ "/app.d").toNativeString();
				else if( existsFile(p ~ (pkg_name~".d")) ) app_files ~= Path(defsf ~ "/"~pkg_name~".d").toNativeString();
			}
		}

		// parse the JSON description
		{
			scope(failure) logError("Failed to parse package description in %s", root.toNativeString());
			m_info.parseJson(packageInfo);
		}

		// generate default configurations if none are defined
		if( m_info.configurations.length == 0 ){
			if( m_info.buildSettings.targetType == TargetType.executable ){
				BuildSettingsTemplate app_settings;
				app_settings.targetType = TargetType.executable;
				m_info.configurations ~= ConfigurationInfo("application", app_settings);
			} else {
				if( m_info.buildSettings.targetType == TargetType.autodetect ){
					if( app_files.length ){
						BuildSettingsTemplate app_settings;
						app_settings.targetType = TargetType.executable;
						m_info.configurations ~= ConfigurationInfo("application", app_settings);
					}
				}

				BuildSettingsTemplate lib_settings;
				lib_settings.targetType = TargetType.library;
				lib_settings.excludedSourceFiles[""] = app_files;
				m_info.configurations ~= ConfigurationInfo("library", lib_settings);
			}
		}

		// warn about use of special flags
		m_info.buildSettings.warnOnSpecialCompilerFlags(m_info.name);
		foreach (ref config; m_info.configurations)
			config.buildSettings.warnOnSpecialCompilerFlags(m_info.name);
	}
	
	@property string name() const { return m_info.name; }
	@property string vers() const { return m_info.version_; }
	@property Version ver() const { return Version(m_info.version_); }
	@property installLocation() const { return m_location; }
	@property Path path() const { return m_path; }
	@property Path packageInfoFile() const { return m_path ~ "package.json"; }
	@property const(Dependency[string]) dependencies() const { return m_info.dependencies; }
	
	@property string[] configurations()
	const {
		auto ret = appender!(string[])();
		foreach( ref config; m_info.configurations )
			ret.put(config.name);
		return ret.data;
	}

	/// Returns all BuildSettings for the given platform and config.
	BuildSettings getBuildSettings(in BuildPlatform platform, string config)
	const {
		logDebug("Using config %s for %s", config, this.name);
		foreach(ref conf; m_info.configurations){
			if( conf.name != config ) continue;
			BuildSettings ret;
			m_info.buildSettings.getPlatformSettings(ret, platform, this.path);
			conf.buildSettings.getPlatformSettings(ret, platform, this.path);
			if( ret.targetName.empty ) ret.targetName = this.name;
			return ret;
		}
		assert(false, "Unknown configuration for "~m_info.name~": "~config);
	}

	string getSubConfiguration(string config, in Package dependency, in BuildPlatform platform)
	const {
		bool found = false;
		foreach(ref c; m_info.configurations){
			if( c.name == config ){
				if( auto pv = dependency.name in c.buildSettings.subConfigurations ) return *pv;
				found = true;
				break;
			}
		}
		assert(found, "Invliad configuration \""~config~"\" for "~this.name);
		if( auto pv = dependency.name in m_info.buildSettings.subConfigurations ) return *pv;
		return null;
	}

	/// Returns the default configuration to build for the given platform
	string getDefaultConfiguration(in BuildPlatform platform, bool is_main_package = false)
	const {
		foreach(ref conf; m_info.configurations){
			if( !conf.matchesPlatform(platform) ) continue;
			if( !is_main_package && conf.buildSettings.targetType == TargetType.executable ) continue;
			return conf.name;
		}
		throw new Exception(format("Found no suitable configuration for %s on this platform.", this.name));
	}

	/// Human readable information of this package and its dependencies.
	string info() const {
		string s;
		s ~= m_info.name ~ ", version '" ~ m_info.version_ ~ "'";
		s ~= "\n  Dependencies:";
		foreach(string p, ref const Dependency v; m_info.dependencies)
			s ~= "\n    " ~ p ~ ", version '" ~ to!string(v) ~ "'";
		return s;
	}
	
	/// Writes the json file back to the filesystem
	void writeJson(Path path) {
		auto dstFile = openFile((path~PackageJsonFilename).toString(), FileMode.CreateTrunc);
		scope(exit) dstFile.close();
		dstFile.writePrettyJsonString(m_info.toJson());
		assert(false);
	}

	/// Adds an dependency, if the package is already a dependency and it cannot be
	/// merged with the supplied dependency, an exception will be generated.
	void addDependency(string packageId, const Dependency dependency) {
		Dependency dep = new Dependency(dependency);
		if(packageId in m_info.dependencies) { 
			dep = dependency.merge(m_info.dependencies[packageId]);
			if(!dep.valid()) throw new Exception("Cannot merge with existing dependency.");
		}
		m_info.dependencies[packageId] = dep;
	}

	/// Removes a dependecy.
	void removeDependency(string packageId) {
		if (packageId in m_info.dependencies)
			m_info.dependencies.remove(packageId);
	}
} 

struct PackageInfo {
	string name;
	string version_;
	string description;
	string homepage;
	string[] authors;
	string copyright;
	string license;
	Dependency[string] dependencies;
	BuildSettingsTemplate buildSettings;
	ConfigurationInfo[] configurations;

	void parseJson(Json json)
	{
		foreach( string field, value; json ){
			switch(field){
				default: break;
				case "name": this.name = value.get!string; break;
				case "version": this.version_ = value.get!string; break;
				case "description": this.description = value.get!string; break;
				case "homepage": this.homepage = value.get!string; break;
				case "authors": this.authors = deserializeJson!(string[])(value); break;
				case "copyright": this.copyright = value.get!string; break;
				case "license": this.license = value.get!string; break;
				case "dependencies":
					foreach( string pkg, verspec; value ) {
						enforce(pkg !in this.dependencies, "The dependency '"~pkg~"' is specified more than once." );
						Dependency dep;
						if( verspec.type == Json.Type.Object ){
							auto ver = verspec["version"].get!string;
							if( auto pp = "path" in verspec ){
								dep = new Dependency(Version(ver));
								dep.path = Path(verspec.path.get!string());
							} else dep = new Dependency(ver);
						} else {
							// canonical "package-id": "version"
							dep = new Dependency(verspec.get!string());
						}
						this.dependencies[pkg] = dep;
					}
					break;
				case "configurations":
					foreach( settings; value ){
						ConfigurationInfo ci;
						ci.parseJson(settings);
						this.configurations ~= ci;
					}
					break;
			}
		}

		// parse build settings
		this.buildSettings.parseJson(json);

		enforce(this.name.length > 0, "The package \"name\" field is missing or empty.");
	}

	Json toJson()
	const {
		auto ret = buildSettings.toJson();
		ret.name = this.name;
		if( !this.version_.empty ) ret["version"] = this.version_;
		if( !this.description.empty ) ret.description = this.description;
		if( !this.homepage.empty ) ret.homepage = this.homepage;
		if( !this.authors.empty ) ret.authors = serializeToJson(this.authors);
		if( !this.copyright.empty ) ret.copyright = this.copyright;
		if( !this.license.empty ) ret.license = this.license;
		if( this.dependencies ){
			auto deps = Json.EmptyObject;
			foreach( pack, d; this.dependencies ){
				if( d.path.empty ){
					deps[pack] = d.toString();
				} else deps[pack] = serializeToJson(["version": d.version_.toString(), "path": d.path.toString()]);
			}
			ret.dependencies = deps;
		}
		if( this.configurations ){
			Json[] configs;
			foreach(config; this.configurations)
				configs ~= config.toJson();
			ret.configurations = configs;
		}
		return ret;
	}
}

struct ConfigurationInfo {
	string name;
	string[] platforms;
	BuildSettingsTemplate buildSettings;

	this(string name, BuildSettingsTemplate build_settings)
	{
		this.name = name;
		this.buildSettings = build_settings;
	}

	void parseJson(Json json)
	{
		this.buildSettings.targetType = TargetType.library;

		foreach(string name, value; json){
			switch(name){
				default: break;
				case "name":
					this.name = value.get!string();
					enforce(!this.name.empty, "Configurations must have a non-empty name.");
					break;
				case "platforms": this.platforms = deserializeJson!(string[])(value); break;
			}
		}

		enforce(!this.name.empty, "Configuration is missing a name.");

		BuildSettingsTemplate bs;
		this.buildSettings.parseJson(json);
	}

	Json toJson()
	const {
		auto ret = buildSettings.toJson();
		ret.name = name;
		if( this.platforms.length ) ret.platforms = serializeToJson(platforms);
		return ret;
	}

	bool matchesPlatform(in BuildPlatform platform)
	const {
		if( platforms.empty ) return true;
		foreach(p; platforms)
			if( platform.matchesSpecification("-"~p) )
				return true;
		return false;
	}
}

struct BuildSettingsTemplate {
	TargetType targetType = TargetType.autodetect;
	string targetPath;
	string targetName;
	string[string] subConfigurations;
	string[][string] dflags;
	string[][string] lflags;
	string[][string] libs;
	string[][string] sourceFiles;
	string[][string] sourcePaths;
	string[][string] excludedSourceFiles;
	string[][string] copyFiles;
	string[][string] versions;
	string[][string] importPaths;
	string[][string] stringImportPaths;
	string[][string] preGenerateCommands;
	string[][string] postGenerateCommands;
	string[][string] preBuildCommands;
	string[][string] postBuildCommands;

	void parseJson(Json json)
	{
		foreach(string name, value; json)
		{
			auto idx = std.string.indexOf(name, "-");
			string basename, suffix;
			if( idx >= 0 ) basename = name[0 .. idx], suffix = name[idx .. $];
			else basename = name;
			switch(basename){
				default: break;
				case "targetType":
					enforce(suffix.empty, "targetType does not support platform customization.");
					targetType = value.get!string().to!TargetType();
					break;
				case "targetPath":
					enforce(suffix.empty, "targetPath does not support platform customization.");
					this.targetPath = value.get!string;
					break;
				case "targetName":
					enforce(suffix.empty, "targetName does not support platform customization.");
					this.targetName = value.get!string;
					break;
				case "subConfigurations":
					enforce(suffix.empty, "subConfigurations does not support platform customization.");
					this.subConfigurations = deserializeJson!(string[string])(value);
					break;
				case "dflags": this.dflags[suffix] = deserializeJson!(string[])(value); break;
				case "lflags": this.lflags[suffix] = deserializeJson!(string[])(value); break;
				case "libs": this.libs[suffix] = deserializeJson!(string[])(value); break;
				case "files":
				case "sourceFiles": this.sourceFiles[suffix] = deserializeJson!(string[])(value); break;
				case "sourcePaths": this.sourcePaths[suffix] = deserializeJson!(string[])(value); break;
				case "sourcePath": this.sourcePaths[suffix] ~= [value.get!string()]; break; // deprecated
				case "excludedSourceFiles": this.excludedSourceFiles[suffix] = deserializeJson!(string[])(value); break;
				case "copyFiles": this.copyFiles[suffix] = deserializeJson!(string[])(value); break;
				case "versions": this.versions[suffix] = deserializeJson!(string[])(value); break;
				case "importPaths": this.importPaths[suffix] = deserializeJson!(string[])(value); break;
				case "stringImportPaths": this.stringImportPaths[suffix] = deserializeJson!(string[])(value); break;
				case "preGenerateCommands": this.preGenerateCommands[suffix] = deserializeJson!(string[])(value); break;
				case "postGenerateCommands": this.postGenerateCommands[suffix] = deserializeJson!(string[])(value); break;
				case "preBuildCommands": this.preBuildCommands[suffix] = deserializeJson!(string[])(value); break;
				case "postBuildCommands": this.postBuildCommands[suffix] = deserializeJson!(string[])(value); break;
			}
		}
	}

	Json toJson()
	const {
		auto ret = Json.EmptyObject;
		if( targetType != TargetType.autodetect ) ret["targetType"] = targetType.to!string();
		if( !targetPath.empty ) ret["targetPath"] = targetPath;
		if( !targetName.empty ) ret["targetName"] = targetPath;
		foreach(suffix, arr; dflags) ret["dflags"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; lflags) ret["lflags"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; libs) ret["libs"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; sourceFiles) ret["sourceFiles"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; sourcePaths) ret["sourcePaths"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; excludedSourceFiles) ret["excludedSourceFiles"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; copyFiles) ret["copyFiles"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; versions) ret["versions"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; importPaths) ret["importPaths"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; stringImportPaths) ret["stringImportPaths"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; preGenerateCommands) ret["preGenerateCommands"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; postGenerateCommands) ret["postGenerateCommands"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; preBuildCommands) ret["preBuildCommands"~suffix] = serializeToJson(arr);
		foreach(suffix, arr; postBuildCommands) ret["postBuildCommands"~suffix] = serializeToJson(arr);
		return ret;
	}

	void getPlatformSettings(ref BuildSettings dst, in BuildPlatform platform, Path base_path)
	const {
		dst.targetType = this.targetType;
		if (!this.targetPath.empty) dst.targetPath = this.targetPath;
		if (!this.targetName.empty) dst.targetName = this.targetName;

		// collect source files from all source folders
		foreach(suffix, paths; sourcePaths){
			if( !platform.matchesSpecification(suffix) )
				continue;

			foreach(spath; paths){
				auto path = base_path ~ spath;
				if( !existsFile(path) || !isDir(path.toNativeString()) ){
					logWarn("Invalid source path: %s", path.toNativeString());
					continue;
				}

				foreach(d; dirEntries(path.toNativeString(), "*.d", SpanMode.depth)){
					if (isDir(d.name)) continue;
					auto src = Path(d.name).relativeTo(base_path);
					dst.addSourceFiles(src.toNativeString());
				}
			}
		}

		getPlatformSetting!("dflags", "addDFlags")(dst, platform);
		getPlatformSetting!("lflags", "addLFlags")(dst, platform);
		getPlatformSetting!("libs", "addLibs")(dst, platform);
		getPlatformSetting!("sourceFiles", "addSourceFiles")(dst, platform);
		getPlatformSetting!("excludedSourceFiles", "removeSourceFiles")(dst, platform);
		getPlatformSetting!("copyFiles", "addCopyFiles")(dst, platform);
		getPlatformSetting!("versions", "addVersions")(dst, platform);
		getPlatformSetting!("importPaths", "addImportPaths")(dst, platform);
		getPlatformSetting!("stringImportPaths", "addStringImportPaths")(dst, platform);
		getPlatformSetting!("preGenerateCommands", "addPreGenerateCommands")(dst, platform);
		getPlatformSetting!("postGenerateCommands", "addPostGenerateCommands")(dst, platform);
		getPlatformSetting!("preBuildCommands", "addPreBuildCommands")(dst, platform);
		getPlatformSetting!("postBuildCommands", "addPostBuildCommands")(dst, platform);
	}

	void getPlatformSetting(string name, string addname)(ref BuildSettings dst, in BuildPlatform platform)
	const {
		foreach(suffix, values; __traits(getMember, this, name)){
			if( platform.matchesSpecification(suffix) )
				__traits(getMember, dst, addname)(values);
		}
	}

	void warnOnSpecialCompilerFlags(string package_name)
	{
		foreach (flags; this.dflags)
			.warnOnSpecialCompilerFlags(flags, package_name);
	}
}