Newer
Older
dub_jkp / source / dub / package_.d
@Sönke Ludwig Sönke Ludwig on 7 Mar 2013 17 KB Added the notion of a target type.
/**
	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.sourcePaths ~= defsf;
				m_info.buildSettings.importPaths[""] ~= defsf;
				if( existsFile(p ~ "app.d") ) app_files ~= defsf ~ "/app.d";
				else if( existsFile(p ~ (pkg_name~".d")) ) app_files ~= defsf ~ "/"~pkg_name~".d";
			}
		}

		// parse the JSON description
		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["application"] = ConfigurationInfo(app_settings);
			} else {
				if( m_info.buildSettings.targetType == TargetType.autodetect ){
					if( app_files.length ){
						BuildSettingsTemplate app_settings;
						app_settings.targetType = TargetType.executable;
						app_settings.sourceFiles[""] = app_files;
						m_info.configurations["application"] = ConfigurationInfo(app_settings);
					}
				}

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

		// determine all source folders
		Path[] source_paths;
		foreach(p; m_info.sourcePaths)
			source_paths ~= this.path ~ p;
		logTrace("Source paths for %s: %s", this.name, source_paths);

		// gather all source files
		string[] sources;
		foreach(sourcePath; source_paths.map!(p => p.toNativeString())()) {
			logTrace("Parsing directories for source path: %s", sourcePath);

			foreach(d; dirEntries(sourcePath, "*d", SpanMode.depth))
			{
				// direct assignment allSources ~= Path(d.name)[...] 
				// spawns internal compiler/linker error
				if(isDir(d.name)) continue;
				auto p = Path(d.name);
				auto src = p.relativeTo(this.path);
				if( m_info.buildSettings.targetType != TargetType.autodetect || !app_files.map!(p => Path(p))().canFind(src) )
					sources ~= src.toNativeString();
			}
		}
		logTrace("allSources: %s", sources);
		m_info.buildSettings.sourceFiles[""] ~= sources;
	}
	
	@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 Path binaryPath()
	const {
		auto p = m_info.binaryPath;
		if( !p.length ) return this.path;
		return this.path ~ Path(p);
	}
	
	@property string[] configurations()
	const {
		auto ret = appender!(string[])();
		foreach( config; m_info.configurations.byKey )
			ret.put(config);
		return ret.data;
	}

	/// Returns all BuildSettings for the given platform and config.
	BuildSettings getBuildSettings(BuildPlatform platform, string config)
	const {
		assert(config in m_info.configurations, "Unknown configuration for "~m_info.name~": "~config);
		BuildSettings ret;
		m_info.buildSettings.getPlatformSettings(ret, platform);
		m_info.configurations[config].buildSettings.getPlatformSettings(ret, platform);
		return ret;
	}

	bool isAppSource(string src)
	const {
		auto ps = Path(src);
		if( ps.absolute ) ps = ps.relativeTo(this.path);
		return ps == Path("source/app.d") || ps == Path("src/app.d");
	}
	
	/// Returns the default configuration to build for the given platform
	string getDefaultConfiguration(BuildPlatform platform, bool is_app = false)
	const {
		string ret;
		foreach(suffix; getPlatformSuffixIterator(platform))
			if( auto pc = suffix in m_info.defaultConfiguration )
				return *pc;
		return is_app && "application" in m_info.configurations ? "application" : "library";
	}

	/// Humanly readible 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 binaryPath;
	string description;
	string homepage;
	string[] authors;
	string copyright;
	string license;
	Dependency[string] dependencies;
	string[] sourcePaths;
	BuildSettingsTemplate buildSettings;
	string[string] defaultConfiguration;
	ConfigurationInfo[string] configurations;

	void parseJson(Json json)
	{
		foreach( string field, value; json ){
			switch(field){
				default:
					auto didx = std.string.indexOf(field, "-");
					string basename, suffix;
					if( didx >= 0 ) basename = field[0 .. didx], suffix = field[didx .. $];
					else basename = field;
					if( basename == "defaultConfiguration" ){
						this.defaultConfiguration[suffix] = value.get!string();
					}
					break;
				case "name": this.name = value.get!string; break;
				case "version": this.version_ = value.get!string; break;
				case "binaryPath": this.binaryPath = 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 "sourcePath": this.sourcePaths = [value.get!string()]; break; // deprecated
				case "sourcePaths": this.sourcePaths = deserializeJson!(string[])(value); break;
				case "configurations":
					foreach( string config, settings; value ){
						ConfigurationInfo ci;
						ci.parseJson(settings);
						this.configurations[config] = 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 = Json.EmptyObject;
		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.sourcePaths.empty ) ret.sourcePaths = serializeToJson(this.sourcePaths);
		if( this.configurations ){
			auto configs = Json.EmptyObject;
			foreach( suffix, conf; defaultConfiguration )
				configs["default"~suffix] = conf;
			foreach(config, settings; this.configurations)
				configs[config] = settings.buildSettings.toJson();
			ret.configurations = configs;
		}
		return ret;
	}
}

struct ConfigurationInfo {
	BuildSettingsTemplate buildSettings;

	void parseJson(Json json)
	{

		foreach(string name, value; json){
			switch(name){
				default: break;
			}
		}

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

struct BuildSettingsTemplate {
	TargetType targetType = TargetType.autodetect;
	string[][string] dflags;
	string[][string] lflags;
	string[][string] libs;
	string[][string] sourceFiles;
	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 "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": logWarn(`The "files" field has been deprecated, please use "sourceFiles" instad.`); goto case;
				case "sourceFiles": this.sourceFiles[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;
		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; 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, BuildPlatform platform)
	const {
		dst.targetType = this.targetType;
		getPlatformSetting!("dflags", "addDFlags")(dst, platform);
		getPlatformSetting!("lflags", "addLFlags")(dst, platform);
		getPlatformSetting!("libs", "addLibs")(dst, platform);
		getPlatformSetting!("sourceFiles", "addSourceFiles")(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, BuildPlatform platform)
	const {
		foreach(suffix, values; __traits(getMember, this, name)){
			if( matchesPlatform(suffix, platform) )
				__traits(getMember, dst, addname)(values);
		}
	}
}


private bool matchesPlatform(string suffix, BuildPlatform platform)
{
	if( suffix.length == 0 ) return true;
	// TODO: optimize
	foreach( psuffix; getPlatformSuffixIterator(platform) )
		if( psuffix == suffix )
			return true;
	return false;
}

/// Based on the BuildPlatform, creates an iterator with all suffixes.
///
/// Suffixes are build upon the following scheme, where each component
/// is optional (indicated by []), but the order is obligatory.
/// "[-platform][-architecture][-compiler]"
///
/// So the following strings are valid suffixes:
/// "-windows-x86-dmd"
/// "-dmd"
/// "-arm"
///
private int delegate(scope int delegate(ref string)) getPlatformSuffixIterator(BuildPlatform platform)
{
	int iterator(scope int delegate(ref string s) del)
	{
		auto c = platform.compiler;
		int delwrap(string s) { return del(s); }
		if( auto ret = delwrap(null) ) return ret;
		if( auto ret = delwrap("-"~c) ) return ret;
		foreach( p; platform.platform ){
			if( auto ret = delwrap("-"~p) ) return ret;
			if( auto ret = delwrap("-"~p~"-"~c) ) return ret;
			foreach( a; platform.architecture ){
				if( auto ret = delwrap("-"~p~"-"~a) ) return ret;
				if( auto ret = delwrap("-"~p~"-"~a~"-"~c) ) return ret;
			}
		}
		foreach( a; platform.architecture ){
			if( auto ret = delwrap("-"~a) ) return ret;
			if( auto ret = delwrap("-"~a~"-"~c) ) return ret;
		}
		return 0;
	}
	return &iterator;
}