Newer
Older
dub_jkp / source / dub / package_.d
@Sönke Ludwig Sönke Ludwig on 23 Aug 2014 36 KB Untangle Package and PackageInfo. See #403.
/**
	Stuff with dependencies.

	Copyright: © 2012-2013 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.internal.utils;
import dub.internal.vibecompat.core.log;
import dub.internal.vibecompat.core.file;
import dub.internal.vibecompat.data.json;
import dub.internal.vibecompat.inet.url;

import std.algorithm;
import std.array;
import std.conv;
import std.exception;
import std.file;
import std.range;
import std.string;
import std.traits : EnumMembers;



enum PackageFormat { json, sdl }
struct FilenameAndFormat
{
	string filename;
	PackageFormat format;
}
struct PathAndFormat
{
	Path path;
	PackageFormat format;
	@property bool empty() { return path.empty; }
	string toString() { return path.toString(); }
}

// Supported package descriptions in decreasing order of preference.
enum FilenameAndFormat[] packageInfoFiles = [
	{"dub.json", PackageFormat.json},
	/*{"dub.sdl",PackageFormat.sdl},*/
	{"package.json", PackageFormat.json}
];

string defaultPackageFilename() {
	return packageInfoFiles[0].filename;
}

/**
	Represents a package, including its sub packages

	Documentation of the dub.json can be found at
	http://registry.vibed.org/package-format
*/
class Package {
	static struct LocalPackageDef { string name; Version version_; Path path; }

	private {
		Path m_path;
		PathAndFormat m_infoFile;
		PackageInfo m_info;
		Package m_parentPackage;
	}

	static PathAndFormat findPackageFile(Path path)
	{
		foreach(file; packageInfoFiles) {
			auto filename = path ~ file.filename;
			if(existsFile(filename)) return PathAndFormat(filename, file.format);
		}
		return PathAndFormat(Path());
	}

	this(Path root, PathAndFormat infoFile = PathAndFormat(), Package parent = null, string versionOverride = "")
	{
		RawPackage raw_package;
		m_infoFile = infoFile;

		try {
			if(m_infoFile.empty) {
				m_infoFile = findPackageFile(root);
				if(m_infoFile.empty) throw new Exception("no package file was found, expected one of the following: "~to!string(packageInfoFiles));
			}
			raw_package = rawPackageFromFile(m_infoFile);
		} catch (Exception ex) throw ex;//throw new Exception(format("Failed to load package %s: %s", m_infoFile.toNativeString(), ex.msg));

		enforce(raw_package !is null, format("Missing package description for package at %s", root.toNativeString()));
		this(raw_package, root, parent, versionOverride);
	}

	this(Json package_info, Path root = Path(), Package parent = null, string versionOverride = "")
	{
		this(new JsonPackage(package_info), root, parent, versionOverride);
	}

	this(const RawPackage raw_package, Path root = Path(), Package parent = null, string versionOverride = "")
	{
		PackageInfo recipe;

		// parse the Package description
		if(raw_package !is null)
		{
			scope(failure) logError("Failed to parse package description in %s", root.toNativeString());
			raw_package.parseInto(recipe, parent ? parent.name : null);

			if (!versionOverride.empty)
				recipe.version_ = versionOverride;

			// try to run git to determine the version of the package if no explicit version was given
			if (recipe.version_.length == 0 && !parent) {
				try recipe.version_ = determineVersionFromSCM(root);
				catch (Exception e) logDebug("Failed to determine version by SCM: %s", e.msg);

				if (recipe.version_.length == 0) {
					logDiagnostic("Note: Failed to determine version of package %s at %s. Assuming ~master.", recipe.name, this.path.toNativeString());
					// TODO: Assume unknown version here?
					// recipe.version_ = Version.UNKNOWN.toString();
					recipe.version_ = Version.MASTER.toString();
				} else logDiagnostic("Determined package version using GIT: %s %s", recipe.name, recipe.version_);
			}
		}

		this(recipe, root, parent);
	}

	this(PackageInfo recipe, Path root = Path(), Package parent = null)
	{
		m_parentPackage = parent;
		m_path = root;
		m_path.endsWithSlash = true;

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

		// use the given recipe as the basis
		m_info = recipe;

		// WARNING: changed semantics here. Previously, "sourcePaths" etc.
		// could overwrite what was determined here. Now the default paths
		// are always added. This must be fixed somehow!

		// check for default source folders
		string app_main_file;
		auto pkg_name = recipe.name.length ? recipe.name : "unknown";
		foreach(defsf; ["source/", "src/"]){
			auto p = m_path ~ defsf;
			if( existsFile(p) ){
				m_info.buildSettings.sourcePaths[""] ~= defsf;
				m_info.buildSettings.importPaths[""] ~= defsf;
				foreach (fil; ["app.d", "main.d", pkg_name ~ "/main.d", pkg_name ~ "/" ~ "app.d"])
					if (existsFile(p ~ fil)) {
						app_main_file = Path(defsf ~ fil).toNativeString();
						break;
					}
			}
		}

		// 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;
				if (m_info.buildSettings.mainSourceFile.empty) app_settings.mainSourceFile = app_main_file;
				m_info.configurations ~= ConfigurationInfo("application", app_settings);
			} else if (m_info.buildSettings.targetType != TargetType.none) {
				BuildSettingsTemplate lib_settings;
				lib_settings.targetType = m_info.buildSettings.targetType == TargetType.autodetect ? TargetType.library : m_info.buildSettings.targetType;

				if (m_info.buildSettings.targetType == TargetType.autodetect) {
					if (app_main_file.length) {
						lib_settings.excludedSourceFiles[""] ~= app_main_file;

						BuildSettingsTemplate app_settings;
						app_settings.targetType = TargetType.executable;
						app_settings.mainSourceFile = app_main_file;
						m_info.configurations ~= ConfigurationInfo("application", app_settings);
					}
				}

				m_info.configurations ~= ConfigurationInfo("library", lib_settings);
			}
		}
		simpleLint();
	}

	@property string name()
	const {
		if (m_parentPackage) return m_parentPackage.name ~ ":" ~ m_info.name;
		else return m_info.name;
	}
	@property string vers() const { return m_parentPackage ? m_parentPackage.vers : m_info.version_; }
	@property Version ver() const { return Version(this.vers); }
	@property void ver(Version ver) { assert(m_parentPackage is null); m_info.version_ = ver.toString(); }
	@property ref inout(PackageInfo) info() inout { return m_info; }
	@property Path path() const { return m_path; }
	@property Path packageInfoFilename() const { return m_infoFile.path; }
	@property const(Dependency[string]) dependencies() const { return m_info.dependencies; }
	@property inout(Package) basePackage() inout { return m_parentPackage ? m_parentPackage.basePackage : this; }
	@property inout(Package) parentPackage() inout { return m_parentPackage; }
	@property inout(SubPackage)[] subPackages() inout { return m_info.subPackages; }

	@property string[] configurations()
	const {
		auto ret = appender!(string[])();
		foreach( ref config; m_info.configurations )
			ret.put(config.name);
		return ret.data;
	}

	const(Dependency[string]) getDependencies(string config)
	const {
		Dependency[string] ret;
		foreach (k, v; m_info.buildSettings.dependencies)
			ret[k] = v;
		foreach (ref conf; m_info.configurations)
			if (conf.name == config) {
				foreach (k, v; conf.buildSettings.dependencies)
					ret[k] = v;
				break;
			}
		return ret;
	}

	/** Overwrites the packge description file using the default filename with the current information.
	*/
	void storeInfo()
	{
		enforce(!ver.isUnknown, "Trying to store a package with an 'unknown' version, this is not supported.");
		auto filename = m_path ~ defaultPackageFilename();
		auto dstFile = openFile(filename.toNativeString(), FileMode.CreateTrunc);
		scope(exit) dstFile.close();
		dstFile.writePrettyJsonString(m_info.toJson());
		m_infoFile = PathAndFormat(filename);
	}

	/*inout(Package) getSubPackage(string name, bool silent_fail = false)
	inout {
		foreach (p; m_info.subPackages)
			if (p.package_ !is null && p.package_.name == this.name ~ ":" ~ name)
				return p.package_;
		enforce(silent_fail, format("Unknown sub package: %s:%s", this.name, name));
		return null;
	}*/

	void warnOnSpecialCompilerFlags()
	{
		// warn about use of special flags
		m_info.buildSettings.warnOnSpecialCompilerFlags(m_info.name, null);
		foreach (ref config; m_info.configurations)
			config.buildSettings.warnOnSpecialCompilerFlags(m_info.name, config.name);
	}

	const(BuildSettingsTemplate) getBuildSettings(string config = null)
	const {
		if (config.length) {
			foreach (ref conf; m_info.configurations)
				if (conf.name == config)
					return conf.buildSettings;
			assert(false, "Unknown configuration: "~config);
		} else {
			return m_info.buildSettings;
		}
	}

	/// Returns all BuildSettings for the given platform and config.
	BuildSettings getBuildSettings(in BuildPlatform platform, string config)
	const {
		BuildSettings ret;
		m_info.buildSettings.getPlatformSettings(ret, platform, this.path);
		bool found = false;
		foreach(ref conf; m_info.configurations){
			if( conf.name != config ) continue;
			conf.buildSettings.getPlatformSettings(ret, platform, this.path);
			found = true;
			break;
		}
		assert(found || config is null, "Unknown configuration for "~m_info.name~": "~config);

		// construct default target name based on package name
		if( ret.targetName.empty ) ret.targetName = this.name.replace(":", "_");

		// special support for DMD style flags
		getCompiler("dmd").extractBuildOptions(ret);

		return ret;
	}

	/// Returns the combination of all build settings for all configurations and platforms
	BuildSettings getCombinedBuildSettings()
	const {
		BuildSettings ret;
		m_info.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path);
		foreach(ref conf; m_info.configurations)
			conf.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path);

		// construct default target name based on package name
		if (ret.targetName.empty) ret.targetName = this.name.replace(":", "_");

		// special support for DMD style flags
		getCompiler("dmd").extractBuildOptions(ret);

		return ret;
	}

	void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type)
	const {
		if (build_type == "$DFLAGS") {
			import std.process;
			string dflags = environment.get("DFLAGS");
			settings.addDFlags(dflags.split());
			return;
		}

		if (auto pbt = build_type in m_info.buildTypes) {
			logDiagnostic("Using custom build type '%s'.", build_type);
			pbt.getPlatformSettings(settings, platform, this.path);
		} else {
			with(BuildOptions) switch (build_type) {
				default: throw new Exception(format("Unknown build type for %s: '%s'", this.name, build_type));
				case "plain": break;
				case "debug": settings.addOptions(debugMode, debugInfo); break;
				case "release": settings.addOptions(releaseMode, optimize, inline); break;
				case "release-nobounds": settings.addOptions(releaseMode, optimize, inline, noBoundsCheck); break;
				case "unittest": settings.addOptions(unittests, debugMode, debugInfo); break;
				case "docs": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Dddocs"); break;
				case "ddox": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Df__dummy.html", "-Xfdocs.json"); break;
				case "profile": settings.addOptions(profile, optimize, inline, debugInfo); break;
				case "cov": settings.addOptions(coverage, debugInfo); break;
				case "unittest-cov": settings.addOptions(unittests, coverage, debugMode, debugInfo); break;
			}
		}
	}

	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 || config is null, "Invalid 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 allow_non_library = false)
	const {
		foreach (ref conf; m_info.configurations) {
			if (!conf.matchesPlatform(platform)) continue;
			if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue;
			return conf.name;
		}
		return null;
	}

	/// Returns a list of configurations suitable for the given platform
	string[] getPlatformConfigurations(in BuildPlatform platform, bool is_main_package = false)
	const {
		auto ret = appender!(string[]);
		foreach(ref conf; m_info.configurations){
			if (!conf.matchesPlatform(platform)) continue;
			if (!is_main_package && conf.buildSettings.targetType == TargetType.executable) continue;
			ret ~= conf.name;
		}
		if (ret.data.length == 0) ret.put(null);
		return ret.data;
	}

	/// Human readable information of this package and its dependencies.
	string generateInfoString() 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 '" ~ v.toString() ~ "'";
		return s;
	}

	bool hasDependency(string depname, string config)
	const {
		if (depname in m_info.buildSettings.dependencies) return true;
		foreach (ref c; m_info.configurations)
			if ((config.empty || c.name == config) && depname in c.buildSettings.dependencies)
				return true;
		return false;
	}

	void describe(ref Json dst, BuildPlatform platform, string config)
	{
		dst.path = m_path.toNativeString();
		dst.name = this.name;
		dst["version"] = this.vers;
		dst.description = m_info.description;
		dst.homepage = m_info.homepage;
		dst.authors = m_info.authors.serializeToJson();
		dst.copyright = m_info.copyright;
		dst.license = m_info.license;
		dst.dependencies = m_info.dependencies.keys.serializeToJson();

		// save build settings
		BuildSettings bs = getBuildSettings(platform, config);
		BuildSettings allbs = getCombinedBuildSettings();

		foreach (string k, v; bs.serializeToJson()) dst[k] = v;
		dst.remove("requirements");
		dst.remove("sourceFiles");
		dst.remove("importFiles");
		dst.remove("stringImportFiles");
		dst.targetType = bs.targetType.to!string();
		if (dst.targetType != TargetType.none)
			dst.targetFileName = getTargetFileName(bs, platform);

		// prettify build requirements output
		Json[] breqs;
		for (int i = 1; i <= BuildRequirements.max; i <<= 1)
			if (bs.requirements & i)
				breqs ~= Json(to!string(cast(BuildRequirements)i));
		dst.buildRequirements = breqs;

		// prettify options output
		Json[] bopts;
		for (int i = 1; i <= BuildOptions.max; i <<= 1)
			if (bs.options & i)
				bopts ~= Json(to!string(cast(BuildOptions)i));
		dst.options = bopts;

		// collect all possible source files and determine their types
		string[string] sourceFileTypes;
		foreach (f; allbs.stringImportFiles) sourceFileTypes[f] = "unusedStringImport";
		foreach (f; allbs.importFiles) sourceFileTypes[f] = "unusedImport";
		foreach (f; allbs.sourceFiles) sourceFileTypes[f] = "unusedSource";
		foreach (f; bs.stringImportFiles) sourceFileTypes[f] = "stringImport";
		foreach (f; bs.importFiles) sourceFileTypes[f] = "import";
		foreach (f; bs.sourceFiles) sourceFileTypes[f] = "source";
		Json[] files;
		foreach (f; sourceFileTypes.byKey.array.sort) {
			auto jf = Json.emptyObject;
			jf["path"] = f;
			jf["type"] = sourceFileTypes[f];
			files ~= jf;
		}
		dst.files = Json(files);
	}

	private void simpleLint() const {
		if (m_parentPackage) {
			if (m_parentPackage.path != path) {
				if (info.license.length && info.license != m_parentPackage.info.license)
					logWarn("License in subpackage %s is different than it's parent package, this is discouraged.", name);
			}
		}
		if (name.empty()) logWarn("The package in %s has no name.", path);
	}

	private static RawPackage rawPackageFromFile(PathAndFormat file, bool silent_fail = false) {
		if( silent_fail && !existsFile(file.path) ) return null;
		auto f = openFile(file.path.toNativeString(), FileMode.Read);
		scope(exit) f.close();
		auto text = stripUTF8Bom(cast(string)f.readAll());

		final switch(file.format) {
			case PackageFormat.json:
				return new JsonPackage(parseJsonString(text));
			case PackageFormat.sdl:
				if(silent_fail) return null; throw new Exception("SDL not implemented");
		}
	}

	static abstract class RawPackage
	{
		string package_name; // Should already be lower case
		string version_;
		abstract void parseInto(ref PackageInfo package_, string parent_name) const;
	}
	private static class JsonPackage : RawPackage
	{
		Json json;
		this(Json json) {
			this.json = json;

			string nameLower;
			if(json.type == Json.Type.string) {
				nameLower = json.get!string.toLower();
				this.json = nameLower;
			} else {
				nameLower = json.name.get!string.toLower();
				this.json.name = nameLower;
				this.package_name = nameLower;

				Json versionJson = json["version"];
				this.version_ = (versionJson.type == Json.Type.undefined) ? null : versionJson.get!string;
			}

			this.package_name = nameLower;
		}
		override void parseInto(ref PackageInfo recipe, string parent_name) const
		{
			recipe.parseJson(json, parent_name);
		}
	}
	private static class SdlPackage : RawPackage
	{
		override void parseInto(ref PackageInfo package_, string parent_name) const
		{
			throw new Exception("SDL packages not implemented yet");
		}
	}
}


struct SubPackage
{
	string path;
	PackageInfo recipe;
}

/// Specifying package information without any connection to a certain
/// retrived package, like Package class is doing.
struct PackageInfo {
	string name;
	string version_;
	string description;
	string homepage;
	string[] authors;
	string copyright;
	string license;
	string[] ddoxFilterArgs;
	BuildSettingsTemplate buildSettings;
	ConfigurationInfo[] configurations;
	BuildSettingsTemplate[string] buildTypes;

	size_t exportedPackageCount;
	SubPackage[] subPackages;

	@property const(Dependency)[string] dependencies()
	const {
		const(Dependency)[string] ret;
		foreach (n, d; this.buildSettings.dependencies)
			ret[n] = d;
		foreach (ref c; configurations)
			foreach (n, d; c.buildSettings.dependencies)
				ret[n] = d;
		return ret;
	}

	inout(ConfigurationInfo) getConfiguration(string name)
	inout {
		foreach (c; configurations)
			if (c.name == name)
				return c;
		throw new Exception("Unknown configuration: "~name);
	}
	void parseJson(Json json, string parent_name)
	{
		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 "configurations": break; // handled below, after the global settings have been parsed
				case "buildTypes":
					foreach (string name, settings; value) {
						BuildSettingsTemplate bs;
						bs.parseJson(settings, null);
						buildTypes[name] = bs;
					}
					break;
				case "-ddoxFilterArgs": this.ddoxFilterArgs = deserializeJson!(string[])(value); break;
			}
		}

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

		auto fullname = parent_name.length ? parent_name ~ ":" ~ this.name : this.name;

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

		if (auto pv = "configurations" in json) {
			TargetType deftargettp = TargetType.library;
			if (this.buildSettings.targetType != TargetType.autodetect)
				deftargettp = this.buildSettings.targetType;

			foreach (settings; *pv) {
				ConfigurationInfo ci;
				ci.parseJson(settings, this.name, deftargettp);
				this.configurations ~= ci;
			}
		}

		// parse any sub packages after the main package has been fully parsed
		if (auto ps = "subPackages" in json)
			parseSubPackages(fullname, ps.opt!(Json[]));
	}

	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.subPackages.empty ) {
			Json[] jsonSubPackages = new Json[this.subPackages.length];
			foreach(i, subPackage; subPackages) {
				if(subPackage.path !is null) {
					jsonSubPackages[i] = Json(subPackage.path);
				} else {
					jsonSubPackages[i] = subPackage.recipe.toJson();
				}
			}
			ret.subPackages = jsonSubPackages;
		}
		if( this.configurations ){
			Json[] configs;
			foreach(config; this.configurations)
				configs ~= config.toJson();
			ret.configurations = configs;
		}
		if( this.buildTypes.length ) {
			Json[string] types;
			foreach(name, settings; this.buildTypes)
				types[name] = settings.toJson();
		}
		if( !this.ddoxFilterArgs.empty ) ret["-ddoxFilterArgs"] = this.ddoxFilterArgs.serializeToJson();
		return ret;
	}

	private void parseSubPackages(string parent_package_name, Json[] subPackagesJson)
	{
		enforce(!parent_package_name.canFind(":"), format("'subPackages' found in '%s'. This is only supported in the main package file for '%s'.",
			parent_package_name, getBasePackageName(parent_package_name)));

		this.exportedPackageCount = 0;
		this.subPackages = new SubPackage[subPackagesJson.length];
		foreach(i, subPackageJson; subPackagesJson) {
			// Handle referenced Packages
			if(subPackageJson.type == Json.Type.string) {
				string subpath = subPackageJson.get!string;
				this.subPackages[i] = SubPackage(subpath, PackageInfo.init);
				this.exportedPackageCount++;
			} else {
				PackageInfo subinfo;
				subinfo.parseJson(subPackageJson, parent_package_name);
				this.subPackages[i] = SubPackage(null, subinfo);
			}
		}
	}
}

/// Bundles information about a build configuration.
struct ConfigurationInfo {
	string name;
	string[] platforms;
	BuildSettingsTemplate buildSettings;

	this(string name, BuildSettingsTemplate build_settings)
	{
		enforce(!name.empty, "Configuration name is empty.");
		this.name = name;
		this.buildSettings = build_settings;
	}

	void parseJson(Json json, string package_name, TargetType default_target_type = TargetType.library)
	{
		this.buildSettings.targetType = default_target_type;

		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, package_name);
	}

	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;
	}
}

/// This keeps general information about how to build a package.
/// It contains functions to create a specific BuildSetting, targeted at
/// a certain BuildPlatform.
struct BuildSettingsTemplate {
	Dependency[string] dependencies;
	string systemDependencies;
	TargetType targetType = TargetType.autodetect;
	string targetPath;
	string targetName;
	string workingDirectory;
	string mainSourceFile;
	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] debugVersions;
	string[][string] importPaths;
	string[][string] stringImportPaths;
	string[][string] preGenerateCommands;
	string[][string] postGenerateCommands;
	string[][string] preBuildCommands;
	string[][string] postBuildCommands;
	BuildRequirements[string] buildRequirements;
	BuildOptions[string] buildOptions;

	void parseJson(Json json, string package_name)
	{
		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 "dependencies":
					foreach (string pkg, verspec; value) {
						if (pkg.startsWith(":")) {
							enforce(!package_name.canFind(':'), format("Short-hand packages syntax not allowed within sub packages: %s -> %s", package_name, pkg));
							pkg = package_name ~ pkg;
						}
						enforce(pkg !in this.dependencies, "The dependency '"~pkg~"' is specified more than once." );
						this.dependencies[pkg] = deserializeJson!Dependency(verspec);
					}
					break;
				case "systemDependencies":
					this.systemDependencies = value.get!string;
					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 "workingDirectory":
					enforce(suffix.empty, "workingDirectory does not support platform customization.");
					this.workingDirectory = value.get!string;
					break;
				case "mainSourceFile":
					enforce(suffix.empty, "mainSourceFile does not support platform customization.");
					this.mainSourceFile = 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 "debugVersions": this.debugVersions[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;
				case "buildRequirements":
					BuildRequirements reqs;
					foreach (req; deserializeJson!(string[])(value))
						reqs |= to!BuildRequirements(req);
					this.buildRequirements[suffix] = reqs;
					break;
				case "buildOptions":
					BuildOptions options;
					foreach (opt; deserializeJson!(string[])(value))
						options |= to!BuildOptions(opt);
					this.buildOptions[suffix] = options;
					break;
			}
		}
	}

	Json toJson()
	const {
		auto ret = Json.emptyObject;
		if( this.dependencies !is null ){
			auto deps = Json.emptyObject;
			foreach( pack, d; this.dependencies )
				deps[pack] = serializeToJson(d);
			ret.dependencies = deps;
		}
		if (this.systemDependencies !is null) ret.systemDependencies = this.systemDependencies;
		if (targetType != TargetType.autodetect) ret["targetType"] = targetType.to!string();
		if (!targetPath.empty) ret["targetPath"] = targetPath;
		if (!targetName.empty) ret["targetName"] = targetName;
		if (!workingDirectory.empty) ret["workingDirectory"] = workingDirectory;
		if (!mainSourceFile.empty) ret["mainSourceFile"] = mainSourceFile;
		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; debugVersions) ret["debugVersions"~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);
		foreach (suffix, arr; buildRequirements) {
			string[] val;
			foreach (i; [EnumMembers!BuildRequirements])
				if (arr & i) val ~= to!string(i);
			ret["buildRequirements"~suffix] = serializeToJson(val);
		}
		foreach (suffix, arr; buildOptions) {
			string[] val;
			foreach (i; [EnumMembers!BuildOptions])
				if (arr & i) val ~= to!string(i);
			ret["buildOptions"~suffix] = serializeToJson(val);
		}
		return ret;
	}

	/// Constructs a BuildSettings object from this template.
	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;
		if (!this.workingDirectory.empty) dst.workingDirectory = this.workingDirectory;
		if (!this.mainSourceFile.empty) {
			dst.mainSourceFile = this.mainSourceFile;
			dst.addSourceFiles(this.mainSourceFile);
		}

		void collectFiles(string method)(in string[][string] paths_map, string pattern)
		{
			foreach (suffix, paths; paths_map) {
				if (!platform.matchesSpecification(suffix))
					continue;

				foreach (spath; paths) {
					enforce(!spath.empty, "Paths must not be empty strings.");
					auto path = Path(spath);
					if (!path.absolute) path = base_path ~ path;
					if (!existsFile(path) || !isDir(path.toNativeString())) {
						logWarn("Invalid source/import path: %s", path.toNativeString());
						continue;
					}

					foreach (d; dirEntries(path.toNativeString(), pattern, SpanMode.depth)) {
						if (isDir(d.name)) continue;
						auto src = Path(d.name).relativeTo(base_path);
						__traits(getMember, dst, method)(src.toNativeString());
					}
				}
			}
		}

		// collect files from all source/import folders
		collectFiles!"addSourceFiles"(sourcePaths, "*.d");
		collectFiles!"addImportFiles"(importPaths, "*.{d,di}");
		dst.removeImportFiles(dst.sourceFiles);
		collectFiles!"addStringImportFiles"(stringImportPaths, "*");

		// ensure a deterministic order of files as passed to the compiler
		dst.sourceFiles.sort();

		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!("debugVersions", "addDebugVersions")(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);
		getPlatformSetting!("buildRequirements", "addRequirements")(dst, platform);
		getPlatformSetting!("buildOptions", "addOptions")(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, string config_name)
	{
		auto nodef = false;
		auto noprop = false;
		foreach (req; this.buildRequirements) {
			if (req & BuildRequirements.noDefaultFlags) nodef = true;
			if (req & BuildRequirements.relaxProperties) noprop = true;
		}

		if (noprop) {
			logWarn(`Warning: "buildRequirements": ["relaxProperties"] is deprecated and is now the default behavior. Note that the -property switch will probably be removed in future versions of DMD.`);
			logWarn("");
		}

		if (nodef) {
			logWarn("Warning: This package uses the \"noDefaultFlags\" build requirement. Please use only for development purposes and not for released packages.");
			logWarn("");
		} else {
			string[] all_dflags;
			BuildOptions all_options;
			foreach (flags; this.dflags) all_dflags ~= flags;
			foreach (options; this.buildOptions) all_options |= options;
			.warnOnSpecialCompilerFlags(all_dflags, all_options, package_name, config_name);
		}
	}
}

/// Returns all package names, starting with the root package in [0].
string[] getSubPackagePath(string package_name)
{
	return package_name.split(":");
}

/// Returns the name of the base package in the case of some sub package or the
/// package itself, if it is already a full package.
string getBasePackageName(string package_name)
{
	return package_name.getSubPackagePath()[0];
}

string getSubPackageName(string package_name)
{
	return getSubPackagePath(package_name)[1 .. $].join(":");
}

private string determineVersionFromSCM(Path path)
{
	import std.process;
	import dub.semver;

	auto git_dir = path ~ ".git";
	if (!existsFile(git_dir) || !isDir(git_dir.toNativeString)) return null;
	auto git_dir_param = "--git-dir=" ~ git_dir.toNativeString();

	static string exec(scope string[] params...) {
		auto ret = executeShell(escapeShellCommand(params));
		if (ret.status == 0) return ret.output.strip;
		logDebug("'%s' failed with exit code %s: %s", params.join(" "), ret.status, ret.output.strip);
		return null;
	}

	if (auto tag = exec("git", git_dir_param, "describe", "--long", "--tags")) {
		auto parts = tag.split("-");
		auto commit = parts[$-1];
		auto num = parts[$-2].to!int;
		tag = parts[0 .. $-2].join("-");
		if (tag.startsWith("v") && isValidVersion(tag[1 .. $])) {
			if (num == 0) return tag[1 .. $];
			else if (tag.canFind("+")) return format("%s.commit.%s.%s", tag[1 .. $], num, commit);
			else return format("%s+commit.%s.%s", tag[1 .. $], num, commit);
		}
	}

	if (auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD")) {
		if (branch != "HEAD") return "~" ~ branch;
	}

	return null;
}