- /**
- Stuff with dependencies.
-
- Copyright: © 2012-2013 Matthias Dondorff, © 2012-2015 Sönke Ludwig
- License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
- Authors: Matthias Dondorff
- */
- module dub.package_;
-
- public import dub.recipe.packagerecipe;
-
- import dub.compilers.compiler;
- import dub.dependency;
- import dub.description;
- import dub.recipe.json;
- import dub.recipe.sdl;
-
- 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.typecons : Nullable;
-
-
- enum PackageFormat {
- json,
- sdl
- }
-
- struct FilenameAndFormat {
- string filename;
- PackageFormat format;
- }
-
- struct PathAndFormat {
- Path path;
- PackageFormat format;
-
- @property bool empty() { return path.empty; }
-
- string toString() const { return path.toString(); }
- }
-
-
- // Supported package descriptions in decreasing order of preference.
- static immutable FilenameAndFormat[] packageInfoFiles = [
- {"dub.json", PackageFormat.json},
- {"dub.sdl", PackageFormat.sdl},
- {"package.json", PackageFormat.json}
- ];
-
- @property string[] packageInfoFilenames() { return packageInfoFiles.map!(f => cast(string)f.filename).array; }
-
- @property 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 {
- private {
- Path m_path;
- PathAndFormat m_infoFile;
- PackageRecipe 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 found in %s, expected one of %s"
- .format(root.toNativeString(), packageInfoFiles.map!(f => cast(string)f.filename).join("/")));
- }
- 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(RawPackage raw_package, Path root = Path(), Package parent = null, string versionOverride = "")
- {
- PackageRecipe recipe;
-
- // parse the Package description
- if(raw_package !is null)
- {
- scope(failure) logError("Failed to parse package description for %s %s in %s.",
- raw_package.package_name, versionOverride.length ? versionOverride : raw_package.version_,
- root.length ? root.toNativeString() : "remote location");
- 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(PackageRecipe recipe, Path root = Path(), Package parent = null)
- {
- m_parentPackage = parent;
- m_path = root;
- m_path.endsWithSlash = true;
-
- // use the given recipe as the basis
- m_info = recipe;
-
- fillWithDefaults();
- 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(PackageRecipe) 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()
- {
- storeInfo(m_path);
- m_infoFile = PathAndFormat(m_path ~ defaultPackageFilename);
- }
- /// ditto
- void storeInfo(Path path)
- const {
- enforce(!ver.isUnknown, "Trying to store a package with an 'unknown' version, this is not supported.");
- auto filename = path ~ defaultPackageFilename;
- auto dstFile = openFile(filename.toNativeString(), FileMode.createTrunc);
- scope(exit) dstFile.close();
- dstFile.writePrettyJsonString(m_info.toJson());
- }
-
- Nullable!PackageRecipe getInternalSubPackage(string name)
- {
- foreach (ref p; m_info.subPackages)
- if (p.path.empty && p.recipe.name == name)
- return Nullable!PackageRecipe(p.recipe);
- return Nullable!PackageRecipe();
- }
-
- 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(BuildOption) 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;
- }
-
- /** Returns a description of the package for use in IDEs or build tools.
- */
- PackageDescription describe(BuildPlatform platform, string config)
- const {
- PackageDescription ret;
- ret.configuration = config;
- ret.path = m_path.toNativeString();
- ret.name = this.name;
- ret.version_ = this.ver;
- ret.description = m_info.description;
- ret.homepage = m_info.homepage;
- ret.authors = m_info.authors.dup;
- ret.copyright = m_info.copyright;
- ret.license = m_info.license;
- ret.dependencies = getDependencies(config).keys;
-
- // save build settings
- BuildSettings bs = getBuildSettings(platform, config);
- BuildSettings allbs = getCombinedBuildSettings();
-
- ret.targetType = bs.targetType;
- ret.targetPath = bs.targetPath;
- ret.targetName = bs.targetName;
- if (ret.targetType != TargetType.none)
- ret.targetFileName = getTargetFileName(bs, platform);
- ret.workingDirectory = bs.workingDirectory;
- ret.mainSourceFile = bs.mainSourceFile;
- ret.dflags = bs.dflags;
- ret.lflags = bs.lflags;
- ret.libs = bs.libs;
- ret.copyFiles = bs.copyFiles;
- ret.versions = bs.versions;
- ret.debugVersions = bs.debugVersions;
- ret.importPaths = bs.importPaths;
- ret.stringImportPaths = bs.stringImportPaths;
- ret.preGenerateCommands = bs.preGenerateCommands;
- ret.postGenerateCommands = bs.postGenerateCommands;
- ret.preBuildCommands = bs.preBuildCommands;
- ret.postBuildCommands = bs.postBuildCommands;
-
- // prettify build requirements output
- for (int i = 1; i <= BuildRequirement.max; i <<= 1)
- if (bs.requirements & cast(BuildRequirement)i)
- ret.buildRequirements ~= cast(BuildRequirement)i;
-
- // prettify options output
- for (int i = 1; i <= BuildOption.max; i <<= 1)
- if (bs.options & cast(BuildOption)i)
- ret.options ~= cast(BuildOption)i;
-
- // collect all possible source files and determine their types
- SourceFileRole[string] sourceFileTypes;
- foreach (f; allbs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.unusedStringImport;
- foreach (f; allbs.importFiles) sourceFileTypes[f] = SourceFileRole.unusedImport;
- foreach (f; allbs.sourceFiles) sourceFileTypes[f] = SourceFileRole.unusedSource;
- foreach (f; bs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.stringImport;
- foreach (f; bs.importFiles) sourceFileTypes[f] = SourceFileRole.import_;
- foreach (f; bs.sourceFiles) sourceFileTypes[f] = SourceFileRole.source;
- foreach (f; sourceFileTypes.byKey.array.sort()) {
- SourceFileDescription sf;
- sf.path = f;
- sf.type = sourceFileTypes[f];
- ret.files ~= sf;
- }
-
- return ret;
- }
- // ditto
- deprecated void describe(ref Json dst, BuildPlatform platform, string config)
- {
- auto res = describe(platform, config);
- foreach (string key, value; res.serializeToJson())
- dst[key] = value;
- }
-
- private void fillWithDefaults()
- {
- auto bs = &m_info.buildSettings;
-
- // check for default string import folders
- if ("" !in bs.stringImportPaths) {
- foreach(defvf; ["views"]){
- if( existsFile(m_path ~ defvf) )
- bs.stringImportPaths[""] ~= defvf;
- }
- }
-
- // check for default source folders
- immutable hasSP = ("" in bs.sourcePaths) !is null;
- immutable hasIP = ("" in bs.importPaths) !is null;
- if (!hasSP || !hasIP) {
- foreach (defsf; ["source/", "src/"]) {
- if (existsFile(m_path ~ defsf)) {
- if (!hasSP) bs.sourcePaths[""] ~= defsf;
- if (!hasIP) bs.importPaths[""] ~= defsf;
- }
- }
- }
-
- // check for default app_main
- string app_main_file;
- auto pkg_name = m_info.name.length ? m_info.name : "unknown";
- foreach(sf; bs.sourcePaths.get("", null)){
- auto p = m_path ~ sf;
- if( !existsFile(p) ) continue;
- foreach(fil; ["app.d", "main.d", pkg_name ~ "/main.d", pkg_name ~ "/" ~ "app.d"]){
- if( existsFile(p ~ fil) ) {
- app_main_file = (Path(sf) ~ fil).toNativeString();
- break;
- }
- }
- }
-
- // generate default configurations if none are defined
- if (m_info.configurations.length == 0) {
- if (bs.targetType == TargetType.executable) {
- BuildSettingsTemplate app_settings;
- app_settings.targetType = TargetType.executable;
- if (bs.mainSourceFile.empty) app_settings.mainSourceFile = app_main_file;
- m_info.configurations ~= ConfigurationInfo("application", app_settings);
- } else if (bs.targetType != TargetType.none) {
- BuildSettingsTemplate lib_settings;
- lib_settings.targetType = bs.targetType == TargetType.autodetect ? TargetType.library : bs.targetType;
-
- if (bs.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);
- }
- }
- }
-
- 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;
-
- string text;
-
- {
- auto f = openFile(file.path.toNativeString(), FileMode.read);
- scope(exit) f.close();
- text = stripUTF8Bom(cast(string)f.readAll());
- }
-
- final switch(file.format) {
- case PackageFormat.json:
- return new JsonPackage(parseJsonString(text, file.path.toNativeString()));
- case PackageFormat.sdl:
- return new SdlPackage(text, file.path.toNativeString());
- }
- }
-
- static abstract class RawPackage
- {
- string package_name; // Should already be lower case
- string version_;
- abstract void parseInto(ref PackageRecipe package_, string parent_name);
- }
- 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.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 PackageRecipe recipe, string parent_name)
- {
- recipe.parseJson(json, parent_name);
- }
- }
-
- private static class SdlPackage : RawPackage
- {
- import dub.internal.sdlang;
- Tag sdl;
-
- this(string sdl_text, string filename)
- {
- this.sdl = parseSource(sdl_text, filename);
- foreach (t; this.sdl.tags) {
- switch (t.name) {
- default: break;
- case "name":
- this.package_name = t.values[0].get!string.toLower();
- break;
- case "version":
- this.version_ = t.values[0].get!string;
- break;
- }
- }
- }
-
- override void parseInto(ref PackageRecipe recipe, string parent_name)
- {
- recipe.parseSDL(sdl, parent_name);
- }
- }
- }
-
- 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;
- }
-
- auto tag = exec("git", git_dir_param, "describe", "--long", "--tags");
- if (tag !is null) {
- 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);
- }
- }
-
- auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD");
- if (branch !is null) {
- if (branch != "HEAD") return "~" ~ branch;
- }
-
- return null;
- }