/** 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_; public import dub.recipe.packagerecipe; import dub.compilers.compiler; import dub.dependency; 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.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; 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 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 = "") { PackageRecipe 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(PackageRecipe 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(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() { 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 PackageRecipe 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 PackageRecipe recipe, string parent_name) const { recipe.parseJson(json, parent_name); } } private static class SdlPackage : RawPackage { override void parseInto(ref PackageRecipe package_, string parent_name) const { throw new Exception("SDL packages not implemented yet"); } } } 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; }