diff --git a/build-files.txt b/build-files.txt index 06451c4..3d48908 100644 --- a/build-files.txt +++ b/build-files.txt @@ -16,6 +16,7 @@ source/dub/compilers/dmd.d source/dub/compilers/gdc.d source/dub/compilers/ldc.d +source/dub/compilers/utils.d source/dub/generators/build.d source/dub/generators/cmake.d source/dub/generators/generator.d diff --git a/source/dub/commandline.d b/source/dub/commandline.d index d933c86..49a2bc7 100644 --- a/source/dub/commandline.d +++ b/source/dub/commandline.d @@ -1,7 +1,7 @@ /** Defines the behavior of the DUB command line client. - Copyright: © 2012-2013 Matthias Dondorff, Copyright © 2012-2014 Sönke Ludwig + Copyright: © 2012-2013 Matthias Dondorff, Copyright © 2012-2016 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Matthias Dondorff, Sönke Ludwig */ @@ -36,6 +36,10 @@ import std.variant; +/** Retrieves a list of all available commands. + + Commands are grouped by category. +*/ CommandGroup[] getCommands() { return [ @@ -73,6 +77,16 @@ ]; } + +/** Processes the given command line and executes the appropriate actions. + + Params: + args = This command line argument array as received in `main`. The first + entry is considered to be the name of the binary invoked. + + Returns: + Returns the exit code that is supposed to be returned to the system. +*/ int runDubCommandLine(string[] args) { logDiagnostic("DUB version %s", getDUBVersion()); @@ -193,11 +207,13 @@ if (!cmd.skipDubInitialization) { if (options.bare) { dub = new Dub(Path(getcwd())); + dub.defaultPlacementLocation = options.placementLocation; } else { // initialize DUB auto package_suppliers = options.registry_urls.map!(url => cast(PackageSupplier)new RegistryPackageSupplier(URL(url))).array; - dub = new Dub(package_suppliers, options.root_path, options.skipRegistry); + dub = new Dub(options.root_path, package_suppliers, options.skipRegistry); dub.dryRun = options.annotate; + dub.defaultPlacementLocation = options.placementLocation; // make the CWD package available so that for example sub packages can reference their // parent package. @@ -221,13 +237,18 @@ } } + +/** Contains and parses options common to all commands. +*/ struct CommonOptions { bool verbose, vverbose, quiet, vquiet; bool help, annotate, bare; string[] registry_urls; string root_path; - SkipRegistry skipRegistry = SkipRegistry.none; + SkipPackageSuppliers skipRegistry = SkipPackageSuppliers.none; + PlacementLocation placementLocation = PlacementLocation.user; + /// Parses all common options and stores the result in the struct instance. void prepare(CommandArgs args) { args.getopt("h|help", &help, ["Display general or command specific help"]); @@ -245,10 +266,17 @@ args.getopt("vverbose", &vverbose, ["Print debug output"]); args.getopt("q|quiet", &quiet, ["Only print warnings and errors"]); args.getopt("vquiet", &vquiet, ["Print no messages"]); - args.getopt("cache", &defaultPlacementLocation, ["Puts any fetched packages in the specified location [local|system|user]."]); + args.getopt("cache", &placementLocation, ["Puts any fetched packages in the specified location [local|system|user]."]); } } +/** Encapsulates a set of application arguments. + + This class serves two purposes. The first is to provide an API for parsing + command line arguments (`getopt`). At the same time it records all calls + to `getopt` and provides a list of all possible options using the + `recognizedArgs` property. +*/ class CommandArgs { struct Arg { Variant defaultValue; @@ -261,11 +289,20 @@ Arg[] m_recognizedArgs; } + /** Initializes the list of source arguments. + + Note that all array entries are considered application arguments (i.e. + no application name entry is present as the first entry) + */ this(string[] args) { m_args = "dummy" ~ args; } + /** Returns the list of all options recognized. + + This list is created by recording all calls to `getopt`. + */ @property const(Arg)[] recognizedArgs() { return m_recognizedArgs; } void getopt(T)(string names, T* var, string[] help_text = null) @@ -286,11 +323,15 @@ m_recognizedArgs ~= arg; } + /** Resets the list of available source arguments. + */ void dropAllArgs() { m_args = null; } + /** Returns the list of unprocessed arguments and calls `dropAllArgs`. + */ string[] extractRemainingArgs() { auto ret = m_args[1 .. $]; @@ -299,6 +340,13 @@ } } + +/** Base class for all commands. + + This cass contains a high-level description of the command, including brief + and full descriptions and a human readable command line pattern. On top of + that it defines the two main entry functions for command execution. +*/ class Command { string name; string argumentsPattern; @@ -308,7 +356,20 @@ bool hidden = false; // used for deprecated commands bool skipDubInitialization = false; + /** Parses all known command line options without executing any actions. + + This function will be called prior to execute, or may be called as + the only method when collecting the list of recognized command line + options. + + Only `args.getopt` should be called within this method. + */ abstract void prepare(scope CommandArgs args); + + /** Executes the actual action. + + Note that `prepare` will be called before any call to `execute`. + */ abstract int execute(Dub dub, string[] free_args, string[] app_args); private bool loadCwdPackage(Dub dub, bool warn_missing_package) @@ -333,14 +394,20 @@ return false; } - dub.loadPackageFromCwd(); + dub.loadPackage(); return true; } } + +/** Encapsulates a group of commands that fit into a common category. +*/ struct CommandGroup { + /// Caption of the command category string caption; + + /// List of commands contained inthis group Command[] commands; this(string caption, Command[] commands...) @@ -431,7 +498,7 @@ auto ver = dub.getLatestVersion(depname); auto dep = ver.isBranch ? Dependency(ver) : Dependency("~>" ~ ver.toString()); p.buildSettings.dependencies[depname] = dep; - logInfo("Added dependency %s %s", depname, dep.versionString); + logInfo("Added dependency %s %s", depname, dep.versionSpec); } catch (Exception e) { logError("Could not find package '%s'.", depname); logDebug("Full error: %s", e.toString().sanitize); @@ -907,15 +974,22 @@ auto config = m_buildConfig.length ? m_buildConfig : m_defaultConfig; - if (m_importPaths) { - dub.listImportPaths(m_buildPlatform, config, m_buildType, m_dataNullDelim); - } else if (m_stringImportPaths) { - dub.listStringImportPaths(m_buildPlatform, config, m_buildType, m_dataNullDelim); - } else if (m_data) { - dub.listProjectData(m_buildPlatform, config, m_buildType, m_data, - m_dataList? null : m_compiler, m_dataNullDelim); + GeneratorSettings settings; + settings.platform = m_buildPlatform; + settings.config = config; + settings.buildType = m_buildType; + settings.compiler = m_compiler; + + if (m_importPaths) { m_data = ["import-paths"]; m_dataList = true; } + else if (m_stringImportPaths) { m_data = ["string-import-paths"]; m_dataList = true; } + + if (m_data.length) { + ListBuildSettingsFormat lt; + with (ListBuildSettingsFormat) + lt = m_dataList ? (m_dataNullDelim ? listNul : list) : (m_dataNullDelim ? commandLineNul : commandLine); + dub.listProjectData(settings, m_data, lt); } else { - auto desc = dub.project.describe(m_buildPlatform, config, m_buildType); + auto desc = dub.project.describe(settings); writeln(desc.serializeToPrettyJson()); } @@ -1013,7 +1087,7 @@ enforceUsage(free_args.length <= 1, "Unexpected arguments."); enforceUsage(app_args.length == 0, "Unexpected application arguments."); enforceUsage(!m_verify, "--verify is not yet implemented."); - dub.loadPackageFromCwd(); + dub.loadPackage(); logInfo("Upgrading project in %s", dub.projectPath.toNativeString()); auto options = UpgradeOptions.upgrade|UpgradeOptions.select; if (m_missingOnly) options &= ~UpgradeOptions.upgrade; @@ -1085,7 +1159,7 @@ enforceUsage(free_args.length == 1, "Expecting exactly one argument."); enforceUsage(app_args.length == 0, "Unexpected application arguments."); - auto location = defaultPlacementLocation; + auto location = dub.defaultPlacementLocation; if (m_local) { logWarn("--local is deprecated. Use --cache=local instead."); @@ -1153,7 +1227,7 @@ enforceUsage(app_args.length == 0, "Unexpected application arguments."); auto package_id = free_args[0]; - auto location = defaultPlacementLocation; + auto location = dub.defaultPlacementLocation; if (m_local) { logWarn("--local is deprecated. Use --cache=local instead."); @@ -1299,7 +1373,7 @@ { logInfo("Packages present in the system and known to dub:"); foreach (p; dub.packageManager.getPackageIterator()) - logInfo(" %s %s: %s", p.name, p.ver, p.path.toNativeString()); + logInfo(" %s %s: %s", p.name, p.version_, p.path.toNativeString()); logInfo(""); return 0; } @@ -1612,7 +1686,7 @@ if (subp.path.length) { auto sub_path = base_path ~ Path(subp.path); auto pack = prj.packageManager.getOrLoadPackage(sub_path); - fixPathDependencies(pack.info, sub_path); + fixPathDependencies(pack.recipe, sub_path); pack.storeInfo(sub_path); } else fixPathDependencies(subp.recipe, base_path); } @@ -1627,7 +1701,7 @@ copyFolderRec(pack.path, dst_path); // adjust all path based dependencies - fixPathDependencies(pack.info, dst_path); + fixPathDependencies(pack.recipe, dst_path); // overwrite package description file with additional version information pack.storeInfo(dst_path); diff --git a/source/dub/compilers/buildsettings.d b/source/dub/compilers/buildsettings.d index 2db92dd..ea86742 100644 --- a/source/dub/compilers/buildsettings.d +++ b/source/dub/compilers/buildsettings.d @@ -302,7 +302,7 @@ @ignore BitFlags!BuildOption values; this(BuildOption opt) { values = opt; } this(BitFlags!BuildOption v) { values = v; } - deprecated("Use BuildOption.* instead.") { + deprecated("Use BuildOption.* instead. Will be removed for version 1.0.0.") { enum none = BuildOption.none; enum debugMode = BuildOption.debugMode; enum releaseMode = BuildOption.releaseMode; diff --git a/source/dub/compilers/compiler.d b/source/dub/compilers/compiler.d index bb34cde..5396fa0 100644 --- a/source/dub/compilers/compiler.d +++ b/source/dub/compilers/compiler.d @@ -1,17 +1,15 @@ /** Compiler settings and abstraction. - Copyright: © 2013-2014 rejectedsoftware e.K. + Copyright: © 2013-2016 rejectedsoftware e.K. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ module dub.compilers.compiler; public import dub.compilers.buildsettings; +public import dub.platform : BuildPlatform, matchesSpecification; -import dub.compilers.dmd; -import dub.compilers.gdc; -import dub.compilers.ldc; import dub.internal.vibecompat.core.file; import dub.internal.vibecompat.core.log; import dub.internal.vibecompat.data.json; @@ -24,13 +22,13 @@ import std.process; -static this() -{ - registerCompiler(new DmdCompiler); - registerCompiler(new GdcCompiler); - registerCompiler(new LdcCompiler); -} +/** Returns a compiler handler for a given binary name. + The name will be compared against the canonical name of each registered + compiler handler. If no match is found, the sub strings "dmd", "gdc" and + "ldc", in this order, will be searched within the name. If this doesn't + yield a match either, an exception will be thrown. +*/ Compiler getCompiler(string name) { foreach (c; s_compilers) @@ -45,7 +43,17 @@ throw new Exception("Unknown compiler: "~name); } -string defaultCompiler() +/** Returns the binary name of the default compiler for the system. + + When calling this function for the first time, it will search the PATH + environment variable for files named "dmd", "gdc", "gdmd", "ldc2", "ldmd2" + (in that order) and return the first match. If no match is found, "dmd" is + returned. + + See_Also: `Dub.defaultCompiler` +*/ +deprecated("Use Dub.defaultCompiler instead. Will be removed for version 1.0.0.") +@property string defaultCompiler() { static string name; if (!name.length) name = findCompiler(); @@ -65,177 +73,28 @@ return res.empty ? compilers[0] : res.front; } +/** Registers a new compiler handler. + + Note that by default `DMDCompiler`, `GDCCompiler` and `LDCCompiler` are + already registered at startup. +*/ void registerCompiler(Compiler c) { s_compilers ~= c; } -void warnOnSpecialCompilerFlags(string[] compiler_flags, BuildOptions options, string package_name, string config_name) -{ - struct SpecialFlag { - string[] flags; - string alternative; - } - static immutable SpecialFlag[] s_specialFlags = [ - {["-c", "-o-"], "Automatically issued by DUB, do not specify in dub.json"}, - {["-w", "-Wall", "-Werr"], `Use "buildRequirements" to control warning behavior`}, - {["-property", "-fproperty"], "Using this flag may break building of dependencies and it will probably be removed from DMD in the future"}, - {["-wi"], `Use the "buildRequirements" field to control warning behavior`}, - {["-d", "-de", "-dw"], `Use the "buildRequirements" field to control deprecation behavior`}, - {["-of"], `Use "targetPath" and "targetName" to customize the output file`}, - {["-debug", "-fdebug", "-g"], "Call dub with --build=debug"}, - {["-release", "-frelease", "-O", "-inline"], "Call dub with --build=release"}, - {["-unittest", "-funittest"], "Call dub with --build=unittest"}, - {["-lib"], `Use {"targetType": "staticLibrary"} or let dub manage this`}, - {["-D"], "Call dub with --build=docs or --build=ddox"}, - {["-X"], "Call dub with --build=ddox"}, - {["-cov"], "Call dub with --build=cov or --build=unittest-cov"}, - {["-profile"], "Call dub with --build=profile"}, - {["-version="], `Use "versions" to specify version constants in a compiler independent way`}, - {["-debug="], `Use "debugVersions" to specify version constants in a compiler independent way`}, - {["-I"], `Use "importPaths" to specify import paths in a compiler independent way`}, - {["-J"], `Use "stringImportPaths" to specify import paths in a compiler independent way`}, - {["-m32", "-m64"], `Use --arch=x86/--arch=x86_64 to specify the target architecture`} - ]; - - struct SpecialOption { - BuildOption[] flags; - string alternative; - } - static immutable SpecialOption[] s_specialOptions = [ - {[BuildOption.debugMode], "Call DUB with --build=debug"}, - {[BuildOption.releaseMode], "Call DUB with --build=release"}, - {[BuildOption.coverage], "Call DUB with --build=cov or --build=unittest-cov"}, - {[BuildOption.debugInfo], "Call DUB with --build=debug"}, - {[BuildOption.inline], "Call DUB with --build=release"}, - {[BuildOption.noBoundsCheck], "Call DUB with --build=release-nobounds"}, - {[BuildOption.optimize], "Call DUB with --build=release"}, - {[BuildOption.profile], "Call DUB with --build=profile"}, - {[BuildOption.unittests], "Call DUB with --build=unittest"}, - {[BuildOption.warnings, BuildOption.warningsAsErrors], "Use \"buildRequirements\" to control the warning level"}, - {[BuildOption.ignoreDeprecations, BuildOption.deprecationWarnings, BuildOption.deprecationErrors], "Use \"buildRequirements\" to control the deprecation warning level"}, - {[BuildOption.property], "This flag is deprecated and has no effect"} - ]; - - bool got_preamble = false; - void outputPreamble() - { - if (got_preamble) return; - got_preamble = true; - logWarn(""); - if (config_name.empty) logWarn("## Warning for package %s ##", package_name); - else logWarn("## Warning for package %s, configuration %s ##", package_name, config_name); - logWarn(""); - logWarn("The following compiler flags have been specified in the package description"); - logWarn("file. They are handled by DUB and direct use in packages is discouraged."); - logWarn("Alternatively, you can set the DFLAGS environment variable to pass custom flags"); - logWarn("to the compiler, or use one of the suggestions below:"); - logWarn(""); - } - - foreach (f; compiler_flags) { - foreach (sf; s_specialFlags) { - if (sf.flags.any!(sff => f == sff || (sff.endsWith("=") && f.startsWith(sff)))) { - outputPreamble(); - logWarn("%s: %s", f, sf.alternative); - break; - } - } - } - - foreach (sf; s_specialOptions) { - foreach (f; sf.flags) { - if (options & f) { - outputPreamble(); - logWarn("%s: %s", f, sf.alternative); - break; - } - } - } - - if (got_preamble) logWarn(""); -} - - -/** - Alters the build options to comply with the specified build requirements. -*/ -void enforceBuildRequirements(ref BuildSettings settings) -{ - settings.addOptions(BuildOption.warningsAsErrors); - if (settings.requirements & BuildRequirement.allowWarnings) { settings.options &= ~BuildOption.warningsAsErrors; settings.options |= BuildOption.warnings; } - if (settings.requirements & BuildRequirement.silenceWarnings) settings.options &= ~(BuildOption.warningsAsErrors|BuildOption.warnings); - if (settings.requirements & BuildRequirement.disallowDeprecations) { settings.options &= ~(BuildOption.ignoreDeprecations|BuildOption.deprecationWarnings); settings.options |= BuildOption.deprecationErrors; } - if (settings.requirements & BuildRequirement.silenceDeprecations) { settings.options &= ~(BuildOption.deprecationErrors|BuildOption.deprecationWarnings); settings.options |= BuildOption.ignoreDeprecations; } - if (settings.requirements & BuildRequirement.disallowInlining) settings.options &= ~BuildOption.inline; - if (settings.requirements & BuildRequirement.disallowOptimization) settings.options &= ~BuildOption.optimize; - if (settings.requirements & BuildRequirement.requireBoundsCheck) settings.options &= ~BuildOption.noBoundsCheck; - if (settings.requirements & BuildRequirement.requireContracts) settings.options &= ~BuildOption.releaseMode; - if (settings.requirements & BuildRequirement.relaxProperties) settings.options &= ~BuildOption.property; -} - - -/** - Replaces each referenced import library by the appropriate linker flags. - - This function tries to invoke "pkg-config" if possible and falls back to - direct flag translation if that fails. -*/ -void resolveLibs(ref BuildSettings settings) -{ - import std.string : format; - - if (settings.libs.length == 0) return; - - if (settings.targetType == TargetType.library || settings.targetType == TargetType.staticLibrary) { - logDiagnostic("Ignoring all import libraries for static library build."); - settings.libs = null; - version(Windows) settings.sourceFiles = settings.sourceFiles.filter!(f => !f.endsWith(".lib")).array; - } - - version (Posix) { - try { - enum pkgconfig_bin = "pkg-config"; - - bool exists(string lib) { - return execute([pkgconfig_bin, "--exists", lib]).status == 0; - } - - auto pkgconfig_libs = settings.libs.partition!(l => !exists(l)); - pkgconfig_libs ~= settings.libs[0 .. $ - pkgconfig_libs.length] - .partition!(l => !exists("lib"~l)).map!(l => "lib"~l).array; - settings.libs = settings.libs[0 .. $ - pkgconfig_libs.length]; - if (pkgconfig_libs.length) { - logDiagnostic("Using pkg-config to resolve library flags for %s.", pkgconfig_libs.join(", ")); - auto libflags = execute([pkgconfig_bin, "--libs"] ~ pkgconfig_libs); - enforce(libflags.status == 0, format("pkg-config exited with error code %s: %s", libflags.status, libflags.output)); - foreach (f; libflags.output.split()) { - if (f.startsWith("-L-L")) { - settings.addLFlags(f[2 .. $]); - } else if (f.startsWith("-defaultlib")) { - settings.addDFlags(f); - } else if (f.startsWith("-L-defaultlib")) { - settings.addDFlags(f[2 .. $]); - } else if (f.startsWith("-pthread")) { - settings.addLFlags("-lpthread"); - } else if (f.startsWith("-L-l")) { - settings.addLFlags(f[2 .. $].split(",")); - } else if (f.startsWith("-Wl,")) settings.addLFlags(f[4 .. $].split(",")); - else settings.addLFlags(f); - } - } - if (settings.libs.length) logDiagnostic("Using direct -l... flags for %s.", settings.libs.array.join(", ")); - } catch (Exception e) { - logDiagnostic("pkg-config failed: %s", e.msg); - logDiagnostic("Falling back to direct -l... flags."); - } - } -} - interface Compiler { + /// Returns the canonical name of the compiler (e.g. "dmd"). @property string name() const; + /** Determines the build platform properties given a set of build settings. + + This will invoke the compiler to build a platform probe file, which + determines the target build platform's properties during compile-time. + + See_Also: `dub.compilers.utils.generatePlatformProbeFile` + */ BuildPlatform determinePlatform(ref BuildSettings settings, string compiler_binary, string arch_override = null); /// Replaces high level fields with low level fields and converts @@ -257,6 +116,11 @@ /// Convert linker flags to compiler format string[] lflagsToDFlags(in string[] lflags) const; + /** Runs a tool and provides common boilerplate code. + + This method should be used by `Compiler` implementations to invoke the + compiler or linker binary. + */ protected final void invokeTool(string[] args, void delegate(int, string) output_callback) { import std.string; @@ -279,263 +143,6 @@ } } - -/// Represents a platform a package can be build upon. -struct BuildPlatform { - /// e.g. ["posix", "windows"] - string[] platform; - /// e.g. ["x86", "x86_64"] - string[] architecture; - /// Canonical compiler name e.g. "dmd" - string compiler; - /// Compiler binary name e.g. "ldmd2" - string compilerBinary; - /// Compiled frontend version (e.g. 2065) - int frontendVersion; - - enum any = BuildPlatform(null, null, null, null, -1); - - /// Build platforms can be specified via a string specification. - /// - /// Specifications 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 specifications: - /// "-windows-x86-dmd" - /// "-dmd" - /// "-arm" - /// "-arm-dmd" - /// "-windows-dmd" - /// - /// Params: - /// specification = The specification being matched. It must be the empty string or start with a dash. - /// - /// Returns: - /// true if the given specification matches this BuildPlatform, false otherwise. (The empty string matches) - /// - bool matchesSpecification(const(char)[] specification) - const { - import std.string : format; - - if (specification.empty) return true; - if (this == any) return true; - - auto splitted=specification.splitter('-'); - assert(!splitted.empty, "No valid platform specification! The leading hyphen is required!"); - splitted.popFront(); // Drop leading empty match. - enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification)); - if (platform.canFind(splitted.front)) { - splitted.popFront(); - if(splitted.empty) - return true; - } - if (architecture.canFind(splitted.front)) { - splitted.popFront(); - if(splitted.empty) - return true; - } - if (compiler == splitted.front) { - splitted.popFront(); - enforce(splitted.empty, "No valid specification! The compiler has to be the last element!"); - return true; - } - return false; - } - unittest { - auto platform=BuildPlatform(["posix", "linux"], ["x86_64"], "dmd"); - assert(platform.matchesSpecification("-posix")); - assert(platform.matchesSpecification("-linux")); - assert(platform.matchesSpecification("-linux-dmd")); - assert(platform.matchesSpecification("-linux-x86_64-dmd")); - assert(platform.matchesSpecification("-x86_64")); - assert(!platform.matchesSpecification("-windows")); - assert(!platform.matchesSpecification("-ldc")); - assert(!platform.matchesSpecification("-windows-dmd")); - } -} - - -string getTargetFileName(in BuildSettings settings, in BuildPlatform platform) -{ - assert(settings.targetName.length > 0, "No target name set."); - final switch (settings.targetType) { - case TargetType.autodetect: assert(false, "Configurations must have a concrete target type."); - case TargetType.none: return null; - case TargetType.sourceLibrary: return null; - case TargetType.executable: - if( platform.platform.canFind("windows") ) - return settings.targetName ~ ".exe"; - else return settings.targetName; - case TargetType.library: - case TargetType.staticLibrary: - if (platform.platform.canFind("windows") && platform.compiler == "dmd") - return settings.targetName ~ ".lib"; - else return "lib" ~ settings.targetName ~ ".a"; - case TargetType.dynamicLibrary: - if( platform.platform.canFind("windows") ) - return settings.targetName ~ ".dll"; - else return "lib" ~ settings.targetName ~ ".so"; - case TargetType.object: - if (platform.platform.canFind("windows")) - return settings.targetName ~ ".obj"; - else return settings.targetName ~ ".o"; - } -} - - -bool isLinkerFile(string f) -{ - import std.path; - switch (extension(f)) { - default: - return false; - version (Windows) { - case ".lib", ".obj", ".res", ".def": - return true; - } else { - case ".a", ".o", ".so", ".dylib": - return true; - } - } -} - -/// Generate a file that will give, at compile time, informations about the compiler (architecture, frontend version...) -Path generatePlatformProbeFile() -{ - import dub.internal.vibecompat.core.file; - import dub.internal.vibecompat.data.json; - import dub.internal.utils; - - auto path = getTempFile("dub_platform_probe", ".d"); - - auto fil = openFile(path, FileMode.createTrunc); - scope (failure) { - fil.close(); - } - - fil.write(q{ - module dub_platform_probe; - - template toString(int v) { enum toString = v.stringof; } - - pragma(msg, `{`); - pragma(msg,` "compiler": "`~ determineCompiler() ~ `",`); - pragma(msg, ` "frontendVersion": ` ~ toString!__VERSION__ ~ `,`); - pragma(msg, ` "compilerVendor": "` ~ __VENDOR__ ~ `",`); - pragma(msg, ` "platform": [`); - pragma(msg, ` ` ~ determinePlatform()); - pragma(msg, ` ],`); - pragma(msg, ` "architecture": [`); - pragma(msg, ` ` ~ determineArchitecture()); - pragma(msg, ` ],`); - pragma(msg, `}`); - - string determinePlatform() - { - string ret; - version(Windows) ret ~= `"windows", `; - version(linux) ret ~= `"linux", `; - version(Posix) ret ~= `"posix", `; - version(OSX) ret ~= `"osx", `; - version(FreeBSD) ret ~= `"freebsd", `; - version(OpenBSD) ret ~= `"openbsd", `; - version(NetBSD) ret ~= `"netbsd", `; - version(DragonFlyBSD) ret ~= `"dragonflybsd", `; - version(BSD) ret ~= `"bsd", `; - version(Solaris) ret ~= `"solaris", `; - version(AIX) ret ~= `"aix", `; - version(Haiku) ret ~= `"haiku", `; - version(SkyOS) ret ~= `"skyos", `; - version(SysV3) ret ~= `"sysv3", `; - version(SysV4) ret ~= `"sysv4", `; - version(Hurd) ret ~= `"hurd", `; - version(Android) ret ~= `"android", `; - version(Cygwin) ret ~= `"cygwin", `; - version(MinGW) ret ~= `"mingw", `; - return ret; - } - - string determineArchitecture() - { - string ret; - version(X86) ret ~= `"x86", `; - version(X86_64) ret ~= `"x86_64", `; - version(ARM) ret ~= `"arm", `; - version(ARM_Thumb) ret ~= `"arm_thumb", `; - version(ARM_SoftFloat) ret ~= `"arm_softfloat", `; - version(ARM_HardFloat) ret ~= `"arm_hardfloat", `; - version(ARM64) ret ~= `"arm64", `; - version(PPC) ret ~= `"ppc", `; - version(PPC_SoftFP) ret ~= `"ppc_softfp", `; - version(PPC_HardFP) ret ~= `"ppc_hardfp", `; - version(PPC64) ret ~= `"ppc64", `; - version(IA64) ret ~= `"ia64", `; - version(MIPS) ret ~= `"mips", `; - version(MIPS32) ret ~= `"mips32", `; - version(MIPS64) ret ~= `"mips64", `; - version(MIPS_O32) ret ~= `"mips_o32", `; - version(MIPS_N32) ret ~= `"mips_n32", `; - version(MIPS_O64) ret ~= `"mips_o64", `; - version(MIPS_N64) ret ~= `"mips_n64", `; - version(MIPS_EABI) ret ~= `"mips_eabi", `; - version(MIPS_NoFloat) ret ~= `"mips_nofloat", `; - version(MIPS_SoftFloat) ret ~= `"mips_softfloat", `; - version(MIPS_HardFloat) ret ~= `"mips_hardfloat", `; - version(SPARC) ret ~= `"sparc", `; - version(SPARC_V8Plus) ret ~= `"sparc_v8plus", `; - version(SPARC_SoftFP) ret ~= `"sparc_softfp", `; - version(SPARC_HardFP) ret ~= `"sparc_hardfp", `; - version(SPARC64) ret ~= `"sparc64", `; - version(S390) ret ~= `"s390", `; - version(S390X) ret ~= `"s390x", `; - version(HPPA) ret ~= `"hppa", `; - version(HPPA64) ret ~= `"hppa64", `; - version(SH) ret ~= `"sh", `; - version(SH64) ret ~= `"sh64", `; - version(Alpha) ret ~= `"alpha", `; - version(Alpha_SoftFP) ret ~= `"alpha_softfp", `; - version(Alpha_HardFP) ret ~= `"alpha_hardfp", `; - return ret; - } - - string determineCompiler() - { - version(DigitalMars) return "dmd"; - else version(GNU) return "gdc"; - else version(LDC) return "ldc"; - else version(SDC) return "sdc"; - else return null; - } - }); - - fil.close(); - - return path; -} - -BuildPlatform readPlatformProbe(string output) -{ - import std.string; - - // work around possible additional output of the compiler - auto idx1 = output.indexOf("{"); - auto idx2 = output.lastIndexOf("}"); - enforce(idx1 >= 0 && idx1 < idx2, - "Unexpected platform information output - does not contain a JSON object."); - output = output[idx1 .. idx2+1]; - - import dub.internal.vibecompat.data.json; - auto json = parseJsonString(output); - - BuildPlatform build_platform; - build_platform.platform = json.platform.get!(Json[]).map!(e => e.get!string()).array(); - build_platform.architecture = json.architecture.get!(Json[]).map!(e => e.get!string()).array(); - build_platform.compiler = json.compiler.get!string; - build_platform.frontendVersion = json.frontendVersion.get!int; - return build_platform; -} - private { Compiler[] s_compilers; } diff --git a/source/dub/compilers/dmd.d b/source/dub/compilers/dmd.d index a51c108..4a2d868 100644 --- a/source/dub/compilers/dmd.d +++ b/source/dub/compilers/dmd.d @@ -8,6 +8,7 @@ module dub.compilers.dmd; import dub.compilers.compiler; +import dub.compilers.utils; import dub.internal.utils; import dub.internal.vibecompat.core.log; import dub.internal.vibecompat.inet.path; @@ -23,7 +24,9 @@ import std.typecons; -class DmdCompiler : Compiler { +deprecated("Use DMDCompiler instead. Will be removed for version 1.0.0.") alias DmdCompiler = DMDCompiler; + +class DMDCompiler : Compiler { private static immutable s_options = [ tuple(BuildOption.debugMode, ["-debug"]), tuple(BuildOption.releaseMode, ["-release"]), diff --git a/source/dub/compilers/gdc.d b/source/dub/compilers/gdc.d index 3c58cf1..10cc753 100644 --- a/source/dub/compilers/gdc.d +++ b/source/dub/compilers/gdc.d @@ -8,6 +8,7 @@ module dub.compilers.gdc; import dub.compilers.compiler; +import dub.compilers.utils; import dub.internal.utils; import dub.internal.vibecompat.core.log; import dub.internal.vibecompat.inet.path; @@ -23,7 +24,9 @@ import std.typecons; -class GdcCompiler : Compiler { +deprecated("Use GDCCompiler instead. Will be removed for version 1.0.0.") alias GdcCompiler = GDCCompiler; + +class GDCCompiler : Compiler { private static immutable s_options = [ tuple(BuildOption.debugMode, ["-fdebug"]), tuple(BuildOption.releaseMode, ["-frelease"]), diff --git a/source/dub/compilers/ldc.d b/source/dub/compilers/ldc.d index f7dcad2..341ae9a 100644 --- a/source/dub/compilers/ldc.d +++ b/source/dub/compilers/ldc.d @@ -8,6 +8,7 @@ module dub.compilers.ldc; import dub.compilers.compiler; +import dub.compilers.utils; import dub.internal.utils; import dub.internal.vibecompat.core.log; import dub.internal.vibecompat.inet.path; @@ -23,7 +24,9 @@ import std.typecons; -class LdcCompiler : Compiler { +deprecated("Use LDCCompiler instead. Will be removed for version 1.0.0.") alias LdcCompiler = LDCCompiler; + +class LDCCompiler : Compiler { private static immutable s_options = [ tuple(BuildOption.debugMode, ["-d-debug"]), tuple(BuildOption.releaseMode, ["-release"]), diff --git a/source/dub/compilers/utils.d b/source/dub/compilers/utils.d new file mode 100644 index 0000000..6d3217b --- /dev/null +++ b/source/dub/compilers/utils.d @@ -0,0 +1,421 @@ +/** + Utility functionality for compiler class implementations. + + Copyright: © 2013-2016 rejectedsoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module dub.compilers.utils; + +import dub.compilers.buildsettings; +import dub.platform; +import dub.internal.vibecompat.core.log; +import dub.internal.vibecompat.inet.path; +import std.algorithm : canFind; + + +/** + Given a set of build settings and a target platform, determines the target + binary file name. + + The returned string contains the file name, as well as the platform + specific file extension. The directory is not included. +*/ +string getTargetFileName(in BuildSettings settings, in BuildPlatform platform) +{ + assert(settings.targetName.length > 0, "No target name set."); + final switch (settings.targetType) { + case TargetType.autodetect: assert(false, "Configurations must have a concrete target type."); + case TargetType.none: return null; + case TargetType.sourceLibrary: return null; + case TargetType.executable: + if (platform.platform.canFind("windows")) + return settings.targetName ~ ".exe"; + else return settings.targetName; + case TargetType.library: + case TargetType.staticLibrary: + if (platform.platform.canFind("windows") && platform.compiler == "dmd") + return settings.targetName ~ ".lib"; + else return "lib" ~ settings.targetName ~ ".a"; + case TargetType.dynamicLibrary: + if( platform.platform.canFind("windows") ) + return settings.targetName ~ ".dll"; + else return "lib" ~ settings.targetName ~ ".so"; + case TargetType.object: + if (platform.platform.canFind("windows")) + return settings.targetName ~ ".obj"; + else return settings.targetName ~ ".o"; + } +} + + +/** + Alters the build options to comply with the specified build requirements. + + And enabled options that do not comply will get disabled. +*/ +void enforceBuildRequirements(ref BuildSettings settings) +{ + settings.addOptions(BuildOption.warningsAsErrors); + if (settings.requirements & BuildRequirement.allowWarnings) { settings.options &= ~BuildOption.warningsAsErrors; settings.options |= BuildOption.warnings; } + if (settings.requirements & BuildRequirement.silenceWarnings) settings.options &= ~(BuildOption.warningsAsErrors|BuildOption.warnings); + if (settings.requirements & BuildRequirement.disallowDeprecations) { settings.options &= ~(BuildOption.ignoreDeprecations|BuildOption.deprecationWarnings); settings.options |= BuildOption.deprecationErrors; } + if (settings.requirements & BuildRequirement.silenceDeprecations) { settings.options &= ~(BuildOption.deprecationErrors|BuildOption.deprecationWarnings); settings.options |= BuildOption.ignoreDeprecations; } + if (settings.requirements & BuildRequirement.disallowInlining) settings.options &= ~BuildOption.inline; + if (settings.requirements & BuildRequirement.disallowOptimization) settings.options &= ~BuildOption.optimize; + if (settings.requirements & BuildRequirement.requireBoundsCheck) settings.options &= ~BuildOption.noBoundsCheck; + if (settings.requirements & BuildRequirement.requireContracts) settings.options &= ~BuildOption.releaseMode; + if (settings.requirements & BuildRequirement.relaxProperties) settings.options &= ~BuildOption.property; +} + + +/** + Determines if a specific file name has the extension of a linker file. + + Linker files include static/dynamic libraries, resource files, object files + and DLL definition files. +*/ +bool isLinkerFile(string f) +{ + import std.path; + switch (extension(f)) { + default: + return false; + version (Windows) { + case ".lib", ".obj", ".res", ".def": + return true; + } else { + case ".a", ".o", ".so", ".dylib": + return true; + } + } +} + +unittest { + version (Windows) { + assert(isLinkerFile("test.obj")); + assert(isLinkerFile("test.lib")); + assert(isLinkerFile("test.res")); + assert(!isLinkerFile("test.o")); + assert(!isLinkerFile("test.d")); + } else { + assert(isLinkerFile("test.o")); + assert(isLinkerFile("test.a")); + assert(isLinkerFile("test.so")); + assert(isLinkerFile("test.dylib")); + assert(!isLinkerFile("test.obj")); + assert(!isLinkerFile("test.d")); + } +} + + +/** + Replaces each referenced import library by the appropriate linker flags. + + This function tries to invoke "pkg-config" if possible and falls back to + direct flag translation if that fails. +*/ +void resolveLibs(ref BuildSettings settings) +{ + import std.string : format; + + if (settings.libs.length == 0) return; + + if (settings.targetType == TargetType.library || settings.targetType == TargetType.staticLibrary) { + logDiagnostic("Ignoring all import libraries for static library build."); + settings.libs = null; + version(Windows) settings.sourceFiles = settings.sourceFiles.filter!(f => !f.endsWith(".lib")).array; + } + + version (Posix) { + import std.algorithm : any, map, partition, startsWith; + import std.array : array, join, split; + import std.exception : enforce; + import std.process : execute; + + try { + enum pkgconfig_bin = "pkg-config"; + + bool exists(string lib) { + return execute([pkgconfig_bin, "--exists", lib]).status == 0; + } + + auto pkgconfig_libs = settings.libs.partition!(l => !exists(l)); + pkgconfig_libs ~= settings.libs[0 .. $ - pkgconfig_libs.length] + .partition!(l => !exists("lib"~l)).map!(l => "lib"~l).array; + settings.libs = settings.libs[0 .. $ - pkgconfig_libs.length]; + + if (pkgconfig_libs.length) { + logDiagnostic("Using pkg-config to resolve library flags for %s.", pkgconfig_libs.join(", ")); + auto libflags = execute([pkgconfig_bin, "--libs"] ~ pkgconfig_libs); + enforce(libflags.status == 0, format("pkg-config exited with error code %s: %s", libflags.status, libflags.output)); + foreach (f; libflags.output.split()) { + if (f.startsWith("-L-L")) { + settings.addLFlags(f[2 .. $]); + } else if (f.startsWith("-defaultlib")) { + settings.addDFlags(f); + } else if (f.startsWith("-L-defaultlib")) { + settings.addDFlags(f[2 .. $]); + } else if (f.startsWith("-pthread")) { + settings.addLFlags("-lpthread"); + } else if (f.startsWith("-L-l")) { + settings.addLFlags(f[2 .. $].split(",")); + } else if (f.startsWith("-Wl,")) settings.addLFlags(f[4 .. $].split(",")); + else settings.addLFlags(f); + } + } + if (settings.libs.length) logDiagnostic("Using direct -l... flags for %s.", settings.libs.array.join(", ")); + } catch (Exception e) { + logDiagnostic("pkg-config failed: %s", e.msg); + logDiagnostic("Falling back to direct -l... flags."); + } + } +} + + +/** Searches the given list of compiler flags for ones that have a generic + equivalent. + + Certain compiler flags should, instead of using compiler-specfic syntax, + be specified as build options (`BuildOptions`) or built requirements + (`BuildRequirements`). This function will output warning messages to + assist the user in making the best choice. +*/ +void warnOnSpecialCompilerFlags(string[] compiler_flags, BuildOptions options, string package_name, string config_name) +{ + import std.algorithm : any, endsWith, startsWith; + import std.range : empty; + + struct SpecialFlag { + string[] flags; + string alternative; + } + static immutable SpecialFlag[] s_specialFlags = [ + {["-c", "-o-"], "Automatically issued by DUB, do not specify in dub.json"}, + {["-w", "-Wall", "-Werr"], `Use "buildRequirements" to control warning behavior`}, + {["-property", "-fproperty"], "Using this flag may break building of dependencies and it will probably be removed from DMD in the future"}, + {["-wi"], `Use the "buildRequirements" field to control warning behavior`}, + {["-d", "-de", "-dw"], `Use the "buildRequirements" field to control deprecation behavior`}, + {["-of"], `Use "targetPath" and "targetName" to customize the output file`}, + {["-debug", "-fdebug", "-g"], "Call dub with --build=debug"}, + {["-release", "-frelease", "-O", "-inline"], "Call dub with --build=release"}, + {["-unittest", "-funittest"], "Call dub with --build=unittest"}, + {["-lib"], `Use {"targetType": "staticLibrary"} or let dub manage this`}, + {["-D"], "Call dub with --build=docs or --build=ddox"}, + {["-X"], "Call dub with --build=ddox"}, + {["-cov"], "Call dub with --build=cov or --build=unittest-cov"}, + {["-profile"], "Call dub with --build=profile"}, + {["-version="], `Use "versions" to specify version constants in a compiler independent way`}, + {["-debug="], `Use "debugVersions" to specify version constants in a compiler independent way`}, + {["-I"], `Use "importPaths" to specify import paths in a compiler independent way`}, + {["-J"], `Use "stringImportPaths" to specify import paths in a compiler independent way`}, + {["-m32", "-m64"], `Use --arch=x86/--arch=x86_64 to specify the target architecture`} + ]; + + struct SpecialOption { + BuildOption[] flags; + string alternative; + } + static immutable SpecialOption[] s_specialOptions = [ + {[BuildOption.debugMode], "Call DUB with --build=debug"}, + {[BuildOption.releaseMode], "Call DUB with --build=release"}, + {[BuildOption.coverage], "Call DUB with --build=cov or --build=unittest-cov"}, + {[BuildOption.debugInfo], "Call DUB with --build=debug"}, + {[BuildOption.inline], "Call DUB with --build=release"}, + {[BuildOption.noBoundsCheck], "Call DUB with --build=release-nobounds"}, + {[BuildOption.optimize], "Call DUB with --build=release"}, + {[BuildOption.profile], "Call DUB with --build=profile"}, + {[BuildOption.unittests], "Call DUB with --build=unittest"}, + {[BuildOption.warnings, BuildOption.warningsAsErrors], "Use \"buildRequirements\" to control the warning level"}, + {[BuildOption.ignoreDeprecations, BuildOption.deprecationWarnings, BuildOption.deprecationErrors], "Use \"buildRequirements\" to control the deprecation warning level"}, + {[BuildOption.property], "This flag is deprecated and has no effect"} + ]; + + bool got_preamble = false; + void outputPreamble() + { + if (got_preamble) return; + got_preamble = true; + logWarn(""); + if (config_name.empty) logWarn("## Warning for package %s ##", package_name); + else logWarn("## Warning for package %s, configuration %s ##", package_name, config_name); + logWarn(""); + logWarn("The following compiler flags have been specified in the package description"); + logWarn("file. They are handled by DUB and direct use in packages is discouraged."); + logWarn("Alternatively, you can set the DFLAGS environment variable to pass custom flags"); + logWarn("to the compiler, or use one of the suggestions below:"); + logWarn(""); + } + + foreach (f; compiler_flags) { + foreach (sf; s_specialFlags) { + if (sf.flags.any!(sff => f == sff || (sff.endsWith("=") && f.startsWith(sff)))) { + outputPreamble(); + logWarn("%s: %s", f, sf.alternative); + break; + } + } + } + + foreach (sf; s_specialOptions) { + foreach (f; sf.flags) { + if (options & f) { + outputPreamble(); + logWarn("%s: %s", f, sf.alternative); + break; + } + } + } + + if (got_preamble) logWarn(""); +} + + +/** + Generate a file that will give, at compile time, informations about the compiler (architecture, frontend version...) + + See_Also: `readPlatformProbe` +*/ +Path generatePlatformProbeFile() +{ + import dub.internal.vibecompat.core.file; + import dub.internal.vibecompat.data.json; + import dub.internal.utils; + + auto path = getTempFile("dub_platform_probe", ".d"); + + auto fil = openFile(path, FileMode.createTrunc); + scope (failure) { + fil.close(); + } + + // NOTE: This must be kept in sync with the dub.platform module + fil.write(q{ + module dub_platform_probe; + + template toString(int v) { enum toString = v.stringof; } + + pragma(msg, `{`); + pragma(msg,` "compiler": "`~ determineCompiler() ~ `",`); + pragma(msg, ` "frontendVersion": ` ~ toString!__VERSION__ ~ `,`); + pragma(msg, ` "compilerVendor": "` ~ __VENDOR__ ~ `",`); + pragma(msg, ` "platform": [`); + pragma(msg, ` ` ~ determinePlatform()); + pragma(msg, ` ],`); + pragma(msg, ` "architecture": [`); + pragma(msg, ` ` ~ determineArchitecture()); + pragma(msg, ` ],`); + pragma(msg, `}`); + + string determinePlatform() + { + string ret; + version(Windows) ret ~= `"windows", `; + version(linux) ret ~= `"linux", `; + version(Posix) ret ~= `"posix", `; + version(OSX) ret ~= `"osx", `; + version(FreeBSD) ret ~= `"freebsd", `; + version(OpenBSD) ret ~= `"openbsd", `; + version(NetBSD) ret ~= `"netbsd", `; + version(DragonFlyBSD) ret ~= `"dragonflybsd", `; + version(BSD) ret ~= `"bsd", `; + version(Solaris) ret ~= `"solaris", `; + version(AIX) ret ~= `"aix", `; + version(Haiku) ret ~= `"haiku", `; + version(SkyOS) ret ~= `"skyos", `; + version(SysV3) ret ~= `"sysv3", `; + version(SysV4) ret ~= `"sysv4", `; + version(Hurd) ret ~= `"hurd", `; + version(Android) ret ~= `"android", `; + version(Cygwin) ret ~= `"cygwin", `; + version(MinGW) ret ~= `"mingw", `; + return ret; + } + + string determineArchitecture() + { + string ret; + version(X86) ret ~= `"x86", `; + version(X86_64) ret ~= `"x86_64", `; + version(ARM) ret ~= `"arm", `; + version(ARM_Thumb) ret ~= `"arm_thumb", `; + version(ARM_SoftFloat) ret ~= `"arm_softfloat", `; + version(ARM_HardFloat) ret ~= `"arm_hardfloat", `; + version(ARM64) ret ~= `"arm64", `; + version(PPC) ret ~= `"ppc", `; + version(PPC_SoftFP) ret ~= `"ppc_softfp", `; + version(PPC_HardFP) ret ~= `"ppc_hardfp", `; + version(PPC64) ret ~= `"ppc64", `; + version(IA64) ret ~= `"ia64", `; + version(MIPS) ret ~= `"mips", `; + version(MIPS32) ret ~= `"mips32", `; + version(MIPS64) ret ~= `"mips64", `; + version(MIPS_O32) ret ~= `"mips_o32", `; + version(MIPS_N32) ret ~= `"mips_n32", `; + version(MIPS_O64) ret ~= `"mips_o64", `; + version(MIPS_N64) ret ~= `"mips_n64", `; + version(MIPS_EABI) ret ~= `"mips_eabi", `; + version(MIPS_NoFloat) ret ~= `"mips_nofloat", `; + version(MIPS_SoftFloat) ret ~= `"mips_softfloat", `; + version(MIPS_HardFloat) ret ~= `"mips_hardfloat", `; + version(SPARC) ret ~= `"sparc", `; + version(SPARC_V8Plus) ret ~= `"sparc_v8plus", `; + version(SPARC_SoftFP) ret ~= `"sparc_softfp", `; + version(SPARC_HardFP) ret ~= `"sparc_hardfp", `; + version(SPARC64) ret ~= `"sparc64", `; + version(S390) ret ~= `"s390", `; + version(S390X) ret ~= `"s390x", `; + version(HPPA) ret ~= `"hppa", `; + version(HPPA64) ret ~= `"hppa64", `; + version(SH) ret ~= `"sh", `; + version(SH64) ret ~= `"sh64", `; + version(Alpha) ret ~= `"alpha", `; + version(Alpha_SoftFP) ret ~= `"alpha_softfp", `; + version(Alpha_HardFP) ret ~= `"alpha_hardfp", `; + return ret; + } + + string determineCompiler() + { + version(DigitalMars) return "dmd"; + else version(GNU) return "gdc"; + else version(LDC) return "ldc"; + else version(SDC) return "sdc"; + else return null; + } + }); + + fil.close(); + + return path; +} + +/** + Processes the output generated by compiling the platform probe file. + + See_Also: `generatePlatformProbeFile`. +*/ +BuildPlatform readPlatformProbe(string output) +{ + import std.algorithm : map; + import std.array : array; + import std.exception : enforce; + import std.string; + + // work around possible additional output of the compiler + auto idx1 = output.indexOf("{"); + auto idx2 = output.lastIndexOf("}"); + enforce(idx1 >= 0 && idx1 < idx2, + "Unexpected platform information output - does not contain a JSON object."); + output = output[idx1 .. idx2+1]; + + import dub.internal.vibecompat.data.json; + auto json = parseJsonString(output); + + BuildPlatform build_platform; + build_platform.platform = json.platform.get!(Json[]).map!(e => e.get!string()).array(); + build_platform.architecture = json.architecture.get!(Json[]).map!(e => e.get!string()).array(); + build_platform.compiler = json.compiler.get!string; + build_platform.frontendVersion = json.frontendVersion.get!int; + return build_platform; +} diff --git a/source/dub/dependency.d b/source/dub/dependency.d index 0ddf681..7a003af 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -1,7 +1,7 @@ /** - Stuff with dependencies. + Dependency specification functionality. - Copyright: © 2012-2013 Matthias Dondorff + Copyright: © 2012-2013 Matthias Dondorff, © 2012-2016 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Matthias Dondorff, Sönke Ludwig */ @@ -24,10 +24,24 @@ static import std.compiler; +/** Encapsulates the name of a package along with its dependency specification. +*/ +struct PackageDependency { + /// Name of the referenced package. + string name; + + /// Dependency specification used to select a particular version of the package. + Dependency spec; +} + + /** - Representing a dependency, which is basically a version string and a - compare methode, e.g. '>=1.0.0 <2.0.0' (i.e. a space separates the two - version numbers) + Represents a dependency specification. + + A dependency specification either represents a specific version or version + range, or a path to a package. In addition to that it has `optional` and + `default_` flags to control how non-mandatory dependencies are handled. The + package name is notably not part of the dependency specification. */ struct Dependency { private { @@ -42,18 +56,30 @@ bool m_default = false; } - // A Dependency, which matches every valid version. - static @property any() { return Dependency(ANY_IDENT); } - static @property invalid() { Dependency ret; ret.m_versA = Version.HEAD; ret.m_versB = Version.RELEASE; return ret; } + /// A Dependency, which matches every valid version. + static @property Dependency any() { return Dependency(ANY_IDENT); } + /// An invalid dependency (with no possible version matches). + static @property Dependency invalid() { Dependency ret; ret.m_versA = Version.maxRelease; ret.m_versB = Version.minRelease; return ret; } + + deprecated("Use .any instead. Will be removed for version 1.0.0.") alias ANY = any; + deprecated("Use .invalid instead. Will be removed for version 1.0.0.") alias INVALID = invalid; - this(string ves) + /** Constructs a new dependency specification from a string + + See the `versionSpec` property for a description of the accepted + contents of that string. + */ + this(string spec) { - this.versionSpec = ves; + this.versionSpec = spec; } + /** Constructs a new dependency specification that matches a specific + version. + */ this(in Version ver) { m_inclusiveA = m_inclusiveB = true; @@ -61,6 +87,9 @@ m_versB = ver; } + /** Constructs a new dependency specification that matches a specific + path. + */ this(Path path) { this(ANY_IDENT); @@ -87,46 +116,33 @@ /// Returns the exact version matched by the version range. @property Version version_() const { - enforce(m_versA == m_versB, "Dependency "~versionString~" is no exact version."); + enforce(m_versA == m_versB, "Dependency "~this.versionSpec~" is no exact version."); return m_versA; } - /// Returns a string representing the version range for the dependency. - @property string versionString() - const { - string r; + /// Compatibility alias + deprecated("Use versionSpec instead. Will be removed for version 1.0.0.") + alias versionString = versionSpec; - if (this == invalid) return "invalid"; + /** Sets/gets the matching version range as a specification string. - if (m_versA == m_versB && m_inclusiveA && m_inclusiveB) { - // Special "==" case - if (m_versA == Version.MASTER ) return "~master"; - else return m_versA.toString(); - } + The acceptable forms for this string are as follows: - // "~>" case - if (m_inclusiveA && !m_inclusiveB && !m_versA.isBranch) { - auto vs = m_versA.toString(); - auto i1 = std.string.indexOf(vs, '-'), i2 = std.string.indexOf(vs, '+'); - auto i12 = i1 >= 0 ? i2 >= 0 ? i1 < i2 ? i1 : i2 : i1 : i2; - auto va = i12 >= 0 ? vs[0 .. i12] : vs; - auto parts = va.splitter('.').array; - assert(parts.length == 3, "Version string with a digit group count != 3: "~va); + $(UL + $(LI `"1.0.0"` - a single version in SemVer format) + $(LI `"==1.0.0"` - alternative single version notation) + $(LI `">1.0.0"` - version range with a single bound) + $(LI `">1.0.0 <2.0.0"` - version range with two bounds) + $(LI `"~>1.0.0"` - a fuzzy version range) + $(LI `"~>1.0"` - a fuzzy version range with partial version) + $(LI `"~master"` - a branch name) + $(LI `"*" - match any version (see also `any`)) + ) - foreach (i; 0 .. 3) { - auto vp = parts[0 .. i+1].join("."); - auto ve = Version(expandVersion(vp)); - auto veb = Version(expandVersion(bumpVersion(vp))); - if (ve == m_versA && veb == m_versB) return "~>" ~ vp; - } - } + Apart from "$(LT)" and "$(GT)", "$(GT)=" and "$(LT)=" are also valid + comparators. - if (m_versA != Version.RELEASE) r = (m_inclusiveA ? ">=" : ">") ~ m_versA.toString(); - if (m_versB != Version.HEAD) r ~= (r.length==0 ? "" : " ") ~ (m_inclusiveB ? "<=" : "<") ~ m_versB.toString(); - if (m_versA == Version.RELEASE && m_versB == Version.HEAD) r = ">=0.0.0"; - return r; - } - + */ @property void versionSpec(string ves) { enforce(ves.length > 0); @@ -145,7 +161,7 @@ ves = ves[2..$]; m_versA = Version(expandVersion(ves)); m_versB = Version(bumpVersion(ves)); - } else if (ves[0] == Version.BRANCH_IDENT) { + } else if (ves[0] == Version.branchPrefix) { m_inclusiveA = true; m_inclusiveB = true; m_versA = m_versB = Version(ves); @@ -158,14 +174,14 @@ size_t idx2 = std.string.indexOf(ves, " "); if (idx2 == -1) { if (cmpa == "<=" || cmpa == "<") { - m_versA = Version.RELEASE; + m_versA = Version.minRelease; m_inclusiveA = true; m_versB = Version(ves); m_inclusiveB = cmpa == "<="; } else if (cmpa == ">=" || cmpa == ">") { m_versA = Version(ves); m_inclusiveA = cmpa == ">="; - m_versB = Version.HEAD; + m_versB = Version.maxRelease; m_inclusiveB = true; } else { // Converts "==" to ">=a&&<=a", which makes merging easier @@ -188,7 +204,48 @@ } } } + /// ditto + @property string versionSpec() + const { + string r; + if (this == invalid) return "invalid"; + + if (m_versA == m_versB && m_inclusiveA && m_inclusiveB) { + // Special "==" case + if (m_versA == Version.masterBranch) return "~master"; + else return m_versA.toString(); + } + + // "~>" case + if (m_inclusiveA && !m_inclusiveB && !m_versA.isBranch) { + auto vs = m_versA.toString(); + auto i1 = std.string.indexOf(vs, '-'), i2 = std.string.indexOf(vs, '+'); + auto i12 = i1 >= 0 ? i2 >= 0 ? i1 < i2 ? i1 : i2 : i1 : i2; + auto va = i12 >= 0 ? vs[0 .. i12] : vs; + auto parts = va.splitter('.').array; + assert(parts.length == 3, "Version string with a digit group count != 3: "~va); + + foreach (i; 0 .. 3) { + auto vp = parts[0 .. i+1].join("."); + auto ve = Version(expandVersion(vp)); + auto veb = Version(expandVersion(bumpVersion(vp))); + if (ve == m_versA && veb == m_versB) return "~>" ~ vp; + } + } + + if (m_versA != Version.minRelease) r = (m_inclusiveA ? ">=" : ">") ~ m_versA.toString(); + if (m_versB != Version.maxRelease) r ~= (r.length==0 ? "" : " ") ~ (m_inclusiveB ? "<=" : "<") ~ m_versB.toString(); + if (m_versA == Version.minRelease && m_versB == Version.maxRelease) r = ">=0.0.0"; + return r; + } + + /** Returns a modified dependency that gets mapped to a given path. + + This function will return an unmodified `Dependency` if it is not path + based. Otherwise, the given `path` will be prefixed to the existing + path. + */ Dependency mapToPath(Path path) const { if (m_path.empty || m_path.absolute) return this; @@ -199,21 +256,31 @@ } } + /** Returns a human-readable string representation of the dependency + specification. + */ string toString()() const { - auto ret = versionString; + auto ret = versionSpec; if (optional) ret ~= " (optional)"; if (!path.empty) ret ~= " @"~path.toNativeString(); return ret; } + /** Returns a JSON representation of the dependency specification. + + Simple specifications will be represented as a single specification + string (`versionSpec`), while more complex specifications will be + represented as a JSON object with optional "version", "path", "optional" + and "default" fields. + */ Json toJson() const { Json json; if( path.empty && !optional ){ - json = Json(this.versionString); + json = Json(this.versionSpec); } else { json = Json.emptyObject; - json["version"] = this.versionString; + json["version"] = this.versionSpec; if (!path.empty) json["path"] = path.toString(); if (optional) json["optional"] = true; if (default_) json["default"] = true; @@ -229,6 +296,10 @@ assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString()); } + /** Constructs a new `Dependency` from its JSON representation. + + See `toJson` for a description of the JSON format. + */ static Dependency fromJson(Json verspec) { Dependency dep; if( verspec.type == Json.Type.object ){ @@ -236,7 +307,7 @@ if (auto pv = "version" in verspec) logDiagnostic("Ignoring version specification (%s) for path based dependency %s", pv.get!string, pp.get!string); - dep = Dependency.ANY; + dep = Dependency.any; dep.path = Path(verspec.path.get!string); } else { enforce("version" in verspec, "No version field specified!"); @@ -264,7 +335,7 @@ "path": "path/to/package" } `)); - Dependency d = Dependency.ANY; // supposed to ignore the version spec + Dependency d = Dependency.any; // supposed to ignore the version spec d.optional = true; d.default_ = true; d.path = Path("path/to/package"); @@ -275,6 +346,11 @@ assert(d.path == parsed.path); } + /** Compares dependency specifications. + + These methods are suitable for equality comparisons, as well as for + using `Dependency` as a key in hash or tree maps. + */ bool opEquals(in Dependency o) const { // TODO(mdondorff): Check if not comparing the path is correct for all clients. @@ -283,6 +359,7 @@ && o.m_optional == m_optional && o.m_default == m_default; } + /// ditto int opCmp(in Dependency o) const { if (m_inclusiveA != o.m_inclusiveA) return m_inclusiveA < o.m_inclusiveA ? -1 : 1; @@ -293,6 +370,7 @@ return 0; } + /// ditto hash_t toHash() const nothrow @trusted { try { auto strhash = &typeid(string).getHash; @@ -301,10 +379,18 @@ } catch (Exception) assert(false); } + /** Determines if this dependency specification is valid. + + A specification is valid if it can match at least one version. + */ bool valid() const { return m_versA <= m_versB && doCmp(m_inclusiveA && m_inclusiveB, m_versA, m_versB); } + /** Determines if this dependency specification matches arbitrary versions. + + This is true in particular for the `any` constant. + */ bool matchesAny() const { auto cmp = Dependency("*"); cmp.optional = m_optional; @@ -312,8 +398,12 @@ return cmp == this; } + /** Tests if the specification matches a specific version. + */ bool matches(string vers) const { return matches(Version(vers)); } + /// ditto bool matches(const(Version) v) const { return matches(v); } + /// ditto bool matches(ref const(Version) v) const { if (this.matchesAny) return true; //logDebug(" try match: %s with: %s", v, this); @@ -331,16 +421,21 @@ return true; } - /// Merges to versions + /** Merges two dependency specifications. + + The result is a specification that matches the intersection of the set + of versions matched by the individual specifications. Note that this + result can be invalid (i.e. not match any version). + */ Dependency merge(ref const(Dependency) o) const { if (this.matchesAny) return o; if (o.matchesAny) return this; - if (!this.valid || !o.valid) return INVALID; - if (m_versA.isBranch != o.m_versA.isBranch) return INVALID; - if (m_versB.isBranch != o.m_versB.isBranch) return INVALID; - if (m_versA.isBranch) return m_versA == o.m_versA ? this : INVALID; - if (this.path != o.path) return INVALID; + if (!this.valid || !o.valid) return invalid; + if (m_versA.isBranch != o.m_versA.isBranch) return invalid; + if (m_versB.isBranch != o.m_versB.isBranch) return invalid; + if (m_versA.isBranch) return m_versA == o.m_versA ? this : invalid; + if (this.path != o.path) return invalid; Version a = m_versA > o.m_versA ? m_versA : o.m_versA; Version b = m_versB < o.m_versB ? m_versB : o.m_versB; @@ -351,7 +446,7 @@ d.m_inclusiveB = !m_inclusiveB && m_versB <= o.m_versB ? false : o.m_inclusiveB; d.m_versB = b; d.m_optional = m_optional && o.m_optional; - if (!d.valid) return INVALID; + if (!d.valid) return invalid; return d; } @@ -359,7 +454,7 @@ private static bool isDigit(char ch) { return ch >= '0' && ch <= '9'; } private static string skipComp(ref string c) { size_t idx = 0; - while (idx < c.length && !isDigit(c[idx]) && c[idx] != Version.BRANCH_IDENT) idx++; + while (idx < c.length && !isDigit(c[idx]) && c[idx] != Version.branchPrefix) idx++; enforce(idx < c.length, "Expected version number in version spec: "~c); string cmp = idx==c.length-1||idx==0? ">=" : c[0..idx]; c = c[idx..$]; @@ -378,19 +473,19 @@ unittest { Dependency a = Dependency(">=1.1.0"), b = Dependency(">=1.3.0"); - assert (a.merge(b).valid() && a.merge(b).versionString == ">=1.3.0", a.merge(b).toString()); + assert (a.merge(b).valid() && a.merge(b).versionSpec == ">=1.3.0", a.merge(b).toString()); assertThrown(Dependency("<=2.0.0 >=1.0.0")); assertThrown(Dependency(">=2.0.0 <=1.0.0")); a = Dependency(">=1.0.0 <=5.0.0"); b = Dependency(">=2.0.0"); - assert (a.merge(b).valid() && a.merge(b).versionString == ">=2.0.0 <=5.0.0", a.merge(b).toString()); + assert (a.merge(b).valid() && a.merge(b).versionSpec == ">=2.0.0 <=5.0.0", a.merge(b).toString()); assertThrown(a = Dependency(">1.0.0 ==5.0.0"), "Construction is invalid"); a = Dependency(">1.0.0"); b = Dependency("<2.0.0"); assert (a.merge(b).valid(), a.merge(b).toString()); - assert (a.merge(b).versionString == ">1.0.0 <2.0.0", a.merge(b).toString()); + assert (a.merge(b).versionSpec == ">1.0.0 <2.0.0", a.merge(b).toString()); a = Dependency(">2.0.0"); b = Dependency("<1.0.0"); assert (!(a.merge(b)).valid(), a.merge(b).toString()); @@ -413,18 +508,18 @@ // branches / head revisions - a = Dependency(Version.MASTER_STRING); + a = Dependency(Version.masterBranch); assert(a.valid()); - assert(a.matches(Version.MASTER)); - b = Dependency(Version.MASTER_STRING); + assert(a.matches(Version.masterBranch)); + b = Dependency(Version.masterBranch); m = a.merge(b); - assert(m.matches(Version.MASTER)); + assert(m.matches(Version.masterBranch)); //assertThrown(a = Dependency(Version.MASTER_STRING ~ " <=1.0.0"), "Construction invalid"); - assertThrown(a = Dependency(">=1.0.0 " ~ Version.MASTER_STRING), "Construction invalid"); + assertThrown(a = Dependency(">=1.0.0 " ~ Version.masterBranch.toString()), "Construction invalid"); - immutable string branch1 = Version.BRANCH_IDENT ~ "Branch1"; - immutable string branch2 = Version.BRANCH_IDENT ~ "Branch2"; + immutable string branch1 = Version.branchPrefix ~ "Branch1"; + immutable string branch2 = Version.branchPrefix ~ "Branch2"; //assertThrown(a = Dependency(branch1 ~ " " ~ branch2), "Error: '" ~ branch1 ~ " " ~ branch2 ~ "' succeeded"); //assertThrown(a = Dependency(Version.MASTER_STRING ~ " " ~ branch1), "Error: '" ~ Version.MASTER_STRING ~ " " ~ branch1 ~ "' succeeded"); @@ -439,7 +534,7 @@ a = Dependency(branch1); assert(a.matches(branch1), "Dependency(branch1) does not match 'branch1'"); assert(a.matches(Version(branch1)), "Dependency(branch1) does not match Version('branch1')"); - assert(!a.matches(Version.MASTER), "Dependency(branch1) matches Version.MASTER"); + assert(!a.matches(Version.masterBranch), "Dependency(branch1) matches Version.masterBranch"); assert(!a.matches(branch2), "Dependency(branch1) matches 'branch2'"); assert(!a.matches(Version("1.0.0")), "Dependency(branch1) matches '1.0.0'"); a = Dependency(">=1.0.0"); @@ -499,28 +594,28 @@ assert(a.valid); assert(a.version_ == Version("~d2test")); - a = Dependency.ANY; + a = Dependency.any; assert(!a.optional); assert(a.valid); assertThrown(a.version_); - assert(a.matches(Version.MASTER)); + assert(a.matches(Version.masterBranch)); assert(a.matches(Version("1.0.0"))); assert(a.matches(Version("0.0.1-pre"))); b = Dependency(">=1.0.1"); assert(b == a.merge(b)); assert(b == b.merge(a)); - b = Dependency(Version.MASTER); + b = Dependency(Version.masterBranch); assert(a.merge(b) == b); assert(b.merge(a) == b); a.optional = true; - assert(a.matches(Version.MASTER)); + assert(a.matches(Version.masterBranch)); assert(a.matches(Version("1.0.0"))); assert(a.matches(Version("0.0.1-pre"))); b = Dependency(">=1.0.1"); assert(b == a.merge(b)); assert(b == b.merge(a)); - b = Dependency(Version.MASTER); + b = Dependency(Version.masterBranch); assert(a.merge(b) == b); assert(b.merge(a) == b); @@ -528,45 +623,62 @@ } unittest { - assert(Dependency("~>1.0.4").versionString == "~>1.0.4"); - assert(Dependency("~>1.4").versionString == "~>1.4"); - assert(Dependency("~>2").versionString == "~>2"); - assert(Dependency("~>1.0.4+1.2.3").versionString == "~>1.0.4"); + assert(Dependency("~>1.0.4").versionSpec == "~>1.0.4"); + assert(Dependency("~>1.4").versionSpec == "~>1.4"); + assert(Dependency("~>2").versionSpec == "~>2"); + assert(Dependency("~>1.0.4+1.2.3").versionSpec == "~>1.0.4"); } /** - A version in the format "major.update.bugfix-prerelease+buildmetadata" - according to Semantic Versioning Specification v2.0.0. + Represents a version in semantic version format, or a branch identifier. - (deprecated): - This also supports a format like "~master", to identify trunk, or - "~branch_name" to identify a branch. Both Version types starting with "~" - refer to the head revision of the corresponding branch. - This is subject to be removed soon. + This can either have the form "~master", where "master" is a branch name, + or the form "major.update.bugfix-prerelease+buildmetadata" (see the + Semantic Versioning Specification v2.0.0 at http://semver.org/). */ struct Version { private { enum MAX_VERS = "99999.0.0"; enum UNKNOWN_VERS = "unknown"; + enum branchPrefix = '~'; + enum masterString = "~master"; string m_version; } - static @property RELEASE() { return Version("0.0.0"); } - static @property HEAD() { return Version(MAX_VERS); } - static @property MASTER() { return Version(MASTER_STRING); } - static @property UNKNOWN() { return Version(UNKNOWN_VERS); } - static @property MASTER_STRING() { return "~master"; } - static @property BRANCH_IDENT() { return '~'; } + static @property Version minRelease() { return Version("0.0.0"); } + static @property Version maxRelease() { return Version(MAX_VERS); } + static @property Version masterBranch() { return Version(masterString); } + static @property Version unknown() { return Version(UNKNOWN_VERS); } + deprecated("Use minRelease instead. Will be removed for version 1.0.0.") + static @property RELEASE() { return Version("0.0.0"); } + deprecated("Use maxRelease instead. Will be removed for version 1.0.0.") + static @property HEAD() { return Version(MAX_VERS); } + deprecated("Use masterBranch instead. Will be removed for version 1.0.0.") + static @property MASTER() { return Version(MASTER_STRING); } + deprecated("Use unknown instead. Will be removed for version 1.0.0.") + static @property UNKNOWN() { return Version(UNKNOWN_VERS); } + deprecated("Use masterBranch.toString() instead. Will be removed for version 1.0.0.") + static @property MASTER_STRING() { return masterString; } + deprecated("Will be removed for version 1.0.0.") + static @property BRANCH_IDENT() { return branchPrefix; } + + /** Constructs a new `Version` from its string representation. + */ this(string vers) { enforce(vers.length > 1, "Version strings must not be empty."); - if (vers[0] != BRANCH_IDENT && vers != UNKNOWN_VERS) + if (vers[0] != branchPrefix && vers != UNKNOWN_VERS) enforce(vers.isValidVersion(), "Invalid SemVer format: " ~ vers); m_version = vers; } + /** Constructs a new `Version` from its string representation. + + This method is equivalent to calling the constructor and is used as an + endpoint for the serialization framework. + */ static Version fromString(string vers) { return Version(vers); } bool opEquals(const Version oth) const { @@ -576,19 +688,30 @@ return opCmp(oth) == 0; } - /// Returns true, if this version indicates a branch, which is not the trunk. - @property bool isBranch() const { return !m_version.empty && m_version[0] == BRANCH_IDENT; } - @property bool isMaster() const { return m_version == MASTER_STRING; } + /// Tests if this represents a branch instead of a version. + @property bool isBranch() const { return !m_version.empty && m_version[0] == branchPrefix; } + + /// Tests if this represents the master branch "~master". + @property bool isMaster() const { return m_version == masterString; } + + /** Tests if this represents a pre-release version. + + Note that branches are always considered pre-release versions. + */ @property bool isPreRelease() const { if (isBranch) return true; return isPreReleaseVersion(m_version); } + + /// Tests if this represents the special unknown version constant. @property bool isUnknown() const { return m_version == UNKNOWN_VERS; } - /** - Comparing Versions is generally possible, but comparing Versions - identifying branches other than master will fail. Only equality - can be tested for these. + /** Compares two versions/branches for precedence. + + Versions generally have precedence over branches and the master branch + has precedence over other branches. Apart from that, versions are + compared using SemVer semantics, while branches are compared + lexicographically. */ int opCmp(ref const Version other) const { @@ -606,8 +729,10 @@ return compareVersions(m_version, other.m_version); } + /// ditto int opCmp(in Version other) const { return opCmp(other); } + /// Returns the string representation of the version/branch. string toString() const { return m_version; } } @@ -618,10 +743,10 @@ assert(!a.isBranch, "Error: '1.0.0' treated as branch"); assert(a == a, "a == a failed"); - assertNotThrown(a = Version(Version.MASTER_STRING), "Constructing Version("~Version.MASTER_STRING~"') failed"); - assert(a.isBranch, "Error: '"~Version.MASTER_STRING~"' treated as branch"); + assertNotThrown(a = Version(Version.masterString), "Constructing Version("~Version.masterString~"') failed"); + assert(a.isBranch, "Error: '"~Version.masterString~"' treated as branch"); assert(a.isMaster); - assert(a == Version.MASTER, "Constructed master version != default master version."); + assert(a == Version.masterBranch, "Constructed master version != default master version."); assertNotThrown(a = Version("~BRANCH"), "Construction of branch Version failed."); assert(a.isBranch, "Error: '~BRANCH' not treated as branch'"); @@ -634,7 +759,7 @@ assert(a == b, "a == b with a:'1.0.0', b:'1.0.0' failed"); b = Version("2.0.0"); assert(a != b, "a != b with a:'1.0.0', b:'2.0.0' failed"); - a = Version(Version.MASTER_STRING); + a = Version.masterBranch; b = Version("~BRANCH"); assert(a != b, "a != b with a:MASTER, b:'~branch' failed"); assert(a > b); @@ -664,12 +789,12 @@ for(int j=i-1; j>=0; --j) assert(versions[j] < versions[i], "Failed: " ~ versions[j].toString() ~ "<" ~ versions[i].toString()); - a = Version.UNKNOWN; - b = Version.RELEASE; + a = Version.unknown; + b = Version.minRelease; assertThrown(a == b, "Failed: compared " ~ a.toString() ~ " with " ~ b.toString() ~ ""); - a = Version.UNKNOWN; - b = Version.UNKNOWN; + a = Version.unknown; + b = Version.unknown; assertThrown(a == b, "Failed: UNKNOWN == UNKNOWN"); assert(Version("1.0.0+a") == Version("1.0.0+b")); diff --git a/source/dub/description.d b/source/dub/description.d index 24a00fc..35e80ea 100644 --- a/source/dub/description.d +++ b/source/dub/description.d @@ -1,7 +1,7 @@ /** Types for project descriptions (dub describe). - Copyright: © 2015 rejectedsoftware e.K. + Copyright: © 2015-2016 rejectedsoftware e.K. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ @@ -19,16 +19,16 @@ and configuration that has been selected. */ struct ProjectDescription { - string rootPackage; - alias mainPackage = rootPackage; /// Compatibility alias - string configuration; - string buildType; - string compiler; - string[] architecture; - string[] platform; + string rootPackage; /// Name of the root package being built + @ignore deprecated("Use rootPackage instead. Will be removed for version 1.0.0.") alias mainPackage = rootPackage; /// Compatibility alias + string configuration; /// Name of the selected build configuration + string buildType; /// Name of the selected build type + string compiler; /// Canonical name of the compiler used (e.g. "dmd", "gdc" or "ldc") + string[] architecture; /// Architecture constants for the selected platform (e.g. `["x86_64"]`) + string[] platform; /// Platform constants for the selected platform (e.g. `["posix", "osx"]`) PackageDescription[] packages; /// All packages in the dependency tree TargetDescription[] targets; /// Build targets - @ignore size_t[string] targetLookup; /// Target index by name + @ignore size_t[string] targetLookup; /// Target index by package name name /// Targets by name ref inout(TargetDescription) lookupTarget(string name) inout @@ -59,12 +59,17 @@ /** - Build settings and meta data of a single package. + Describes the build settings and meta data of a single package. + + This structure contains the effective build settings and dependencies for + the selected build platform. This structure is most useful for displaying + information about a package in an IDE. Use `TargetDescription` instead when + writing a build-tool. */ struct PackageDescription { - string path; - string name; - Version version_; + string path; /// Path to the package + string name; /// Qualified name of the package + Version version_; /// Version of the package string description; string homepage; string[] authors; @@ -80,49 +85,57 @@ string targetFileName; string workingDirectory; string mainSourceFile; - string[] dflags; - string[] lflags; - string[] libs; - string[] copyFiles; - string[] versions; - string[] debugVersions; + string[] dflags; /// Flags passed to the D compiler + string[] lflags; /// Flags passed to the linker + string[] libs; /// Librariy names to link against (typically using "-l") + string[] copyFiles; /// Files to copy to the target directory + string[] versions; /// D version identifiers to set + string[] debugVersions; /// D debug version identifiers to set string[] importPaths; string[] stringImportPaths; - string[] preGenerateCommands; - string[] postGenerateCommands; - string[] preBuildCommands; - string[] postBuildCommands; + string[] preGenerateCommands; /// commands executed before creating the description + string[] postGenerateCommands; /// commands executed after creating the description + string[] preBuildCommands; /// Commands to execute prior to every build + string[] postBuildCommands; /// Commands to execute after every build @byName BuildRequirement[] buildRequirements; @byName BuildOption[] options; - SourceFileDescription[] files; + SourceFileDescription[] files; /// A list of all source/import files possibly used by the package } + +/** + Describes the settings necessary to build a certain binary target. +*/ struct TargetDescription { - string rootPackage; - string[] packages; - string rootConfiguration; - BuildSettings buildSettings; - string[] dependencies; - string[] linkDependencies; + string rootPackage; /// Main package associated with this target, this is also the name of the target. + string[] packages; /// All packages contained in this target (e.g. for target type "sourceLibrary") + string rootConfiguration; /// Build configuration of the target's root package used for building + BuildSettings buildSettings; /// Final build settings to use when building the target + string[] dependencies; /// List of all dependencies of this target (package names) + string[] linkDependencies; /// List of all link-dependencies of this target (target names) } /** - Description for a single source file. + Description for a single source file known to the package. */ struct SourceFileDescription { - @byName SourceFileRole role; - alias type = role; /// Compatibility alias - string path; + @byName SourceFileRole role; /// Main role this file plays in the build process + @ignore deprecated("Use role instead. Will be removed for version 1.0.0.") alias type = role; /// Compatibility alias + string path; /// Full path to the file } /** - Determines + Determines the role that a file plays in the build process. + + If a file has multiple roles, higher enum values will have precedence, i.e. + if a file is used both, as a source file and as an import file, it will + be classified as a source file. */ enum SourceFileRole { - unusedStringImport, - unusedImport, - unusedSource, - stringImport, - import_, - source + unusedStringImport, /// Used as a string import for another configuration/platform + unusedImport, /// Used as an import for another configuration/platform + unusedSource, /// Used as a source file for another configuration/platform + stringImport, /// Used as a string import file + import_, /// Used as an import file + source /// Used as a source file } diff --git a/source/dub/dub.d b/source/dub/dub.d index 9e022dc..154dddf 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -1,7 +1,7 @@ /** A package manager. - Copyright: © 2012-2013 Matthias Dondorff + Copyright: © 2012-2013 Matthias Dondorff, 2012-2016 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Matthias Dondorff, Sönke Ludwig */ @@ -53,28 +53,39 @@ } } +static this() +{ + import dub.compilers.dmd : DMDCompiler; + import dub.compilers.gdc : GDCCompiler; + import dub.compilers.ldc : LDCCompiler; + registerCompiler(new DMDCompiler); + registerCompiler(new GDCCompiler); + registerCompiler(new LDCCompiler); +} + +/// The URL to the official package registry. enum defaultRegistryURL = "http://code.dlang.org/"; -/// The default supplier for packages, which is the registry -/// hosted by code.dlang.org. +/** Returns a default list of package suppliers. + + This will contain a single package supplier that points to the official + package registry. + + See_Also: `defaultRegistryURL` +*/ PackageSupplier[] defaultPackageSuppliers() { logDiagnostic("Using dub registry url '%s'", defaultRegistryURL); return [new RegistryPackageSupplier(URL(defaultRegistryURL))]; } -/// Option flags for fetch -enum FetchOptions -{ - none = 0, - forceBranchUpgrade = 1<<0, - usePrerelease = 1<<1, - forceRemove = 1<<2, - printOnly = 1<<3, -} -/// The Dub class helps in getting the applications -/// dependencies up and running. An instance manages one application. +/** Provides a high-level entry point for DUB's functionality. + + This class provides means to load a certain project (a root package with + all of its dependencies) and to perform high-level operations as found in + the command line interface. +*/ class Dub { private { bool m_dryRun = false; @@ -87,11 +98,33 @@ Path m_projectPath; Project m_project; Path m_overrideSearchPath; + string m_defaultCompiler; } - /// Initiales the package manager for the vibe application - /// under root. - this(PackageSupplier[] additional_package_suppliers = null, string root_path = ".", SkipRegistry skip_registry = SkipRegistry.none) + /** The default placement location of fetched packages. + + This property can be altered, so that packages which are downloaded as part + of the normal upgrade process are stored in a certain location. This is + how the "--local" and "--system" command line switches operate. + */ + PlacementLocation defaultPlacementLocation = PlacementLocation.user; + + + /** Initializes the instance for use with a specific root package. + + Note that a package still has to be loaded using one of the + `loadPackage` overloads. + + Params: + root_path = Path to the root package + additional_package_suppliers = A list of package suppliers to try + before the suppliers found in the configurations files and the + `defaultPackageSuppliers`. + skip_registry = Can be used to skip using the configured package + suppliers, as well as the default suppliers. + */ + this(string root_path = ".", PackageSupplier[] additional_package_suppliers = null, + SkipPackageSuppliers skip_registry = SkipPackageSuppliers.none) { m_rootPath = Path(root_path); if (!m_rootPath.absolute) m_rootPath = Path(getcwd()) ~ m_rootPath; @@ -100,29 +133,41 @@ PackageSupplier[] ps = additional_package_suppliers; - if (skip_registry < SkipRegistry.all) { + if (skip_registry < SkipPackageSuppliers.all) { if (auto pp = "registryUrls" in m_userConfig) ps ~= deserializeJson!(string[])(*pp) .map!(url => cast(PackageSupplier)new RegistryPackageSupplier(URL(url))) .array; } - if (skip_registry < SkipRegistry.all) { + if (skip_registry < SkipPackageSuppliers.all) { if (auto pp = "registryUrls" in m_systemConfig) ps ~= deserializeJson!(string[])(*pp) .map!(url => cast(PackageSupplier)new RegistryPackageSupplier(URL(url))) .array; } - if (skip_registry < SkipRegistry.standard) + if (skip_registry < SkipPackageSuppliers.standard) ps ~= defaultPackageSuppliers(); m_packageSuppliers = ps; m_packageManager = new PackageManager(m_userDubPath, m_systemDubPath); updatePackageSearchPath(); } + /// ditto + deprecated("Will be removed for version 1.0.0.") + this(PackageSupplier[] additional_package_suppliers = null, string root_path = ".", + SkipPackageSuppliers skip_registry = SkipPackageSuppliers.none) + { + this(root_path, additional_package_suppliers, skip_registry); + } - /// Initializes DUB with only a single search path + /** Initializes the instance with a single package search path, without + loading a package. + + This constructor corresponds to the "--bare" option of the command line + interface. Use + */ this(Path override_path) { init(); @@ -147,6 +192,8 @@ m_userConfig = jsonFromFile(m_userDubPath ~ "settings.json", true); m_systemConfig = jsonFromFile(m_systemDubPath ~ "settings.json", true); + + determineDefaultCompiler(); } @property void dryRun(bool v) { m_dryRun = v; } @@ -173,26 +220,26 @@ @property inout(Project) project() inout { return m_project; } - /// Returns the default compiler binary to use for building D code. - @property string defaultCompiler() - const { - if (auto pv = "defaultCompiler" in m_userConfig) - if (pv.type == Json.Type.string) - return pv.get!string; + /** Returns the default compiler binary to use for building D code. - if (auto pv = "defaultCompiler" in m_systemConfig) - if (pv.type == Json.Type.string) - return pv.get!string; + If set, the "defaultCompiler" field of the DUB user or system + configuration file will be used. Otherwise the PATH environment variable + will be searched for files named "dmd", "gdc", "gdmd", "ldc2", "ldmd2" + (in that order, taking into account operating system specific file + extensions) and the first match is returned. If no match is found, "dmd" + will be used. + */ + @property string defaultCompiler() const { return m_defaultCompiler; } - return .defaultCompiler(); - } + deprecated("Will be removed for version 1.0.0.") void shutdown() {} + deprecated("Will be removed for version 1.0.0.") void cleanCaches() {} - deprecated void shutdown() {} - deprecated void cleanCaches() {} + deprecated("Use loadPackage instead. Will be removed for version 1.0.0.") + alias loadPackageFromCwd = loadPackage; - /// Loads the package from the current working directory as the main - /// project package. - void loadPackageFromCwd() + /** Loads the package that resides within the configured `rootPath`. + */ + void loadPackage() { loadPackage(m_rootPath); } @@ -213,6 +260,9 @@ m_project = new Project(m_packageManager, pack); } + /** Disables the default search paths and only searches a specific directory + for packages. + */ void overrideSearchPath(Path path) { if (!path.absolute) path = Path(getcwd()) ~ path; @@ -220,8 +270,15 @@ updatePackageSearchPath(); } + /** Gets the default configuration for a particular build platform. + + This forwards to `Project.getDefaultConfiguration` and requires a + project to be loaded. + */ string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) const { return m_project.getDefaultConfiguration(platform, allow_non_library_configs); } + /** Attempts to upgrade the dependency selection of the loaded project. + */ void upgrade(UpgradeOptions options) { // clear non-existent version selections @@ -335,16 +392,21 @@ m_project.saveSelections(); } - /// Generate project files for a specified IDE. - /// Any existing project files will be overridden. - void generateProject(string ide, GeneratorSettings settings) { + /** Generate project files for a specified generator. + + Any existing project files will be overridden. + */ + void generateProject(string ide, GeneratorSettings settings) + { auto generator = createProjectGenerator(ide, m_project); if (m_dryRun) return; // TODO: pass m_dryRun to the generator generator.generate(settings); } - /// Executes tests on the current project. Throws an exception, if - /// unittests failed. + /** Executes tests on the current project. + + Throws an exception, if unittests failed. + */ void testProject(GeneratorSettings settings, string config, Path custom_main_file) { if (custom_main_file.length && !custom_main_file.absolute) custom_main_file = getWorkingDirectory() ~ custom_main_file; @@ -383,7 +445,7 @@ } else { logInfo(`Generating test runner configuration '%s' for '%s' (%s).`, test_config, config, lbuildsettings.targetType); - BuildSettingsTemplate tcinfo = m_project.rootPackage.info.getConfiguration(config).buildSettings; + BuildSettingsTemplate tcinfo = m_project.rootPackage.recipe.getConfiguration(config).buildSettings; tcinfo.targetType = TargetType.executable; tcinfo.targetName = test_config; tcinfo.versions[""] ~= "VibeCustomMain"; // HACK for vibe.d's legacy main() behavior @@ -398,7 +460,7 @@ string[] import_modules; foreach (file; lbuildsettings.sourceFiles) { if (file.endsWith(".d") && Path(file).head.toString() != "package.d") - import_modules ~= lbuildsettings.determineModuleName(Path(file), m_project.rootPackage.path); + import_modules ~= dub.internal.utils.determineModuleName(lbuildsettings, Path(file), m_project.rootPackage.path); } // generate main file @@ -438,7 +500,7 @@ }); } } - m_project.rootPackage.info.configurations ~= ConfigurationInfo(test_config, tcinfo); + m_project.rootPackage.recipe.configurations ~= ConfigurationInfo(test_config, tcinfo); m_project = new Project(m_packageManager, m_project.rootPackage); settings.config = test_config; @@ -448,14 +510,16 @@ } /// Outputs a JSON description of the project, including its dependencies. - deprecated void describeProject(BuildPlatform platform, string config) + deprecated("Will be removed for version 1.0.0.") void describeProject(BuildPlatform platform, string config) { import std.stdio; auto desc = m_project.describe(platform, config); writeln(desc.serializeToPrettyJson()); } - void listImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) + /** Prints a list of all import paths necessary for building the root package. + */ + deprecated("Will be removed for version 1.0.0.") void listImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) { import std.stdio; @@ -464,7 +528,9 @@ } } - void listStringImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) + /** Prints a list of all string import paths necessary for building the root package. + */ + deprecated("Will be removed for version 1.0.0.") void listStringImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) { import std.stdio; @@ -473,8 +539,9 @@ } } - void listProjectData(BuildPlatform platform, string config, string buildType, - string[] requestedData, Compiler formattingCompiler, bool nullDelim) + /** Prints the specified build settings necessary for building the root package. + */ + void listProjectData(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type) { import std.stdio; import std.ascii : newline; @@ -486,12 +553,32 @@ .joiner() .array(); - auto data = m_project.listBuildSettings(platform, config, buildType, - requestedDataSplit, formattingCompiler, nullDelim); + auto data = m_project.listBuildSettings(settings, requestedDataSplit, list_type); - write( data.joiner(nullDelim? "\0" : newline) ); - if(!nullDelim) - writeln(); + string delimiter; + final switch (list_type) with (ListBuildSettingsFormat) { + case list: delimiter = newline ~ newline; break; + case listNul: delimiter = "\0\0"; break; + case commandLine: delimiter = " "; break; + case commandLineNul: delimiter = "\0\0"; break; + } + + write(data.joiner(delimiter)); + if (delimiter != "\0\0") writeln(); + } + deprecated("Use the overload taking GeneratorSettings instead. Will be removed for version 1.0.0.") + void listProjectData(BuildPlatform platform, string config, string buildType, + string[] requestedData, Compiler formattingCompiler, bool null_delim) + { + GeneratorSettings settings; + settings.platform = platform; + settings.compiler = formattingCompiler; + settings.config = config; + settings.buildType = buildType; + ListBuildSettingsFormat lt; + with (ListBuildSettingsFormat) + lt = formattingCompiler ? (null_delim ? commandLineNul : commandLine) : (null_delim ? listNul : list); + listProjectData(settings, requestedData, lt); } /// Cleans intermediate/cache files of the given package @@ -508,7 +595,7 @@ /// Returns all cached packages as a "packageId" = "version" associative array - string[string] cachedPackages() const { return m_project.cachedPackagesIDs; } + deprecated("Will be removed for version 1.0.0.") string[string] cachedPackages() const { return m_project.cachedPackagesIDs; } /// Fetches the package matching the dependency and places it in the specified location. Package fetch(string packageId, const Dependency dep, PlacementLocation location, FetchOptions options, string reason = "") @@ -517,7 +604,7 @@ PackageSupplier supplier; foreach(ps; m_packageSuppliers){ try { - pinfo = ps.getPackageDescription(packageId, dep, (options & FetchOptions.usePrerelease) != 0); + pinfo = ps.fetchPackageRecipe(packageId, dep, (options & FetchOptions.usePrerelease) != 0); supplier = ps; break; } catch(Exception e) { @@ -544,9 +631,9 @@ } if (options & FetchOptions.printOnly) { - if (existing && existing.vers != ver) + if (existing && existing.version_ != Version(ver)) logInfo("A new version for %s is available (%s -> %s). Run \"dub upgrade %s\" to switch.", - packageId, existing.vers, ver, packageId); + packageId, existing.version_, ver, packageId); return null; } @@ -589,16 +676,23 @@ } auto path = getTempFile(packageId, ".zip"); - supplier.retrievePackage(path, packageId, dep, (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail? + supplier.fetchPackage(path, packageId, dep, (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail? scope(exit) std.file.remove(path.toNativeString()); logInfo("Placing %s %s to %s...", packageId, ver, placement.toNativeString()); return m_packageManager.storeFetchedPackage(path, pinfo, dstpath); } - /// Removes a given package from the list of present/cached modules. - /// @removeFromApplication: if true, this will also remove an entry in the - /// list of dependencies in the application's dub.json + /** Removes a specific locally cached package. + + This will delete the package files from disk and removes the + corresponding entry from the list of known packages. + + Params: + pack = Package instance to remove + force_remove = Forces removal of the package, even if untracked + files are found in its folder. + */ void remove(in Package pack, bool force_remove) { logInfo("Removing %s in %s", pack.name, pack.path.toNativeString()); @@ -608,21 +702,27 @@ /// @see remove(string, string, RemoveLocation) enum RemoveVersionWildcard = "*"; - /// This will remove a given package with a specified version from the - /// location. - /// It will remove at most one package, unless @param version_ is - /// specified as wildcard "*". - /// @param package_id Package to be removed - /// @param version_ Identifying a version or a wild card. An empty string - /// may be passed into. In this case the package will be removed from the - /// location, if there is only one version retrieved. This will throw an - /// exception, if there are multiple versions retrieved. - /// Note: as wildcard string only RemoveVersionWildcard ("*") is supported. - /// @param location_ - void remove(string package_id, string version_, PlacementLocation location_, bool force_remove) + /** Removes one or more versions of a locally cached package. + + This will remove a given package with a specified version from the + given location. It will remove at most one package, unless `version_` + is set to `RemoveVersionWildcard`. + + Params: + package_id = Name of the package to be removed + version_ = Identifying a version or a wild card. If an empty string + is passed, the package will be removed from the location, if + there is only one version retrieved. This will throw an + exception, if there are multiple versions retrieved. + location_ = Specifies the location to look for the given package + name/version. + force_remove = Forces removal of the package, even if untracked + files are found in its folder. + */ + void remove(string package_id, string version_, PlacementLocation location, bool force_remove) { enforce(!package_id.empty); - if (location_ == PlacementLocation.local) { + if (location == PlacementLocation.local) { logInfo("To remove a locally placed package, make sure you don't have any data" ~ "\nleft in it's directory and then simply remove the whole directory."); throw new Exception("dub cannot remove locally installed packages."); @@ -633,21 +733,21 @@ // Retrieve packages to be removed. foreach(pack; m_packageManager.getPackageIterator(package_id)) - if ((wildcardOrEmpty || pack.vers == version_) && m_packageManager.isManagedPackage(pack)) + if ((wildcardOrEmpty || pack.version_ == Version(version_)) && m_packageManager.isManagedPackage(pack)) packages ~= pack; // Check validity of packages to be removed. if(packages.empty) { throw new Exception("Cannot find package to remove. (" - ~ "id: '" ~ package_id ~ "', version: '" ~ version_ ~ "', location: '" ~ to!string(location_) ~ "'" + ~ "id: '" ~ package_id ~ "', version: '" ~ version_ ~ "', location: '" ~ to!string(location) ~ "'" ~ ")"); } if(version_.empty && packages.length > 1) { logError("Cannot remove package '" ~ package_id ~ "', there are multiple possibilities at location\n" - ~ "'" ~ to!string(location_) ~ "'."); + ~ "'" ~ to!string(location) ~ "'."); logError("Available versions:"); foreach(pack; packages) - logError(" %s", pack.vers); + logError(" %s", pack.version_); throw new Exception("Please specify a individual version using --version=... or use the" ~ " wildcard --version=" ~ RemoveVersionWildcard ~ " to remove all versions."); } @@ -656,38 +756,89 @@ foreach(pack; packages) { try { remove(pack, force_remove); - logInfo("Removed %s, version %s.", package_id, pack.vers); + logInfo("Removed %s, version %s.", package_id, pack.version_); } catch (Exception e) { - logError("Failed to remove %s %s: %s", package_id, pack.vers, e.msg); + logError("Failed to remove %s %s: %s", package_id, pack.version_, e.msg); logInfo("Continuing with other packages (if any)."); } } } + /** Adds a directory to the list of locally known packages. + + Forwards to `PackageManager.addLocalPackage`. + + Params: + path = Path to the package + ver = Optional version to associate with the package (can be left + empty) + system = Make the package known system wide instead of user wide + (requires administrator privileges). + + See_Also: `removeLocalPackage` + */ void addLocalPackage(string path, string ver, bool system) { if (m_dryRun) return; m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? LocalPackageType.system : LocalPackageType.user); } + /** Removes a directory from the list of locally known packages. + + Forwards to `PackageManager.removeLocalPackage`. + + Params: + path = Path to the package + system = Make the package known system wide instead of user wide + (requires administrator privileges). + + See_Also: `addLocalPackage` + */ void removeLocalPackage(string path, bool system) { if (m_dryRun) return; m_packageManager.removeLocalPackage(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); } + /** Registers a local directory to search for packages to use for satisfying + dependencies. + + Params: + path = Path to a directory containing package directories + system = Make the package known system wide instead of user wide + (requires administrator privileges). + + See_Also: `removeSearchPath` + */ void addSearchPath(string path, bool system) { if (m_dryRun) return; m_packageManager.addSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); } + /** Unregisters a local directory search path. + + Params: + path = Path to a directory containing package directories + system = Make the package known system wide instead of user wide + (requires administrator privileges). + + See_Also: `addSearchPath` + */ void removeSearchPath(string path, bool system) { if (m_dryRun) return; m_packageManager.removeSearchPath(makeAbsolute(path), system ? LocalPackageType.system : LocalPackageType.user); } + /** Queries all package suppliers with the given query string. + + Returns a list of tuples, where the first entry is the human readable + name of the package supplier and the second entry is the list of + matched packages. + + See_Also: `PackageSupplier.searchPackages` + */ auto searchPackages(string query) { return m_packageSuppliers.map!(ps => tuple(ps.description, ps.searchPackages(query))).array @@ -737,6 +888,17 @@ else return vers[$-1]; } + /** Initializes a directory with a package skeleton. + + Params: + path = Path of the directory to create the new package in. The + directory will be created if it doesn't exist. + deps = List of dependencies to add to the package recipe. + type = Specifies the type of the application skeleton to use. + format = Determines the package recipe format to use. + recipe_callback = Optional callback that can be used to + customize the recipe before it gets written. + */ void createEmptyPackage(Path path, string[] deps, string type, PackageFormat format = PackageFormat.sdl, scope void delegate(ref PackageRecipe, ref PackageFormat) recipe_callback = null) @@ -771,7 +933,7 @@ logInfo("Successfully created an empty project in '%s'.", path.toNativeString()); } - /** Converts the package recipe to the given format. + /** Converts the package recipe of the loaded root package to the given format. Params: destination_file_ext = The file extension matching the desired @@ -782,23 +944,29 @@ import std.path : extension; import dub.recipe.io : writePackageRecipe; - auto srcfile = m_project.rootPackage.packageInfoFilename; + auto srcfile = m_project.rootPackage.recipePath; auto srcext = srcfile[$-1].toString().extension; if (srcext == "."~destination_file_ext) { logInfo("Package format is already %s.", destination_file_ext); return; } - writePackageRecipe(srcfile[0 .. $-1] ~ ("dub."~destination_file_ext), m_project.rootPackage.info); + writePackageRecipe(srcfile[0 .. $-1] ~ ("dub."~destination_file_ext), m_project.rootPackage.recipe); removeFile(srcfile); } + /** Runs DDOX to generate or serve documentation. + + Params: + run = If set to true, serves documentation on a local web server. + Otherwise generates actual HTML files. + */ void runDdox(bool run) { if (m_dryRun) return; // allow to choose a custom ddox tool - auto tool = m_project.rootPackage.info.ddoxTool; + auto tool = m_project.rootPackage.recipe.ddoxTool; if (tool.empty) tool = "ddox"; auto tool_pack = m_packageManager.getBestPackage(tool, ">=0.0.0"); @@ -808,7 +976,7 @@ tool_pack = fetch(tool, Dependency(">=0.0.0"), defaultPlacementLocation, FetchOptions.none); } - auto ddox_dub = new Dub(m_packageSuppliers); + auto ddox_dub = new Dub(null, m_packageSuppliers); ddox_dub.loadPackage(tool_pack.path); ddox_dub.upgrade(UpgradeOptions.select); @@ -821,7 +989,7 @@ settings.buildType = "debug"; settings.run = true; - auto filterargs = m_project.rootPackage.info.ddoxFilterArgs.dup; + auto filterargs = m_project.rootPackage.recipe.ddoxFilterArgs.dup; if (filterargs.empty) filterargs = ["--min-protection=Protected", "--only-documented"]; settings.runArgs = "filter" ~ filterargs ~ "docs.json"; @@ -863,110 +1031,46 @@ } } + private void determineDefaultCompiler() + { + import std.process : environment; + + m_defaultCompiler = m_userConfig["defaultCompiler"].opt!string(); + if (m_defaultCompiler.length) return; + + m_defaultCompiler = m_systemConfig["defaultCompiler"].opt!string(); + if (m_defaultCompiler.length) return; + + version (Windows) enum sep = ";", exe = ".exe"; + version (Posix) enum sep = ":", exe = ""; + + auto compilers = ["dmd", "gdc", "gdmd", "ldc2", "ldmd2"]; + + auto paths = environment.get("PATH", "").splitter(sep).map!Path; + auto res = compilers.find!(bin => paths.canFind!(p => existsFile(p ~ (bin~exe)))); + m_defaultCompiler = res.empty ? compilers[0] : res.front; + } + private Path makeAbsolute(Path p) const { return p.absolute ? p : m_rootPath ~ p; } private Path makeAbsolute(string p) const { return makeAbsolute(Path(p)); } } -string determineModuleName(BuildSettings settings, Path file, Path base_path) +deprecated("Will be removed for version 1.0.0.") alias determineModuleName = dub.internal.utils.determineModuleName; +deprecated("Will be removed for version 1.0.0.") alias getModuleNameFromContent = dub.internal.utils.getModuleNameFromContent; +deprecated("Will be removed for version 1.0.0.") alias getModuleNameFromFile = dub.internal.utils.getModuleNameFromFile; + + +/// Option flags for `Dub.fetch` +enum FetchOptions { - assert(base_path.absolute); - if (!file.absolute) file = base_path ~ file; - - size_t path_skip = 0; - foreach (ipath; settings.importPaths.map!(p => Path(p))) { - if (!ipath.absolute) ipath = base_path ~ ipath; - assert(!ipath.empty); - if (file.startsWith(ipath) && ipath.length > path_skip) - path_skip = ipath.length; - } - - enforce(path_skip > 0, - format("Source file '%s' not found in any import path.", file.toNativeString())); - - auto mpath = file[path_skip .. file.length]; - auto ret = appender!string; - - //search for module keyword in file - string moduleName = getModuleNameFromFile(file.to!string); - - if(moduleName.length) return moduleName; - - //create module name from path - foreach (i; 0 .. mpath.length) { - import std.path; - auto p = mpath[i].toString(); - if (p == "package.d") break; - if (i > 0) ret ~= "."; - if (i+1 < mpath.length) ret ~= p; - else ret ~= p.baseName(".d"); - } - - return ret.data; + none = 0, + forceBranchUpgrade = 1<<0, + usePrerelease = 1<<1, + forceRemove = 1<<2, + printOnly = 1<<3, } -/** - * Search for module keyword in D Code - */ -string getModuleNameFromContent(string content) { - import std.regex; - import std.string; - - content = content.strip; - if (!content.length) return null; - - static bool regex_initialized = false; - static Regex!char comments_pattern, module_pattern; - - if (!regex_initialized) { - comments_pattern = regex(`(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|(//.*)`, "g"); - module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g"); - regex_initialized = true; - } - - content = replaceAll(content, comments_pattern, ""); - auto result = matchFirst(content, module_pattern); - - string moduleName; - if(!result.empty) moduleName = result.front; - - if (moduleName.length >= 7) moduleName = moduleName[7..$-1]; - - return moduleName; -} - -unittest { - //test empty string - string name = getModuleNameFromContent(""); - assert(name == "", "can't get module name from empty string"); - - //test simple name - name = getModuleNameFromContent("module myPackage.myModule;"); - assert(name == "myPackage.myModule", "can't parse module name"); - - //test if it can ignore module inside comments - name = getModuleNameFromContent("/** - module fakePackage.fakeModule; - */ - module myPackage.myModule;"); - - assert(name == "myPackage.myModule", "can't parse module name"); - - name = getModuleNameFromContent("//module fakePackage.fakeModule; - module myPackage.myModule;"); - - assert(name == "myPackage.myModule", "can't parse module name"); -} - -/** - * Search for module keyword in file - */ -string getModuleNameFromFile(string filePath) { - string fileContent = filePath.readText; - - logDiagnostic("Get module name from path: " ~ filePath); - return getModuleNameFromContent(fileContent); -} - +/// Option flags for `Dub.upgrade` enum UpgradeOptions { none = 0, @@ -978,13 +1082,17 @@ useCachedResult = 1<<6, /// Use cached information stored with the package to determine upgrades } -enum SkipRegistry { - none, - standard, - all +/// Determines which of the default package suppliers are queried for packages. +enum SkipPackageSuppliers { + none, /// Uses all configured package suppliers. + standard, /// Does not use the default package suppliers (`defaultPackageSuppliers`). + all /// Uses only manually specified package suppliers. } -class DependencyVersionResolver : DependencyResolver!(Dependency, Dependency) { +deprecated("Use SkipPackageSuppliers instead. Will be removed for version 1.0.0.") +alias SkipRegistry = SkipPackageSuppliers; + +private class DependencyVersionResolver : DependencyResolver!(Dependency, Dependency) { protected { Dub m_dub; UpgradeOptions m_options; @@ -1005,7 +1113,7 @@ { m_rootPackage = root; m_selectedVersions = selected_versions; - return super.resolve(TreeNode(root.name, Dependency(root.ver)), (m_options & UpgradeOptions.printUpgradesOnly) == 0); + return super.resolve(TreeNode(root.name, Dependency(root.version_)), (m_options & UpgradeOptions.printUpgradesOnly) == 0); } protected override Dependency[] getAllConfigs(string pack) @@ -1023,7 +1131,7 @@ logDiagnostic("Search for versions of %s (%s package suppliers)", pack, m_dub.m_packageSuppliers.length); Version[] versions; foreach (p; m_dub.packageManager.getPackageIterator(pack)) - versions ~= p.ver; + versions ~= p.version_; foreach (ps; m_dub.m_packageSuppliers) { try { @@ -1082,28 +1190,28 @@ } auto basepack = pack.basePackage; - foreach (dname, dspec; pack.dependencies) { - auto dbasename = getBasePackageName(dname); + foreach (d; pack.getAllDependencies()) { + auto dbasename = getBasePackageName(d.name); // detect dependencies to the root package (or sub packages thereof) if (dbasename == basepack.name) { - auto absdeppath = dspec.mapToPath(pack.path).path; + auto absdeppath = d.spec.mapToPath(pack.path).path; absdeppath.endsWithSlash = true; - auto subpack = m_dub.m_packageManager.getSubPackage(basepack, getSubPackageName(dname), true); + auto subpack = m_dub.m_packageManager.getSubPackage(basepack, getSubPackageName(d.name), true); if (subpack) { - auto desireddeppath = dname == dbasename ? basepack.path : subpack.path; + auto desireddeppath = d.name == dbasename ? basepack.path : subpack.path; desireddeppath.endsWithSlash = true; - enforce(dspec.path.empty || absdeppath == desireddeppath, + enforce(d.spec.path.empty || absdeppath == desireddeppath, format("Dependency from %s to root package references wrong path: %s vs. %s", node.pack, absdeppath.toNativeString(), desireddeppath.toNativeString())); } - ret ~= TreeNodes(dname, node.config); + ret ~= TreeNodes(d.name, node.config); continue; } DependencyType dt; - if (dspec.optional) { - if (dspec.default_) dt = DependencyType.optionalDefault; + if (d.spec.optional) { + if (d.spec.default_) dt = DependencyType.optionalDefault; else dt = DependencyType.optional; } else dt = DependencyType.required; @@ -1111,11 +1219,11 @@ // keep deselected dependencies deselected by default if (m_selectedVersions && !m_selectedVersions.bare && dt == DependencyType.optionalDefault) dt = DependencyType.optional; - ret ~= TreeNodes(dname, dspec.mapToPath(pack.path), dt); + ret ~= TreeNodes(d.name, d.spec.mapToPath(pack.path), dt); } else { // keep already selected optional dependencies if possible if (dt == DependencyType.optional) dt = DependencyType.optionalDefault; - ret ~= TreeNodes(dname, m_selectedVersions.getSelectedVersion(dbasename), dt); + ret ~= TreeNodes(d.name, m_selectedVersions.getSelectedVersion(dbasename), dt); } } return ret.data; @@ -1160,7 +1268,7 @@ if (!dep.path.empty) { try { auto ret = m_dub.packageManager.getOrLoadPackage(dep.path); - if (dep.matches(ret.ver)) return ret; + if (dep.matches(ret.version_)) return ret; } catch (Exception e) { logDiagnostic("Failed to load path based dependency %s: %s", name, e.msg); logDebug("Full error: %s", e.toString().sanitize); @@ -1182,7 +1290,7 @@ foreach (ps; m_dub.m_packageSuppliers) { if (rootpack == name) { try { - auto desc = ps.getPackageDescription(name, dep, prerelease); + auto desc = ps.fetchPackageRecipe(name, dep, prerelease); auto ret = new Package(desc); m_remotePackages[key] = ret; return ret; @@ -1196,7 +1304,7 @@ FetchOptions fetchOpts; fetchOpts |= prerelease ? FetchOptions.usePrerelease : FetchOptions.none; fetchOpts |= (m_options & UpgradeOptions.forceRemove) != 0 ? FetchOptions.forceRemove : FetchOptions.none; - m_dub.fetch(rootpack, dep, defaultPlacementLocation, fetchOpts, "need sub package description"); + m_dub.fetch(rootpack, dep, m_dub.defaultPlacementLocation, fetchOpts, "need sub package description"); auto ret = m_dub.m_packageManager.getBestPackage(name, dep); if (!ret) { logWarn("Package %s %s doesn't have a sub package %s", rootpack, dep.version_, name); diff --git a/source/dub/generators/build.d b/source/dub/generators/build.d index 8db2c32..57267d3 100644 --- a/source/dub/generators/build.d +++ b/source/dub/generators/build.d @@ -8,6 +8,7 @@ module dub.generators.build; import dub.compilers.compiler; +import dub.compilers.utils; import dub.generators.generator; import dub.internal.utils; import dub.internal.vibecompat.core.file; @@ -142,13 +143,13 @@ return cached; } - bool performCachedBuild(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, string build_id, in Package[] packages, in Path[] additional_dep_files) + private bool performCachedBuild(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, string build_id, in Package[] packages, in Path[] additional_dep_files) { auto cwd = Path(getcwd()); auto target_path = pack.path ~ format(".dub/build/%s/", build_id); if (!settings.force && isUpToDate(target_path, buildsettings, settings.platform, pack, packages, additional_dep_files)) { - logInfo("%s %s: target for configuration \"%s\" is up to date.", pack.name, pack.vers, config); + logInfo("%s %s: target for configuration \"%s\" is up to date.", pack.name, pack.version_, config); logDiagnostic("Using existing build in %s.", target_path.toNativeString()); copyTargetFile(target_path, buildsettings, settings.platform); return true; @@ -164,7 +165,7 @@ // determine basic build properties auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); - logInfo("%s %s: building configuration \"%s\"...", pack.name, pack.vers, config); + logInfo("%s %s: building configuration \"%s\"...", pack.name, pack.version_, config); if( buildsettings.preBuildCommands.length ){ logInfo("Running pre-build commands..."); @@ -181,7 +182,7 @@ return false; } - void performRDMDBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config) + private void performRDMDBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config) { auto cwd = Path(getcwd()); //Added check for existance of [AppNameInPackagejson].d @@ -232,7 +233,7 @@ runCommands(buildsettings.preBuildCommands); } - logInfo("%s %s: building configuration \"%s\"...", pack.name, pack.vers, config); + logInfo("%s %s: building configuration \"%s\"...", pack.name, pack.version_, config); logInfo("Running rdmd..."); logDiagnostic("rdmd %s", join(flags, " ")); @@ -247,7 +248,7 @@ } } - void performDirectBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config) + private void performDirectBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config) { auto cwd = Path(getcwd()); @@ -261,7 +262,7 @@ f = fp.toNativeString(); } - logInfo("%s %s: building configuration \"%s\"...", pack.name, pack.vers, config); + logInfo("%s %s: building configuration \"%s\"...", pack.name, pack.version_, config); // make all target/import paths relative string makeRelative(string path) { @@ -359,7 +360,7 @@ allfiles ~= buildsettings.stringImportFiles; // TODO: add library files foreach (p; packages) - allfiles ~= (p.packageInfoFilename != Path.init ? p : p.basePackage).packageInfoFilename.toNativeString(); + allfiles ~= (p.recipePath != Path.init ? p : p.basePackage).recipePath.toNativeString(); foreach (f; additional_dep_files) allfiles ~= f.toNativeString(); if (main_pack is m_project.rootPackage) allfiles ~= (main_pack.path ~ SelectedVersions.defaultFile).toNativeString(); @@ -404,7 +405,7 @@ return objPath; } - void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings) + private void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings) { auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; @@ -480,7 +481,7 @@ } } - void runTarget(Path exe_file_path, in BuildSettings buildsettings, string[] run_args, GeneratorSettings settings) + private void runTarget(Path exe_file_path, in BuildSettings buildsettings, string[] run_args, GeneratorSettings settings) { if (buildsettings.targetType == TargetType.executable) { auto cwd = Path(getcwd()); @@ -515,7 +516,7 @@ enforce(false, "Target is a library. Skipping execution."); } - void cleanupTemporaries() + private void cleanupTemporaries() { foreach_reverse (f; m_temporaryFiles) { try { @@ -537,20 +538,3 @@ return prj.path ~ f; return prj.path ~ "source/app.d"; } - -unittest { - version (Windows) { - assert(isLinkerFile("test.obj")); - assert(isLinkerFile("test.lib")); - assert(isLinkerFile("test.res")); - assert(!isLinkerFile("test.o")); - assert(!isLinkerFile("test.d")); - } else { - assert(isLinkerFile("test.o")); - assert(isLinkerFile("test.a")); - assert(isLinkerFile("test.so")); - assert(isLinkerFile("test.dylib")); - assert(!isLinkerFile("test.obj")); - assert(!isLinkerFile("test.d")); - } -} diff --git a/source/dub/generators/cmake.d b/source/dub/generators/cmake.d index 529cccd..fcf0b94 100644 --- a/source/dub/generators/cmake.d +++ b/source/dub/generators/cmake.d @@ -95,14 +95,14 @@ script.put( "target_link_libraries(%s %s %s)\n".format( name, - (info.dependencies ~ info.linkDependencies).dup.stdsort.uniq.map!sanitize.join(" "), + (info.dependencies ~ info.linkDependencies).dup.stdsort.uniq.map!(s => sanitize(s)).join(" "), info.buildSettings.libs.dup.join(" ") ) ); script.put( `set_target_properties(%s PROPERTIES TEXT_INCLUDE_DIRECTORIES "%s")`.format( name, - info.buildSettings.stringImportPaths.map!sanitizeSlashes.join(";") + info.buildSettings.stringImportPaths.map!(s => sanitizeSlashes(s)).join(";") ) ~ "\n" ); } @@ -136,12 +136,12 @@ } ///Transform a package name into a valid CMake target name. -string sanitize(string name) +private string sanitize(string name) { return name.replace(":", "_"); } -string sanitizeSlashes(string path) +private string sanitizeSlashes(string path) { version(Windows) return path.replace("\\", "/"); diff --git a/source/dub/generators/generator.d b/source/dub/generators/generator.d index 78dc4f1..6f99cf4 100644 --- a/source/dub/generators/generator.d +++ b/source/dub/generators/generator.d @@ -1,7 +1,7 @@ /** Generator for project files - Copyright: © 2012-2013 Matthias Dondorff + Copyright: © 2012-2013 Matthias Dondorff, © 2013-2016 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Matthias Dondorff */ @@ -85,6 +85,8 @@ */ final void generate(GeneratorSettings settings) { + import dub.compilers.utils : enforceBuildRequirements; + if (!settings.config.length) settings.config = m_project.getDefaultConfiguration(settings.platform); TargetInfo[string] targets; @@ -144,6 +146,7 @@ private BuildSettings collect(GeneratorSettings settings, Package pack, ref TargetInfo[string] targets, in string[string] configs, ref string[] main_files, string bin_pack) { import std.algorithm : sort; + import dub.compilers.utils : isLinkerFile; if (auto pt = pack.name in targets) return pt.buildSettings; @@ -188,9 +191,9 @@ if (is_target) targets[pack.name] = TargetInfo(pack, [pack], configs[pack.name], buildsettings, null); - foreach (depname; pack.dependencies.byKey.array.sort()) { - auto depspec = pack.dependencies[depname]; - if (!pack.hasDependency(depname, configs[pack.name])) continue; + auto deps = pack.getDependencies(configs[pack.name]); + foreach (depname; deps.keys.sort()) { + auto depspec = deps[depname]; auto dep = m_project.getDependency(depname, depspec.optional); if (!dep) continue; @@ -452,6 +455,14 @@ } } + +/** Runs a list of build commands for a particular package. + + This funtion sets all DUB speficic environment variables and makes sure + that recursive dub invocations are detected and don't result in infinite + command execution loops. The latter could otherwise happen when a command + runs "dub describe" or similar functionality. +*/ void runBuildCommands(in string[] commands, in Package pack, in Project proj, in GeneratorSettings settings, in BuildSettings build_settings) { @@ -505,3 +516,27 @@ storeRecursiveInvokations(env, proj.rootPackage.name ~ depNames); runCommands(commands, env); } + +private bool isRecursiveInvocation(string pack) +{ + import std.algorithm : canFind, splitter; + import std.process : environment; + + return environment + .get("DUB_PACKAGES_USED", "") + .splitter(",") + .canFind(pack); +} + +private void storeRecursiveInvokations(string[string] env, string[] packs) +{ + import std.algorithm : canFind, splitter; + import std.range : chain; + import std.process : environment; + + env["DUB_PACKAGES_USED"] = environment + .get("DUB_PACKAGES_USED", "") + .splitter(",") + .chain(packs) + .join(","); +} diff --git a/source/dub/generators/sublimetext.d b/source/dub/generators/sublimetext.d index 7b35199..d3970fa 100644 --- a/source/dub/generators/sublimetext.d +++ b/source/dub/generators/sublimetext.d @@ -36,7 +36,7 @@ logDebug("About to generate sublime project for %s.", m_project.rootPackage.name); auto root = Json([ - "folders": targets.byValue.map!targetFolderJson.array.Json, + "folders": targets.byValue.map!(f => targetFolderJson(f)).array.Json, "build_systems": buildSystems(settings.platform), "settings": [ "include_paths": buildSettings.importPaths.map!Json.array.Json ].Json, ]); @@ -53,7 +53,7 @@ } -Json targetFolderJson(in ProjectGenerator.TargetInfo target) +private Json targetFolderJson(in ProjectGenerator.TargetInfo target) { return [ "name": target.pack.name.Json, @@ -64,7 +64,7 @@ } -Json buildSystems(BuildPlatform buildPlatform, string workingDiretory = getcwd()) +private Json buildSystems(BuildPlatform buildPlatform, string workingDiretory = getcwd()) { enum BUILD_TYPES = [ //"plain", diff --git a/source/dub/generators/targetdescription.d b/source/dub/generators/targetdescription.d index cd0032e..5f6fb5e 100644 --- a/source/dub/generators/targetdescription.d +++ b/source/dub/generators/targetdescription.d @@ -9,6 +9,7 @@ import dub.compilers.buildsettings; import dub.compilers.compiler; +import dub.compilers.utils : getTargetFileName; import dub.description; import dub.generators.generator; import dub.internal.vibecompat.inet.path; diff --git a/source/dub/generators/visuald.d b/source/dub/generators/visuald.d index f4e7abb..a7ace29 100644 --- a/source/dub/generators/visuald.d +++ b/source/dub/generators/visuald.d @@ -45,7 +45,7 @@ override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets) { - logDebug("About to generate projects for %s, with %s direct dependencies.", m_project.rootPackage.name, m_project.rootPackage.dependencies.length); + logDebug("About to generate projects for %s, with %s direct dependencies.", m_project.rootPackage.name, m_project.rootPackage.getAllDependencies().length); generateProjectFiles(settings, targets); generateSolutionFile(settings, targets); } @@ -153,6 +153,8 @@ void generateProjectFile(string packname, GeneratorSettings settings, in TargetInfo[string] targets) { + import dub.compilers.utils : isLinkerFile; + int i = 0; auto ret = appender!(char[])(); @@ -188,8 +190,8 @@ } foreach (p; targets[packname].packages) - if (!p.packageInfoFilename.empty) - addFile(p.packageInfoFilename.toNativeString(), false); + if (!p.recipePath.empty) + addFile(p.recipePath.toNativeString(), false); if (files.targetType == TargetType.staticLibrary) foreach(s; files.sourceFiles.filter!(s => !isLinkerFile(s))) addFile(s, true); @@ -446,7 +448,7 @@ } // TODO: nice folders - struct SourceFile { + private struct SourceFile { Path structurePath; Path filePath; bool build; @@ -478,7 +480,7 @@ } } - auto sortedSources(SourceFile[] sources) { + private auto sortedSources(SourceFile[] sources) { return sort(sources); } diff --git a/source/dub/init.d b/source/dub/init.d index a73657a..d2ee8f2 100644 --- a/source/dub/init.d +++ b/source/dub/init.d @@ -1,7 +1,7 @@ /** - Empty package initialization code. + Package skeleton initialization code. - Copyright: © 2013-2015 rejectedsoftware e.K. + Copyright: © 2013-2016 rejectedsoftware e.K. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ @@ -20,7 +20,27 @@ import std.process; import std.string; -void initPackage(Path root_path, string[string] deps, string type, PackageFormat format, scope void delegate(ref PackageRecipe, ref PackageFormat) recipe_callback = null) + +/** Intializes a new package in the given directory. + + The given `root_path` will be checked for any of the files that will be + created by this function. If any exist, an exception will be thrown before + altering the directory. + + Params: + root_path = Directory in which to create the new package. If the + directory doesn't exist, a new one will be created. + deps = A set of extra dependencies to add to the package recipe. The + associative array is expected to map from package name to package + version. + type = The type of package skeleton to create. Can currently be + "minimal", "vibe.d" or "deimos" + recipe_callback = Optional callback that can be used to customize the + package recipe and the file format used to store it prior to + writing it to disk. +*/ +void initPackage(Path root_path, string[string] deps, string type, + PackageFormat format, scope void delegate(ref PackageRecipe, ref PackageFormat) recipe_callback = null) { import std.conv : to; import dub.recipe.io : writePackageRecipe; @@ -83,7 +103,7 @@ private void initVibeDPackage(Path root_path, ref PackageRecipe p) { - if("vibe-d" !in p.dependencies) + if ("vibe-d" !in p.buildSettings.dependencies) p.buildSettings.dependencies["vibe-d"] = Dependency("~>0.7.26"); p.description = "A simple vibe.d server application."; p.buildSettings.versions[""] ~= "VibeDefaultMain"; @@ -123,7 +143,7 @@ createDirectory(root_path ~ "deimos"); } -void writeGitignore(Path root_path) +private void writeGitignore(Path root_path) { write((root_path ~ ".gitignore").toNativeString(), ".dub\ndocs.json\n__dummy.html\n*.o\n*.obj\n"); diff --git a/source/dub/internal/utils.d b/source/dub/internal/utils.d index 13b6fae..83fb0ab 100644 --- a/source/dub/internal/utils.d +++ b/source/dub/internal/utils.d @@ -11,6 +11,7 @@ import dub.internal.vibecompat.core.log; import dub.internal.vibecompat.data.json; import dub.internal.vibecompat.inet.url; +import dub.compilers.buildsettings : BuildSettings; import dub.version_; // todo: cleanup imports. @@ -356,3 +357,121 @@ .filter!(member => member==0? value==0 : (value & member) == member) .map!(member => to!string(member)); } + + +bool isIdentChar(dchar ch) +{ + import std.ascii : isAlphaNum; + return isAlphaNum(ch) || ch == '_'; +} + +string stripDlangSpecialChars(string s) +{ + import std.array : appender; + auto ret = appender!string(); + foreach(ch; s) + ret.put(isIdentChar(ch) ? ch : '_'); + return ret.data; +} + +string determineModuleName(BuildSettings settings, Path file, Path base_path) +{ + import std.algorithm : map; + + assert(base_path.absolute); + if (!file.absolute) file = base_path ~ file; + + size_t path_skip = 0; + foreach (ipath; settings.importPaths.map!(p => Path(p))) { + if (!ipath.absolute) ipath = base_path ~ ipath; + assert(!ipath.empty); + if (file.startsWith(ipath) && ipath.length > path_skip) + path_skip = ipath.length; + } + + enforce(path_skip > 0, + format("Source file '%s' not found in any import path.", file.toNativeString())); + + auto mpath = file[path_skip .. file.length]; + auto ret = appender!string; + + //search for module keyword in file + string moduleName = getModuleNameFromFile(file.to!string); + + if(moduleName.length) return moduleName; + + //create module name from path + foreach (i; 0 .. mpath.length) { + import std.path; + auto p = mpath[i].toString(); + if (p == "package.d") break; + if (i > 0) ret ~= "."; + if (i+1 < mpath.length) ret ~= p; + else ret ~= p.baseName(".d"); + } + + return ret.data; +} + +/** + * Search for module keyword in D Code + */ +string getModuleNameFromContent(string content) { + import std.regex; + import std.string; + + content = content.strip; + if (!content.length) return null; + + static bool regex_initialized = false; + static Regex!char comments_pattern, module_pattern; + + if (!regex_initialized) { + comments_pattern = regex(`(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|(//.*)`, "g"); + module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g"); + regex_initialized = true; + } + + content = replaceAll(content, comments_pattern, ""); + auto result = matchFirst(content, module_pattern); + + string moduleName; + if(!result.empty) moduleName = result.front; + + if (moduleName.length >= 7) moduleName = moduleName[7..$-1]; + + return moduleName; +} + +unittest { + //test empty string + string name = getModuleNameFromContent(""); + assert(name == "", "can't get module name from empty string"); + + //test simple name + name = getModuleNameFromContent("module myPackage.myModule;"); + assert(name == "myPackage.myModule", "can't parse module name"); + + //test if it can ignore module inside comments + name = getModuleNameFromContent("/** + module fakePackage.fakeModule; + */ + module myPackage.myModule;"); + + assert(name == "myPackage.myModule", "can't parse module name"); + + name = getModuleNameFromContent("//module fakePackage.fakeModule; + module myPackage.myModule;"); + + assert(name == "myPackage.myModule", "can't parse module name"); +} + +/** + * Search for module keyword in file + */ +string getModuleNameFromFile(string filePath) { + string fileContent = filePath.readText; + + logDiagnostic("Get module name from path: " ~ filePath); + return getModuleNameFromContent(fileContent); +} diff --git a/source/dub/package_.d b/source/dub/package_.d index 90b19bd..df986ac 100644 --- a/source/dub/package_.d +++ b/source/dub/package_.d @@ -1,9 +1,9 @@ /** - Stuff with dependencies. + Contains high-level functionality for working with packages. - Copyright: © 2012-2013 Matthias Dondorff, © 2012-2015 Sönke Ludwig + Copyright: © 2012-2013 Matthias Dondorff, © 2012-2016 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. - Authors: Matthias Dondorff + Authors: Matthias Dondorff, Sönke Ludwig, Martin Nowak, Nick Sabalausky */ module dub.package_; @@ -31,9 +31,10 @@ import std.typecons : Nullable; +/// Lists the supported package recipe formats. enum PackageFormat { - json, - sdl + json, /// JSON based, using the ".json" file extension + sdl /// SDLang based, using the ".sdl" file extension } struct FilenameAndFormat { @@ -41,110 +42,100 @@ 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. +/// Supported package descriptions in decreasing order of preference. static immutable FilenameAndFormat[] packageInfoFiles = [ {"dub.json", PackageFormat.json}, {"dub.sdl", PackageFormat.sdl}, {"package.json", PackageFormat.json} ]; +/// Returns a list of all recognized package recipe file names in descending order of precedence. @property string[] packageInfoFilenames() { return packageInfoFiles.map!(f => cast(string)f.filename).array; } +/// Returns the default package recile file name. @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 +/** Represents a package, including its sub packages. */ class Package { private { Path m_path; - PathAndFormat m_infoFile; + Path m_infoFile; PackageRecipe m_info; Package m_parentPackage; } - static PathAndFormat findPackageFile(Path path) + /** Constructs a `Package` using a package that is physically present on the local file system. + + Params: + root = The directory in which the package resides. + recipe_file = Optional path to the package recipe file. If left + empty, the `root` directory will be searched for a recipe file. + parent = Reference to the parent package, if the new package is a + sub package. + version_override = Optional version to associate to the package + instead of the one declared in the package recipe, or the one + determined by invoking the VCS (GIT currently). + */ + deprecated("Use load() instead. Will be removed for version 1.0.0.") + this(Path root, Path recipe_file = Path.init, Package parent = null, string version_override = "") { - foreach(file; packageInfoFiles) { - auto filename = path ~ file.filename; - if(existsFile(filename)) return PathAndFormat(filename, file.format); - } - return PathAndFormat(Path()); + import dub.recipe.io; + + if (recipe_file.empty) recipe_file = findPackageFile(root); + + enforce(!recipe_file.empty, + "No package file found in %s, expected one of %s" + .format(root.toNativeString(), + packageInfoFiles.map!(f => cast(string)f.filename).join("/"))); + + m_infoFile = recipe_file; + + auto recipe = readPackageRecipe(m_infoFile, parent ? parent.name : null); + + this(recipe, root, parent, version_override); } - this(Path root, PathAndFormat infoFile = PathAndFormat(), Package parent = null, string versionOverride = "") + /** Constructs a `Package` using an in-memory package recipe. + + Params: + json_recipe = The package recipe in JSON format + recipe = The package recipe in generic format + root = The directory in which the package resides (if any). + parent = Reference to the parent package, if the new package is a + sub package. + version_override = Optional version to associate to the package + instead of the one declared in the package recipe, or the one + determined by invoking the VCS (GIT currently). + */ + this(Json json_recipe, Path root = Path(), Package parent = null, string version_override = "") { - RawPackage raw_package; - m_infoFile = infoFile; + import dub.recipe.json; - 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; + parseJson(recipe, json_recipe, parent ? parent.name : null); + this(recipe, root, parent, version_override); + } + /// ditto + this(PackageRecipe recipe, Path root = Path(), Package parent = null, string version_override = "") + { + if (!version_override.empty) + recipe.version_ = version_override; - // 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); + // 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 (!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_); - } + 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.masterBranch.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; @@ -156,61 +147,185 @@ simpleLint(); } + /** Searches the given directory for package recipe files. + + Params: + directory = The directory to search + + Returns: + Returns the full path to the package file, if any was found. + Otherwise returns an empty path. + */ + static Path findPackageFile(Path directory) + { + foreach (file; packageInfoFiles) { + auto filename = directory ~ file.filename; + if (existsFile(filename)) return filename; + } + return Path.init; + } + + /** Constructs a `Package` using a package that is physically present on the local file system. + + Params: + root = The directory in which the package resides. + recipe_file = Optional path to the package recipe file. If left + empty, the `root` directory will be searched for a recipe file. + parent = Reference to the parent package, if the new package is a + sub package. + version_override = Optional version to associate to the package + instead of the one declared in the package recipe, or the one + determined by invoking the VCS (GIT currently). + */ + static Package load(Path root, Path recipe_file = Path.init, Package parent = null, string version_override = "") + { + import dub.recipe.io; + + if (recipe_file.empty) recipe_file = findPackageFile(root); + + enforce(!recipe_file.empty, + "No package file found in %s, expected one of %s" + .format(root.toNativeString(), + packageInfoFiles.map!(f => cast(string)f.filename).join("/"))); + + auto recipe = readPackageRecipe(recipe_file, parent ? parent.name : null); + + auto ret = new Package(recipe, root, parent, version_override); + ret.m_infoFile = recipe_file; + return ret; + } + + /** Returns the qualified name of the package. + + The qualified name includes any possible parent package if this package + is a sub package. + */ @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; } + + /** Returns the directory in which the package resides. + + Note that this can be empty for packages that are not stored in the + local file system. + */ @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; } + + + /** Accesses the version associated with this package. + + Note that this is a shortcut to `this.recipe.version_`. + */ + @property Version version_() const { return m_parentPackage ? m_parentPackage.version_ : Version(m_info.version_); } + /// ditto + @property void version_(Version value) { assert(m_parentPackage is null); m_info.version_ = value.toString(); } + + /** Accesses the recipe contents of this package. + */ + @property ref inout(PackageRecipe) recipe() inout { return m_info; } + + /** Returns the path to the package recipe file. + + Note that this can be empty for packages that are not stored in the + local file system. + */ + @property Path recipePath() const { return m_infoFile; } + + + deprecated("Use .version_.toString() instead. Will be removed for version 1.0.0.") + @property string vers() const { return m_parentPackage ? m_parentPackage.vers : m_info.version_; } + + deprecated("Use .version instead. Will be removed for version 1.0.0.") + @property Version ver() const { return Version(this.vers); } + deprecated("Use .version instead. Will be removed for version 1.0.0.") + @property void ver(Version ver) { assert(m_parentPackage is null); m_info.version_ = ver.toString(); } + + deprecated("Use .recipe instead. Will be removed for version 1.0.0.") + @property ref inout(PackageRecipe) info() inout { return m_info; } + + deprecated("Use .recipePath instead. Will be removed for version 1.0.0.") + @property Path packageInfoFilename() const { return m_infoFile; } + + /** Returns a list of all possible dependencies of the package. + + This list includes all dependencies of all configurations. If different + configurations have a dependency to the same package, but with differing + version specifications, only one of them will be returned. Which one + is returned is undefined behavior. + + See_Also: `getAllDependencies` + */ + deprecated("Use getAllDependencies instead. Will be removed for version 1.0.0.") + @property const(Dependency[string]) dependencies() + const { + Dependency[string] ret; + foreach (d; getAllDependencies()) + ret[d.name] = d.spec; + return ret; + } + + /** Returns the base package of this package. + + The base package is the root of the sub package hierarchy (i.e. the + topmost parent). This will be `null` for packages that are not sub + packages. + */ @property inout(Package) basePackage() inout { return m_parentPackage ? m_parentPackage.basePackage : this; } + + /** Returns the parent of this package. + + The parent package is the package that contains a sub package. This will + be `null` for packages that are not sub packages. + */ @property inout(Package) parentPackage() inout { return m_parentPackage; } + + /** Returns the list of all sub packages. + + Note that this is a shortcut for `this.recipe.subPackages`. + */ @property inout(SubPackage)[] subPackages() inout { return m_info.subPackages; } + /** Returns the list of all build configuration names. + + Configuration contents can be accessed using `this.recipe.configurations`. + */ @property string[] configurations() const { auto ret = appender!(string[])(); - foreach( ref config; m_info.configurations ) + 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; - } + /** Writes the current recipe contents to a recipe file. - /** Overwrites the packge description file using the default filename with the current information. + The parameter-less overload writes to `this.path`, which must not be + empty. The default recipe file name will be used in this case. */ void storeInfo() { storeInfo(m_path); - m_infoFile = PathAndFormat(m_path ~ defaultPackageFilename); + m_infoFile = 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."); + enforce(!version_.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()); } + /** Returns the package recipe of a non-path-based sub package. + + For sub packages that are declared within the package recipe of the + parent package, this function will return the corresponding recipe. Sub + packages declared using a path must be loaded manually (or using the + `PackageManager`). + */ Nullable!PackageRecipe getInternalSubPackage(string name) { foreach (ref p; m_info.subPackages) @@ -219,6 +334,11 @@ return Nullable!PackageRecipe(); } + /** Searches for use of compiler-specific flags that have generic + alternatives. + + This will output a warning message for each such flag to the console. + */ void warnOnSpecialCompilerFlags() { // warn about use of special flags @@ -227,6 +347,16 @@ config.buildSettings.warnOnSpecialCompilerFlags(m_info.name, config.name); } + /** Retrieves a build settings template. + + If no `config` is given, this returns the build settings declared at the + root level of the package recipe. Otherwise returns the settings + declared within the given configuration (excluding those at the root + level). + + Note that this is a shortcut to accessing `this.recipe.buildSettings` or + `this.recipe.configurations[].buildSettings`. + */ const(BuildSettingsTemplate) getBuildSettings(string config = null) const { if (config.length) { @@ -239,7 +369,13 @@ } } - /// Returns all BuildSettings for the given platform and config. + /** Returns all BuildSettings for the given platform and configuration. + + This will gather the effective build settings declared in tha package + recipe for when building on a particular platform and configuration. + Root build settings and configuration specific settings will be + merged. + */ BuildSettings getBuildSettings(in BuildPlatform platform, string config) const { BuildSettings ret; @@ -262,7 +398,12 @@ return ret; } - /// Returns the combination of all build settings for all configurations and platforms + /** Returns the combination of all build settings for all configurations + and platforms. + + This can be useful for IDEs to gather a list of all potentially used + files or settings. + */ BuildSettings getCombinedBuildSettings() const { BuildSettings ret; @@ -279,6 +420,13 @@ return ret; } + /** Adds build type specific settings to an existing set of build settings. + + This function searches the package recipe for overridden build types. If + none is found, the default build settings will be applied, if + `build_type` matches a default build type name. An exception is thrown + otherwise. + */ void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type) const { if (build_type == "$DFLAGS") { @@ -310,6 +458,14 @@ } } + /** Returns the selected configuration for a certain dependency. + + If no configuration is specified in the package recipe, null will be + returned instead. + + FIXME: The `platform` parameter is currently ignored, as the + `"subConfigurations"` field doesn't support platform suffixes. + */ string getSubConfiguration(string config, in Package dependency, in BuildPlatform platform) const { bool found = false; @@ -325,7 +481,15 @@ return null; } - /// Returns the default configuration to build for the given platform + /** Returns the default configuration to build for the given platform. + + This will return the first configuration that is applicable to the given + platform, or `null` if none is applicable. By default, only library + configurations will be returned. Setting `allow_non_library` to `true` + will also return executable configurations. + + See_Also: `getPlatformConfigurations` + */ string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library = false) const { foreach (ref conf; m_info.configurations) { @@ -336,47 +500,114 @@ return null; } - /// Returns a list of configurations suitable for the given platform - string[] getPlatformConfigurations(in BuildPlatform platform, bool is_main_package = false) + /** Returns a list of configurations suitable for the given platform. + + Params: + platform = The platform against which to match configurations + allow_non_library = If set to true, executable configurations will + also be included. + + See_Also: `getDefaultConfiguration` + */ + string[] getPlatformConfigurations(in BuildPlatform platform, bool allow_non_library = 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; + if (!allow_non_library && 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 { + + /** Human readable information of this package and its dependencies. + + This will return the name and version of this package, as well as of all + root dependencies. + */ + deprecated("Will be removed for version 1.0.0.") + 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() ~ "'"; + foreach(PackageDependency d; this.getAllDependencies()) + s ~= "\n " ~ d.name ~ ", version '" ~ d.spec.toString() ~ "'"; return s; } - bool hasDependency(string depname, string config) + /** Determines if the package has a dependency to a certain package. + + Params: + dependency_name = The name of the package to search for + config = Name of the configuration to use when searching + for dependencies + + See_Also: `getDependencies` + */ + bool hasDependency(string dependency_name, string config) const { - if (depname in m_info.buildSettings.dependencies) return true; + if (dependency_name in m_info.buildSettings.dependencies) return true; foreach (ref c; m_info.configurations) - if ((config.empty || c.name == config) && depname in c.buildSettings.dependencies) + if ((config.empty || c.name == config) && dependency_name in c.buildSettings.dependencies) return true; return false; } + /** Retrieves all dependencies for a particular configuration. + + This includes dependencies that are declared at the root level of the + package recipe, as well as those declared within the specified + configuration. If no configuration with the given name exists, only + dependencies declared at the root level will be retunred. + + See_Also: `hasDependency` + */ + 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; + } + + /** Returns a list of all possible dependencies of the package. + + This list includes all dependencies of all configurations. The same + package may occur multiple times with possibly different `Dependency` + values. + */ + PackageDependency[] getAllDependencies() + const { + auto ret = appender!(PackageDependency[]); + foreach (n, d; this.recipe.buildSettings.dependencies) + ret ~= PackageDependency(n, d); + foreach (ref c; this.recipe.configurations) + foreach (n, d; c.buildSettings.dependencies) + ret ~= PackageDependency(n, d); + return ret.data; + } + + /** Returns a description of the package for use in IDEs or build tools. */ PackageDescription describe(BuildPlatform platform, string config) const { + import dub.compilers.utils : getTargetFileName; + PackageDescription ret; ret.configuration = config; ret.path = m_path.toNativeString(); ret.name = this.name; - ret.version_ = this.ver; + ret.version_ = this.version_; ret.description = m_info.description; ret.homepage = m_info.homepage; ret.authors = m_info.authors.dup; @@ -429,14 +660,14 @@ foreach (f; sourceFileTypes.byKey.array.sort()) { SourceFileDescription sf; sf.path = f; - sf.type = sourceFileTypes[f]; + sf.role = sourceFileTypes[f]; ret.files ~= sf; } return ret; } // ditto - deprecated void describe(ref Json dst, BuildPlatform platform, string config) + deprecated("Will be removed for version 1.0.0.") void describe(ref Json dst, BuildPlatform platform, string config) { auto res = describe(platform, config); foreach (string key, value; res.serializeToJson()) @@ -511,90 +742,12 @@ private void simpleLint() const { if (m_parentPackage) { if (m_parentPackage.path != path) { - if (info.license.length && info.license != m_parentPackage.info.license) + if (this.recipe.license.length && this.recipe.license != m_parentPackage.recipe.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) @@ -681,24 +834,3 @@ return null; } - -bool isRecursiveInvocation(string pack) -{ - import std.process : environment; - - return environment - .get("DUB_PACKAGES_USED", "") - .splitter(",") - .canFind(pack); -} - -void storeRecursiveInvokations(string[string] env, string[] packs) -{ - import std.process : environment; - - env["DUB_PACKAGES_USED"] = environment - .get("DUB_PACKAGES_USED", "") - .splitter(",") - .chain(packs) - .join(","); -} diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index aa5f921..f4dad84 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -1,7 +1,7 @@ /** Management of packages on the local computer. - Copyright: © 2012-2013 rejectedsoftware e.K. + Copyright: © 2012-2016 rejectedsoftware e.K. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig, Matthias Dondorff */ @@ -44,14 +44,19 @@ if (refresh_packages) refresh(true); } + /** Gets/sets the list of paths to search for local packages. + */ @property void searchPath(Path[] paths) { if (paths == m_searchPath) return; m_searchPath = paths.dup; refresh(false); } + /// ditto @property const(Path)[] searchPath() const { return m_searchPath; } + /** Disables searching DUB's predefined search paths. + */ @property void disableDefaultSearchPaths(bool val) { if (val == m_disableDefaultSearchPaths) return; @@ -59,6 +64,8 @@ refresh(true); } + /** Returns the effective list of search paths, including default ones. + */ @property const(Path)[] completeSearchPath() const { auto ret = appender!(Path[])(); @@ -107,7 +114,7 @@ } foreach (p; getPackageIterator(name)) - if (p.ver == ver) + if (p.version_ == ver) return p; return null; @@ -123,7 +130,7 @@ Package getPackage(string name, Version ver, Path path) { auto ret = getPackage(name, path); - if (!ret || ret.ver != ver) return null; + if (!ret || ret.version_ != ver) return null; return ret; } @@ -152,13 +159,26 @@ return null; } - Package getOrLoadPackage(Path path, PathAndFormat infoFile = PathAndFormat(), bool allow_sub_packages = false) + /** For a given package path, returns the corresponding package. + + If the package is already loaded, a reference is returned. Otherwise + the package gets loaded and cached for the next call to this function. + + Params: + path = Path to the root directory of the package + recipe_path = Optional path to the recipe file of the package + allow_sub_packages = Also return a sub package if it resides in the given folder + + Returns: The packages loaded from the given path + Throws: Throws an exception if no package can be loaded + */ + Package getOrLoadPackage(Path path, Path recipe_path = Path.init, bool allow_sub_packages = false) { path.endsWithSlash = true; foreach (p; getPackageIterator()) if (p.path == path && (!p.parentPackage || (allow_sub_packages && p.parentPackage.path != p.path))) return p; - auto pack = new Package(path, infoFile); + auto pack = Package.load(path, recipe_path); addPackages(m_temporaryPackages, pack); return pack; } @@ -170,11 +190,11 @@ { Package ret; foreach (p; getPackageIterator(name)) - if (version_spec.matches(p.ver) && (!ret || p.ver > ret.ver)) + if (version_spec.matches(p.version_) && (!ret || p.version_ > ret.version_)) ret = p; if (enable_overrides && ret) { - if (auto ovr = getPackage(name, ret.ver)) + if (auto ovr = getPackage(name, ret.version_)) return ovr; } return ret; @@ -186,6 +206,19 @@ return getBestPackage(name, Dependency(version_spec)); } + /** Gets the a specific sub package. + + In contrast to `Package.getSubPackage`, this function supports path + based sub packages. + + Params: + base_package = The package from which to get a sub package + sub_name = Name of the sub package (not prefixed with the base + package name) + silent_fail = If set to true, the function will return `null` if no + package is found. Otherwise will throw an exception. + + */ Package getSubPackage(Package base_package, string sub_name, bool silent_fail) { foreach (p; getPackageIterator(base_package.name~":"~sub_name)) @@ -221,6 +254,10 @@ return false; } + /** Enables iteration over all known local packages. + + Returns: A delegate suitable for use with `foreach` is returned. + */ int delegate(int delegate(ref Package)) getPackageIterator() { int iterator(int delegate(ref Package) del) @@ -243,6 +280,10 @@ return &iterator; } + /** Enables iteration over all known local packages with a certain name. + + Returns: A delegate suitable for use with `foreach` is returned. + */ int delegate(int delegate(ref Package)) getPackageIterator(string name) { int iterator(int delegate(ref Package) del) @@ -363,11 +404,11 @@ logDiagnostic("%s file(s) copied.", to!string(countFiles)); // overwrite dub.json (this one includes a version field) - auto pack = new Package(destination, PathAndFormat(), null, package_info["version"].get!string); + auto pack = Package.load(destination, Path.init, null, package_info["version"].get!string); - if (pack.packageInfoFilename.head != defaultPackageFilename) + if (pack.recipePath.head != defaultPackageFilename) // Storeinfo saved a default file, this could be different to the file from the zip. - removeFile(pack.packageInfoFilename); + removeFile(pack.recipePath); pack.storeInfo(); addPackages(m_packages, pack); return pack; @@ -376,7 +417,7 @@ /// Removes the given the package. void remove(in Package pack, bool force_remove) { - logDebug("Remove %s, version %s, path '%s'", pack.name, pack.vers, pack.path); + logDebug("Remove %s, version %s, path '%s'", pack.name, pack.version_, pack.path); enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path."); // remove package from repositories' list @@ -407,17 +448,17 @@ Package addLocalPackage(Path path, string verName, LocalPackageType type) { path.endsWithSlash = true; - auto pack = new Package(path); + auto pack = Package.load(path); enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString()); if (verName.length) - pack.ver = Version(verName); + pack.version_ = Version(verName); // don't double-add packages Package[]* packs = &m_repositories[type].localPackages; foreach (p; *packs) { if (p.path == path) { - enforce(p.ver == pack.ver, "Adding the same local package twice with differing versions is not allowed."); - logInfo("Package is already registered: %s (version: %s)", p.name, p.ver); + enforce(p.version_ == pack.version_, "Adding the same local package twice with differing versions is not allowed."); + logInfo("Package is already registered: %s (version: %s)", p.name, p.version_); return p; } } @@ -426,7 +467,7 @@ writeLocalPackageList(type); - logInfo("Registered package: %s (version: %s)", pack.name, pack.ver); + logInfo("Registered package: %s (version: %s)", pack.name, pack.version_); return pack; } @@ -443,7 +484,7 @@ string[Version] removed; foreach_reverse( i; to_remove ) { - removed[(*packs)[i].ver] = (*packs)[i].name; + removed[(*packs)[i].version_] = (*packs)[i].name; *packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $]; } @@ -504,7 +545,7 @@ if (!pp) { auto infoFile = Package.findPackageFile(path); - if (!infoFile.empty) pp = new Package(path, infoFile); + if (!infoFile.empty) pp = Package.load(path, infoFile); else { logWarn("Locally registered package %s %s was not found. Please run \"dub remove-local %s\".", name, ver, path.toNativeString()); @@ -516,7 +557,7 @@ if (pp.name != name) logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, pp.name); - pp.ver = ver; + pp.version_ = ver; addPackages(packs, pp); } @@ -566,7 +607,7 @@ p = pp; break; } - if (!p) p = new Package(pack_path, packageFile); + if (!p) p = Package.load(pack_path, packageFile); addPackages(m_packages, p); } catch( Exception e ){ logError("Failed to load package in %s: %s", pack_path, e.msg); @@ -644,7 +685,7 @@ if (p.parentPackage) continue; // do not store sub packages auto entry = Json.emptyObject; entry["name"] = p.name; - entry["version"] = p.ver.toString(); + entry["version"] = p.version_.toString(); entry["path"] = p.path.toNativeString(); newlist ~= entry; } @@ -660,7 +701,7 @@ foreach (ovr; m_repositories[type].overrides) { auto jovr = Json.emptyObject; jovr.name = ovr.package_; - jovr["version"] = ovr.version_.versionString; + jovr["version"] = ovr.version_.versionSpec; if (!ovr.targetPath.empty) jovr.targetPath = ovr.targetPath.toNativeString(); else jovr.targetVersion = ovr.targetVersion.toString(); newlist ~= jovr; @@ -691,7 +732,7 @@ logError("Package %s declared a sub-package, definition file is missing: %s", pack.name, path.toNativeString()); continue; } - sp = new Package(path, PathAndFormat(), pack); + sp = Package.load(path, Path.init, pack); } else sp = new Package(spr.recipe, pack.path, pack); // Add the subpackage. @@ -732,8 +773,8 @@ system } -enum LocalPackagesFilename = "local-packages.json"; -enum LocalOverridesFilename = "local-overrides.json"; +private enum LocalPackagesFilename = "local-packages.json"; +private enum LocalOverridesFilename = "local-overrides.json"; private struct Repository { diff --git a/source/dub/packagesupplier.d b/source/dub/packagesupplier.d index 30a2cee..a1cfe71 100644 --- a/source/dub/packagesupplier.d +++ b/source/dub/packagesupplier.d @@ -1,7 +1,7 @@ /** - A package supplier, able to get some packages to the local FS. + Contains (remote) package supplier interface and implementations. - Copyright: © 2012-2013 Matthias Dondorff + Copyright: © 2012-2013 Matthias Dondorff, 2012-2016 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Matthias Dondorff */ @@ -23,26 +23,74 @@ import std.string : format; import std.zip; -// TODO: drop the "best package" behavior and let retrievePackage/getPackageDescription take a Version instead of Dependency +// TODO: Could drop the "best package" behavior and let retrievePackage/ +// getPackageDescription take a Version instead of Dependency. But note +// this means that two requests to the registry are necessary to retrieve +// a package recipe instead of one (first get version list, then the +// package recipe) -/// Supplies packages, this is done by supplying the latest possible version -/// which is available. +/** + Base interface for remote package suppliers. + + Provides functionality necessary to query package versions, recipes and + contents. +*/ interface PackageSupplier { - /// Returns a hunman readable representation of the supplier + /// Represents a single package search result. + static struct SearchResult { string name, description, version_; } + + /// Returns a human-readable representation of the package supplier. @property string description(); + /** Retrieves a list of all available versions(/branches) of a package. + + Throws: Throws an exception if the package name is not known, or if + an error occurred while retrieving the version list. + */ Version[] getVersions(string package_id); - /// path: absolute path to store the package (usually in a zip format) - void retrievePackage(Path path, string packageId, Dependency dep, bool pre_release); + /** Downloads a package and stores it as a ZIP file. - /// returns the metadata for the package - Json getPackageDescription(string packageId, Dependency dep, bool pre_release); + Params: + path = Absolute path of the target ZIP file + package_id = Name of the package to retrieve + dep: Version constraint to match against + pre_release: If true, matches the latest pre-release version. + Otherwise prefers stable versions. + */ + void fetchPackage(Path path, string package_id, Dependency dep, bool pre_release); - static struct SearchResult { string name, description, version_; } + deprecated("Use fetchPackage instead. Will be removed for version 1.0.0.") + alias retrievePackage = fetchPackage; + + /** Retrieves only the recipe of a particular package. + + Params: + package_id = Name of the package of which to retrieve the recipe + dep: Version constraint to match against + pre_release: If true, matches the latest pre-release version. + Otherwise prefers stable versions. + */ + Json fetchPackageRecipe(string package_id, Dependency dep, bool pre_release); + + deprecated("Use fetchPackageRecipe instead. Will be removed for version 1.0.0.") + alias getPackageDescription = fetchPackageRecipe; + + /** Searches for packages matching the given search query term. + + Search queries are currently a simple list of words separated by + white space. Results will get ordered from best match to worst. + */ SearchResult[] searchPackages(string query); } + +/** + File system based package supplier. + + This package supplier searches a certain directory for files with names of + the form "[package name]-[version].zip". +*/ class FileSystemPackageSupplier : PackageSupplier { private { Path m_path; @@ -67,7 +115,7 @@ return ret; } - void retrievePackage(Path path, string packageId, Dependency dep, bool pre_release) + void fetchPackage(Path path, string packageId, Dependency dep, bool pre_release) { enforce(path.absolute); logInfo("Storing package '"~packageId~"', version requirements: %s", dep); @@ -76,13 +124,15 @@ copyFile(filename, path); } - Json getPackageDescription(string packageId, Dependency dep, bool pre_release) + Json fetchPackageRecipe(string packageId, Dependency dep, bool pre_release) { auto filename = bestPackageFile(packageId, dep, pre_release); return jsonFromZip(filename, "dub.json"); } - SearchResult[] searchPackages(string query) { + SearchResult[] searchPackages(string query) + { + // TODO! return null; } @@ -102,7 +152,12 @@ } -/// Client PackageSupplier using the registry available via registerVpmRegistry +/** + Online registry based package supplier. + + This package supplier connects to an online registry (e.g. + $(LINK https://code.dlang.org/)) to search for available packages. +*/ class RegistryPackageSupplier : PackageSupplier { private { URL m_registryUrl; @@ -131,7 +186,7 @@ return ret; } - void retrievePackage(Path path, string packageId, Dependency dep, bool pre_release) + void fetchPackage(Path path, string packageId, Dependency dep, bool pre_release) { import std.array : replace; Json best = getBestPackage(packageId, dep, pre_release); @@ -141,7 +196,7 @@ download(url, path); } - Json getPackageDescription(string packageId, Dependency dep, bool pre_release) + Json fetchPackageRecipe(string packageId, Dependency dep, bool pre_release) { return getBestPackage(packageId, dep, pre_release); } diff --git a/source/dub/platform.d b/source/dub/platform.d index acc9baf..8350a21 100644 --- a/source/dub/platform.d +++ b/source/dub/platform.d @@ -1,7 +1,14 @@ /** - Determines the strings to identify the current build platform. + Build platform identification and speficiation matching. - Copyright: © 2012 rejectedsoftware e.K. + This module is useful for determining the build platform for a certain + machine and compiler invocation. Example applications include classifying + CI slave machines. + + It also contains means to match build platforms against a platform + specification string as used in package reciptes. + + Copyright: © 2012-2016 rejectedsoftware e.K. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ @@ -9,6 +16,33 @@ import std.array; + +/** Determines the full build platform used for the current build. + + Note that the `BuildPlatform.compilerBinary` field will be left empty. + + See_Also: `determinePlatform`, `determineArchitecture`, `determineCompiler` +*/ +BuildPlatform determineBuildPlatform() +{ + BuildPlatform ret; + ret.platform = determinePlatform(); + ret.architecture = determineArchitecture(); + ret.compiler = determineCompiler(); + ret.frontendVersion = __VERSION__; + return ret; +} + + +/** Returns a list of platform identifiers that apply to the current + build. + + Example results are `["windows"]` or `["posix", "osx"]`. The identifiers + correspond to the compiler defined version constants built into the + language, except that they are converted to lower case. + + See_Also: `determineBuildPlatform` +*/ string[] determinePlatform() { auto ret = appender!(string[])(); @@ -34,6 +68,15 @@ return ret.data; } +/** Returns a list of architecture identifiers that apply to the current + build. + + Example results are `["x86_64"]` or `["arm", "arm_softfloat"]`. The + identifiers correspond to the compiler defined version constants built into + the language, except that they are converted to lower case. + + See_Also: `determineBuildPlatform` +*/ string[] determineArchitecture() { auto ret = appender!(string[])(); @@ -77,6 +120,13 @@ return ret.data; } +/** Determines the canonical compiler name used for the current build. + + The possible values currently are "dmd", "gdc", "ldc2" or "sdc". If an + unknown compiler is used, this function will return an empty string. + + See_Also: `determineBuildPlatform` +*/ string determineCompiler() { version(DigitalMars) return "dmd"; @@ -85,3 +135,85 @@ else version(SDC) return "sdc"; else return null; } + +/** Matches a platform specification string against a build platform. + + Specifications 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 specifications: `"-windows-x86-dmd"`, + `"-dmd"`, `"-arm"`, `"-arm-dmd"`, `"-windows-dmd"` + + Params: + platform = The build platform to match agains the platform specification + specification = The specification being matched. It must either be an + empty string or start with a dash. + + Returns: + `true` if the given specification matches the build platform, `false` + otherwise. Using an empty string as the platform specification will + always result in a match. +*/ +bool matchesSpecification(in BuildPlatform platform, const(char)[] specification) +{ + import std.string : format; + import std.algorithm : canFind, splitter; + import std.exception : enforce; + + if (specification.empty) return true; + if (platform == BuildPlatform.any) return true; + + auto splitted = specification.splitter('-'); + assert(!splitted.empty, "No valid platform specification! The leading hyphen is required!"); + splitted.popFront(); // Drop leading empty match. + enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification)); + + if (platform.platform.canFind(splitted.front)) { + splitted.popFront(); + if (splitted.empty) + return true; + } + if (platform.architecture.canFind(splitted.front)) { + splitted.popFront(); + if (splitted.empty) + return true; + } + if (platform.compiler == splitted.front) { + splitted.popFront(); + enforce(splitted.empty, "No valid specification! The compiler has to be the last element!"); + return true; + } + return false; +} + +/// +unittest { + auto platform=BuildPlatform(["posix", "linux"], ["x86_64"], "dmd"); + assert(platform.matchesSpecification("")); + assert(platform.matchesSpecification("-posix")); + assert(platform.matchesSpecification("-linux")); + assert(platform.matchesSpecification("-linux-dmd")); + assert(platform.matchesSpecification("-linux-x86_64-dmd")); + assert(platform.matchesSpecification("-x86_64")); + assert(!platform.matchesSpecification("-windows")); + assert(!platform.matchesSpecification("-ldc")); + assert(!platform.matchesSpecification("-windows-dmd")); +} + +/// Represents a platform a package can be build upon. +struct BuildPlatform { + /// Special constant used to denote matching any build platform. + enum any = BuildPlatform(null, null, null, null, -1); + + /// Platform identifiers, e.g. ["posix", "windows"] + string[] platform; + /// CPU architecture identifiers, e.g. ["x86", "x86_64"] + string[] architecture; + /// Canonical compiler name e.g. "dmd" + string compiler; + /// Compiler binary name e.g. "ldmd2" + string compilerBinary; + /// Compiled frontend version (e.g. `2067` for frontend versions 2.067.x) + int frontendVersion; +} diff --git a/source/dub/project.d b/source/dub/project.d index fb25c07..eb59e8e 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -1,7 +1,7 @@ /** Representing a full project, with a root Package and several dependencies. - Copyright: © 2012-2013 Matthias Dondorff + Copyright: © 2012-2013 Matthias Dondorff, 2012-2016 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Matthias Dondorff, Sönke Ludwig */ @@ -34,7 +34,14 @@ import std.zip; import std.encoding : sanitize; -/// Representing a full project, with a root Package and several dependencies. +/** + Represents a full project, a root package with its dependencies and package + selection. + + All dependencies must be available locally so that the package dependency + graph can be built. Use `Project.reinit` if necessary for reloading + dependencies after more packages are available. +*/ class Project { private { PackageManager m_packageManager; @@ -45,13 +52,21 @@ SelectedVersions m_selections; } + /** Loads a project. + + Params: + package_manager = Package manager instance to use for loading + dependencies + project_path = Path of the root package to load + pack = An existing `Package` instance to use as the root package + */ this(PackageManager package_manager, Path project_path) { Package pack; auto packageFile = Package.findPackageFile(project_path); if (packageFile.empty) { logWarn("There was no package description found for the application in '%s'.", project_path.toNativeString()); - pack = new Package(null, project_path); + pack = new Package(PackageRecipe.init, project_path); } else { pack = package_manager.getOrLoadPackage(project_path, packageFile); } @@ -59,6 +74,7 @@ this(package_manager, pack); } + /// ditto this(PackageManager package_manager, Package pack) { m_packageManager = package_manager; @@ -82,7 +98,7 @@ } /// Gathers information - @property string info() + deprecated("Will be removed for version 1.0.0.") @property string info() const { if(!m_rootPackage) return "-Unrecognized application in '"~m_rootPackage.path.toNativeString()~"' (probably no dub.json in this directory)"; @@ -95,17 +111,21 @@ } /// Gets all retrieved packages as a "packageId" = "version" associative array - @property string[string] cachedPackagesIDs() const { + deprecated("Will be removed for version 1.0.0.") @property string[string] cachedPackagesIDs() const { string[string] pkgs; foreach(p; m_dependencies) - pkgs[p.name] = p.vers; + pkgs[p.name] = p.version_.toString(); return pkgs; } - /// List of retrieved dependency Packages + /** List of all resolved dependencies. + + This includes all direct and indirect dependencies of all configurations + combined. Optional dependencies that were not chosen are not included. + */ @property const(Package[]) dependencies() const { return m_dependencies; } - /// Main package. + /// The root package of the project. @property inout(Package) rootPackage() inout { return m_rootPackage; } /// The versions to use for all dependencies. Call reinit() after changing these. @@ -141,13 +161,18 @@ auto cfg = configs.get(p.name, null); - foreach (dn; p.dependencies.byKey.array.sort()) { - auto dv = p.dependencies[dn]; - // filter out dependencies not in the current configuration set - if (!p.hasDependency(dn, cfg)) continue; - auto dependency = getDependency(dn, true); - assert(dependency || dv.optional, - format("Non-optional dependency %s of %s not found in dependency tree!?.", dn, p.name)); + PackageDependency[] deps; + if (!cfg.length) deps = p.getAllDependencies(); + else { + auto depmap = p.getDependencies(cfg); + deps = depmap.keys.map!(k => PackageDependency(k, depmap[k])).array; + } + deps.sort!((a, b) => a.name < b.name); + + foreach (d; deps) { + auto dependency = getDependency(d.name, true); + assert(dependency || d.spec.optional, + format("Non-optional dependency %s of %s not found in dependency tree!?.", d.name, p.name)); if(dependency) perform_rec(dependency); if( ret ) return; } @@ -164,30 +189,51 @@ return &iterator; } - inout(Package) getDependency(string name, bool isOptional) + /** Retrieves a particular dependency by name. + + Params: + name = (Qualified) package name of the dependency + is_optional = If set to true, will return `null` for unsatisfiable + dependencies instead of throwing an exception. + */ + inout(Package) getDependency(string name, bool is_optional) inout { foreach(dp; m_dependencies) if( dp.name == name ) return dp; - if(!isOptional) throw new Exception("Unknown dependency: "~name); + if (!is_optional) throw new Exception("Unknown dependency: "~name); else return null; } + /** Returns the name of the default build configuration for the specified + target platform. + + Params: + platform = The target build platform + allow_non_library_configs = If set to true, will use the first + possible configuration instead of the first "executable" + configuration. + */ string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) const { auto cfgs = getPackageConfigs(platform, null, allow_non_library_configs); return cfgs[m_rootPackage.name]; } + /** Performs basic validation of various aspects of the package. + + This will emit warnings to `stderr` if any discouraged names or + dependency patterns are found. + */ void validate() { // some basic package lint m_rootPackage.warnOnSpecialCompilerFlags(); string nameSuggestion() { string ret; - ret ~= `Please modify the "name" field in %s accordingly.`.format(m_rootPackage.packageInfoFilename.toNativeString()); - if (!m_rootPackage.info.buildSettings.targetName.length) { - if (m_rootPackage.packageInfoFilename.head.toString().endsWith(".sdl")) { + ret ~= `Please modify the "name" field in %s accordingly.`.format(m_rootPackage.recipePath.toNativeString()); + if (!m_rootPackage.recipe.buildSettings.targetName.length) { + if (m_rootPackage.recipePath.head.toString().endsWith(".sdl")) { ret ~= ` You can then add 'targetName "%s"' to keep the current executable name.`.format(m_rootPackage.name); } else { ret ~= ` You can then add '"targetName": "%s"' to keep the current executable name.`.format(m_rootPackage.name); @@ -197,44 +243,43 @@ } if (m_rootPackage.name != m_rootPackage.name.toLower()) { logWarn(`WARNING: DUB package names should always be lower case. %s`, nameSuggestion()); - } else if (!m_rootPackage.info.name.all!(ch => ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '-' || ch == '_')) { + } else if (!m_rootPackage.recipe.name.all!(ch => ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '-' || ch == '_')) { logWarn(`WARNING: DUB package names may only contain alphanumeric characters, ` ~ `as well as '-' and '_'. %s`, nameSuggestion()); } enforce(!m_rootPackage.name.canFind(' '), "Aborting due to the package name containing spaces."); - foreach (dn, ds; m_rootPackage.dependencies) - if (ds.isExactVersion && ds.version_.isBranch) { + foreach (d; m_rootPackage.getAllDependencies()) + if (d.spec.isExactVersion && d.spec.version_.isBranch) { logWarn("WARNING: A deprecated branch based version specification is used " ~ "for the dependency %s. Please use numbered versions instead. Also " ~ "note that you can still use the %s file to override a certain " ~ "dependency to use a branch instead.", - dn, SelectedVersions.defaultFile); + d.name, SelectedVersions.defaultFile); } - bool[string] visited; + bool[Package] visited; void validateDependenciesRec(Package pack) { - foreach (name, vspec_; pack.dependencies) { - if (name in visited) continue; - visited[name] = true; - - auto basename = getBasePackageName(name); + foreach (d; pack.getAllDependencies()) { + auto basename = getBasePackageName(d.name); if (m_selections.hasSelectedVersion(basename)) { auto selver = m_selections.getSelectedVersion(basename); - if (vspec_.merge(selver) == Dependency.invalid) { + if (d.spec.merge(selver) == Dependency.invalid) { logWarn("Selected package %s %s does not match the dependency specification %s in package %s. Need to \"dub upgrade\"?", - basename, selver, vspec_, pack.name); + basename, selver, d.spec, pack.name); } } auto deppack = getDependency(name, true); + if (deppack in visited) continue; + visited[deppack] = true; if (deppack) validateDependenciesRec(deppack); } } validateDependenciesRec(m_rootPackage); } - /// Rereads the applications state. + /// Reloads dependencies. void reinit() { m_dependencies = null; @@ -246,76 +291,77 @@ logDebug("%sCollecting dependencies for %s", indent, pack.name); indent ~= " "; - foreach (name, vspec_; pack.dependencies) { - Dependency vspec = vspec_; + foreach (dep; pack.getAllDependencies()) { + Dependency vspec = dep.spec; Package p; - auto basename = getBasePackageName(name); - if (name == m_rootPackage.basePackage.name) { - vspec = Dependency(m_rootPackage.ver); + auto basename = getBasePackageName(dep.name); + if (dep.name == m_rootPackage.basePackage.name) { + vspec = Dependency(m_rootPackage.version_); p = m_rootPackage.basePackage; } else if (basename == m_rootPackage.basePackage.name) { - vspec = Dependency(m_rootPackage.ver); - try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, getSubPackageName(name), false); + vspec = Dependency(m_rootPackage.version_); + try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, getSubPackageName(dep.name), false); catch (Exception e) { - logDiagnostic("%sError getting sub package %s: %s", indent, name, e.msg); + logDiagnostic("%sError getting sub package %s: %s", indent, dep.name, e.msg); continue; } } else if (m_selections.hasSelectedVersion(basename)) { vspec = m_selections.getSelectedVersion(basename); - if (vspec.path.empty) p = m_packageManager.getBestPackage(name, vspec); + if (vspec.path.empty) p = m_packageManager.getBestPackage(dep.name, vspec); else { auto path = vspec.path; if (!path.absolute) path = m_rootPackage.path ~ path; - p = m_packageManager.getOrLoadPackage(path, PathAndFormat.init, true); + p = m_packageManager.getOrLoadPackage(path, Path.init, true); } } else if (m_dependencies.canFind!(d => getBasePackageName(d.name) == basename)) { auto idx = m_dependencies.countUntil!(d => getBasePackageName(d.name) == basename); auto bp = m_dependencies[idx].basePackage; vspec = Dependency(bp.path); - p = m_packageManager.getSubPackage(bp, getSubPackageName(name), false); + p = m_packageManager.getSubPackage(bp, getSubPackageName(dep.name), false); } else { logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.", - indent, basename, name, pack.name); + indent, basename, dep.name, pack.name); } if (!p && !vspec.path.empty) { Path path = vspec.path; if (!path.absolute) path = pack.path ~ path; logDiagnostic("%sAdding local %s", indent, path); - p = m_packageManager.getOrLoadPackage(path, PathAndFormat.init, true); + p = m_packageManager.getOrLoadPackage(path, Path.init, true); if (p.parentPackage !is null) { - logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, name); + logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name); p = p.parentPackage; } - if (name.canFind(':')) p = m_packageManager.getSubPackage(p, getSubPackageName(name), false); - enforce(p.name == name, + if (dep.name.canFind(':')) p = m_packageManager.getSubPackage(p, getSubPackageName(dep.name), false); + enforce(p.name == dep.name, format("Path based dependency %s is referenced with a wrong name: %s vs. %s", - path.toNativeString(), name, p.name)); + path.toNativeString(), dep.name, p.name)); } if (!p) { - logDiagnostic("%sMissing dependency %s %s of %s", indent, name, vspec, pack.name); + logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name); continue; } if (!m_dependencies.canFind(p)) { - logDiagnostic("%sFound dependency %s %s", indent, name, vspec.toString()); + logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString()); m_dependencies ~= p; p.warnOnSpecialCompilerFlags(); collectDependenciesRec(p, depth+1); } m_dependees[p] ~= pack; - //enforce(p !is null, "Failed to resolve dependency "~name~" "~vspec.toString()); + //enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString()); } } collectDependenciesRec(m_rootPackage); } - /// Returns the applications name. + /// Returns the name of the root package. @property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; } + /// Returns the names of all configurations of the root package. @property string[] configurations() const { return m_rootPackage.configurations; } /// Returns a map with the configuration for all packages in the dependency tree. @@ -329,8 +375,8 @@ string[][string] parents; parents[m_rootPackage.name] = null; foreach (p; getTopologicalPackageList()) - foreach (d; p.dependencies.byKey) - parents[d] ~= p.name; + foreach (d; p.getAllDependencies()) + parents[d.name] ~= p.name; size_t createConfig(string pack, string config) { @@ -394,8 +440,8 @@ scope (exit) allconfigs_path.length--; // first, add all dependency configurations - foreach (dn; p.dependencies.byKey) { - auto dp = getDependency(dn, true); + foreach (d; p.getAllDependencies) { + auto dp = getDependency(d.name, true); if (!dp) continue; determineAllConfigs(dp); } @@ -403,15 +449,15 @@ // for each configuration, determine the configurations usable for the dependencies outer: foreach (c; p.getPlatformConfigurations(platform, p is m_rootPackage && allow_non_library)) { string[][string] depconfigs; - foreach (dn; p.dependencies.byKey) { - auto dp = getDependency(dn, true); + foreach (d; p.getAllDependencies()) { + auto dp = getDependency(d.name, true); if (!dp) continue; string[] cfgs; auto subconf = p.getSubConfiguration(c, dp, platform); if (!subconf.empty) cfgs = [subconf]; else cfgs = dp.getPlatformConfigurations(platform); - cfgs = cfgs.filter!(c => haveConfig(dn, c)).array; + cfgs = cfgs.filter!(c => haveConfig(d.name, c)).array; // if no valid configuration was found for a dependency, don't include the // current configuration @@ -419,14 +465,14 @@ logDebug("Skip %s %s (missing configuration for %s)", p.name, c, dp.name); continue outer; } - depconfigs[dn] = cfgs; + depconfigs[d.name] = cfgs; } // add this configuration to the graph size_t cidx = createConfig(p.name, c); - foreach (dn; p.dependencies.byKey) - foreach (sc; depconfigs.get(dn, null)) - createEdge(cidx, createConfig(dn, sc)); + foreach (d; p.getAllDependencies()) + foreach (sc; depconfigs.get(d.name, null)) + createEdge(cidx, createConfig(d.name, sc)); } } if (config.length) createConfig(m_rootPackage.name, config); @@ -491,9 +537,9 @@ } /** - * Fills dst with values from this project. + * Fills `dst` with values from this project. * - * dst gets initialized according to the given platform and config. + * `dst` gets initialized according to the given platform and config. * * Params: * dst = The BuildSettings struct to fill with data. @@ -504,6 +550,8 @@ */ void addBuildSettings(ref BuildSettings dst, in BuildPlatform platform, string config, in Package root_package = null, bool shallow = false) const { + import dub.internal.utils : stripDlangSpecialChars; + auto configs = getPackageConfigs(platform, config); foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) { @@ -545,6 +593,16 @@ } } + /** Fills `dst` with build settings specific to the given build type. + + Params: + dst = The `BuildSettings` instance to add the build settings to + platform = Target build platform + build_type = Name of the build type + for_root_package = Selects if the build settings are for the root + package or for one of the dependencies. Unittest flags will + only be added to the root package. + */ void addBuildTypeSettings(ref BuildSettings dst, in BuildPlatform platform, string build_type, bool for_root_package = true) { bool usedefflags = !(dst.requirements & BuildRequirement.noDefaultFlags); @@ -564,9 +622,9 @@ } /// Determines if the given dependency is already indirectly referenced by other dependencies of pack. - bool isRedundantDependency(in Package pack, in Package dependency) + deprecated("Will be removed for version 1.0.0.") bool isRedundantDependency(in Package pack, in Package dependency) const { - foreach (dep; pack.dependencies.byKey) { + foreach (dep; pack.recipe.dependencies.byKey) { auto dp = getDependency(dep, true); if (!dp) continue; if (dp is dependency) continue; @@ -576,56 +634,31 @@ return false; } - /*bool iterateDependencies(bool delegate(Package pack, string dep_name, Dependency dep_spec) del) - { - bool all_found = true; - - bool[string] visited; - void iterate(Package pack) - { - if (pack.name in visited) return; - visited[pack.name] = true; - - foreach (dn, ds; pack.dependencies) { - auto dep = del(pack, dn, ds); - if (dep) iterateDependencies(dep); - else all_found = false; - } - } - - return all_found; - }*/ - /// Outputs a build description of the project, including its dependencies. - ProjectDescription describe(BuildPlatform platform, string config, string build_type = null) + ProjectDescription describe(GeneratorSettings settings) { import dub.generators.targetdescription; // store basic build parameters ProjectDescription ret; ret.rootPackage = m_rootPackage.name; - ret.configuration = config; - ret.buildType = build_type; - ret.compiler = platform.compiler; - ret.architecture = platform.architecture; - ret.platform = platform.platform; + ret.configuration = settings.config; + ret.buildType = settings.buildType; + ret.compiler = settings.platform.compiler; + ret.architecture = settings.platform.architecture; + ret.platform = settings.platform.platform; // collect high level information about projects (useful for IDE display) - auto configs = getPackageConfigs(platform, config); - ret.packages ~= m_rootPackage.describe(platform, config); + auto configs = getPackageConfigs(settings.platform, settings.config); + ret.packages ~= m_rootPackage.describe(settings.platform, settings.config); foreach (dep; m_dependencies) - ret.packages ~= dep.describe(platform, configs[dep.name]); + ret.packages ~= dep.describe(settings.platform, configs[dep.name]); foreach (p; getTopologicalPackageList(false, null, configs)) ret.packages[ret.packages.countUntil!(pp => pp.name == p.name)].active = true; - if (build_type.length) { + if (settings.buildType.length) { // collect build target information (useful for build tools) - GeneratorSettings settings; - settings.platform = platform; - settings.compiler = getCompiler(platform.compilerBinary); - settings.config = config; - settings.buildType = build_type; auto gen = new TargetDescriptionGenerator(this); try { gen.generate(settings); @@ -640,12 +673,23 @@ return ret; } /// ditto - deprecated void describe(ref Json dst, BuildPlatform platform, string config) + deprecated("Will be removed for version 1.0.0.") void describe(ref Json dst, BuildPlatform platform, string config) { auto desc = describe(platform, config); foreach (string key, value; desc.serializeToJson()) dst[key] = value; } + /// ditto + deprecated("Use the overload taking a GeneratorSettings instance. Will be removed for version 1.0.0.") + ProjectDescription describe(BuildPlatform platform, string config, string build_type = null) + { + GeneratorSettings settings; + settings.platform = platform; + settings.compiler = getCompiler(platform.compilerBinary); + settings.config = config; + settings.buildType = build_type; + return describe(settings); + } private string[] listBuildSetting(string attributeName)(BuildPlatform platform, string config, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping) @@ -912,11 +956,12 @@ } /// Outputs requested data for the project, optionally including its dependencies. - string[] listBuildSettings(BuildPlatform platform, string config, string buildType, - string[] requestedData, Compiler formattingCompiler, bool nullDelim) + string[] listBuildSettings(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type) { - auto projectDescription = describe(platform, config, buildType); - auto configs = getPackageConfigs(platform, config); + import dub.compilers.utils : isLinkerFile; + + auto projectDescription = describe(settings); + auto configs = getPackageConfigs(settings.platform, settings.config); PackageDescription packageDescription; foreach (pack; projectDescription.packages) { if (pack.name == projectDescription.rootPackage) @@ -935,40 +980,62 @@ .array(); projectDescription.lookupTarget(projectDescription.rootPackage) = target; - // Genrate results - if (formattingCompiler) - { - // Format for a compiler - return [ - requestedData - .map!(dataName => listBuildSetting(platform, configs, projectDescription, dataName, formattingCompiler, nullDelim)) - .join().join(nullDelim? "\0" : " ") - ]; + Compiler compiler; + bool no_escape; + final switch (list_type) with (ListBuildSettingsFormat) { + case list: break; + case listNul: no_escape = true; break; + case commandLine: compiler = settings.compiler; break; + case commandLineNul: compiler = settings.compiler; no_escape = true; break; + } - else - { - // Format list-style - return requestedData - .map!(dataName => listBuildSetting(platform, configs, projectDescription, dataName, null, nullDelim)) - .joiner([""]) // Blank entry between each type of requestedData - .array(); + + auto result = requestedData + .map!(dataName => listBuildSetting(settings.platform, configs, projectDescription, dataName, compiler, no_escape)); + + final switch (list_type) with (ListBuildSettingsFormat) { + case list: return result.map!(l => l.join("\n")).array(); + case listNul: return result.map!(l => l.join("\0")).array; + case commandLine: return result.map!(l => l.join(" ")).array; + case commandLineNul: return result.map!(l => l.join("\0")).array; } } + deprecated("Use the overload taking a GeneratorSettings instance instead. Will be removed for version 1.0.0.") + string[] listBuildSettings(BuildPlatform platform, string config, string buildType, + string[] requestedData, Compiler formattingCompiler, bool nullDelim) + { + GeneratorSettings settings; + settings.platform = platform; + settings.config = config; + settings.buildType = buildType; + settings.compiler = formattingCompiler; + ListBuildSettingsFormat listtype; + with (ListBuildSettingsFormat) + listtype = formattingCompiler ? (nullDelim ? commandLineNul : commandLine) : (nullDelim ? listNul : list); + return listBuildSettings(settings, requestedData, listtype); + } + /// Outputs the import paths for the project, including its dependencies. - string[] listImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) + deprecated("Will be removed for version 1.0.0.") string[] listImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) { auto projectDescription = describe(platform, config, buildType); return listBuildSetting!"importPaths"(platform, config, projectDescription, null, nullDelim); } /// Outputs the string import paths for the project, including its dependencies. - string[] listStringImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) + deprecated("Will be removed for version 1.0.0.") string[] listStringImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) { auto projectDescription = describe(platform, config, buildType); return listBuildSetting!"stringImportPaths"(platform, config, projectDescription, null, nullDelim); } + /** Saves the currently selected dependency versions to disk. + + The selections will be written to a file named + `SelectedVersions.defaultFile` ("dub.selections.json") within the + directory of the root package. Any existing file will get overwritten. + */ void saveSelections() { assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections)."); @@ -980,6 +1047,11 @@ m_selections.save(path); } + /** Checks if the cached upgrade information is still considered up to date. + + The cache will be considered out of date after 24 hours after the last + online check. + */ bool isUpgradeCacheUpToDate() { try { @@ -994,6 +1066,11 @@ } } + /** Returns the currently cached upgrade information. + + The returned dictionary maps from dependency package name to the latest + available version that matches the dependency specifications. + */ Dependency[string] getUpgradeCache() { try { @@ -1007,6 +1084,8 @@ } } + /** Sets a new set of versions for the upgrade cache. + */ void setUpgradeCache(Dependency[string] versions) { logDebug("markUpToDate"); @@ -1041,8 +1120,18 @@ } } + +/// Determines the output format used for `Project.listBuildSettings`. +enum ListBuildSettingsFormat { + list, /// Newline separated list entries + listNul, /// NUL character separated list entries (unescaped) + commandLine, /// Formatted for compiler command line (one data list per line) + commandLineNul, /// NUL character separated list entries (unescaped, data lists separated by two NUL characters) +} + + /// Actions to be performed by the dub -struct Action { +deprecated("Will be removed for version 1.0.0.") struct Action { enum Type { fetch, remove, @@ -1060,7 +1149,7 @@ const Package pack; const Dependency[string] issuer; - static Action get(string pkg, PlacementLocation location, in Dependency dep, Dependency[string] context, Version old_version = Version.UNKNOWN) + static Action get(string pkg, PlacementLocation location, in Dependency dep, Dependency[string] context, Version old_version = Version.unknown) { return Action(Type.fetch, pkg, location, dep, context, old_version); } @@ -1080,7 +1169,7 @@ return Action(Type.failure, pkg, PlacementLocation.user, dep, context); } - private this(Type id, string pkg, PlacementLocation location, in Dependency d, Dependency[string] issue, Version existing_version = Version.UNKNOWN) + private this(Type id, string pkg, PlacementLocation location, in Dependency d, Dependency[string] issue, Version existing_version = Version.unknown) { this.type = id; this.packageId = pkg; @@ -1095,7 +1184,7 @@ pack = pkg; type = id; packageId = pkg.name; - vers = cast(immutable)Dependency(pkg.ver); + vers = cast(immutable)Dependency(pkg.version_); issuer = issue; } @@ -1107,7 +1196,7 @@ /// Indicates where a package has been or should be placed to. enum PlacementLocation { - /// Packages retrived with 'local' will be placed in the current folder + /// Packages retrieved with 'local' will be placed in the current folder /// using the package name as destination. local, /// Packages with 'userWide' will be placed in a folder accessible by @@ -1118,11 +1207,8 @@ system } -/// The default placement location of fetched packages. Can be changed by --local or --system. -auto defaultPlacementLocation = PlacementLocation.user; - -void processVars(ref BuildSettings dst, in Project project, in Package pack, BuildSettings settings, bool include_target_settings = false) - +void processVars(ref BuildSettings dst, in Project project, in Package pack, + BuildSettings settings, bool include_target_settings = false) { dst.addDFlags(processVars(project, pack, settings.dflags)); dst.addLFlags(processVars(project, pack, settings.lflags)); @@ -1218,21 +1304,16 @@ throw new Exception("Invalid variable: "~name); } -private bool isIdentChar(dchar ch) +deprecated("Will be removed for version 1.0.0.") string stripDlangSpecialChars(string s) { - return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '_'; + return dub.internal.utils.stripDlangSpecialChars(s); } -string stripDlangSpecialChars(string s) -{ - import std.array; - import std.uni; - auto ret = appender!string(); - foreach(ch; s) - ret.put(isIdentChar(ch) ? ch : '_'); - return ret.data; -} +/** Holds and stores a set of version selections for package dependencies. + This is the runtime representation of the information contained in + "dub.selections.json" within a package's directory. +*/ final class SelectedVersions { private struct Selected { Dependency dep; @@ -1245,16 +1326,25 @@ bool m_bare = true; } + /// Default file name to use for storing selections. enum defaultFile = "dub.selections.json"; + /// Constructs a new empty version selection. this() {} + /** Constructs a new version selection from JSON data. + + The structure of the JSON document must match the contents of the + "dub.selections.json" file. + */ this(Json data) { deserialize(data); m_dirty = false; } + /** Constructs a new version selections from an existing JSON file. + */ this(Path path) { auto json = jsonFromFile(path); @@ -1263,25 +1353,30 @@ m_bare = false; } + /// Returns a list of names for all packages that have a version selection. @property string[] selectedPackages() const { return m_selections.keys; } + /// Determines if any changes have been made after loading the selections from a file. @property bool dirty() const { return m_dirty; } - /// Determine if this set of selections is empty and has not been saved to disk. + /// Determine if this set of selections is still empty (but not `clear`ed). @property bool bare() const { return m_bare && !m_dirty; } + /// Removes all selections. void clear() { m_selections = null; m_dirty = true; } + /// Duplicates the set of selected versions from another instance. void set(SelectedVersions versions) { m_selections = versions.m_selections.dup; m_dirty = true; } + /// Selects a certain version for a specific package. void selectVersion(string package_id, Version version_) { if (auto ps = package_id in m_selections) { @@ -1292,6 +1387,7 @@ m_dirty = true; } + /// Selects a certain path for a specific package. void selectVersion(string package_id, Path path) { if (auto ps = package_id in m_selections) { @@ -1302,23 +1398,38 @@ m_dirty = true; } + /// Removes the selection for a particular package. void deselectVersion(string package_id) { m_selections.remove(package_id); m_dirty = true; } + /// Determines if a particular package has a selection set. bool hasSelectedVersion(string packageId) const { return (packageId in m_selections) !is null; } + /** Returns the selection for a particular package. + + Note that the returned `Dependency` can either have the + `Dependency.path` property set to a non-empty value, in which case this + is a path based selection, or its `Dependency.version_` property is + valid and it is a version selection. + */ Dependency getSelectedVersion(string packageId) const { enforce(hasSelectedVersion(packageId)); return m_selections[packageId].dep; } + /** Stores the selections to disk. + + The target file will be written in JSON format. Usually, `defaultFile` + should be used as the file name and the directory should be the root + directory of the project's root package. + */ void save(Path path) { Json json = serialize(); diff --git a/source/dub/recipe/io.d b/source/dub/recipe/io.d index ef5a967..9704931 100644 --- a/source/dub/recipe/io.d +++ b/source/dub/recipe/io.d @@ -1,3 +1,10 @@ +/** + Package recipe reading/writing facilities. + + Copyright: © 2015-2016, Sönke Ludwig + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ module dub.recipe.io; import dub.recipe.packagerecipe; @@ -5,13 +12,22 @@ /** Reads a package recipe from a file. + + The file format (JSON/SDLang) will be determined from the file extension. + + Params: + filename = Path of the package recipe file + parent_name = Optional name of the parent package (if this is a sub package) + + Returns: Returns the package recipe contents + Throws: Throws an exception if an I/O or syntax error occurs */ PackageRecipe readPackageRecipe(string filename, string parent_name = null) { return readPackageRecipe(Path(filename), parent_name); } /// ditto -PackageRecipe readPackageRecipe(Path file, string parent_name = null) +PackageRecipe readPackageRecipe(Path filename, string parent_name = null) { import dub.internal.utils : stripUTF8Bom; import dub.internal.vibecompat.core.file : openFile, FileMode; @@ -19,15 +35,27 @@ string text; { - auto f = openFile(file.toNativeString(), FileMode.read); + auto f = openFile(filename.toNativeString(), FileMode.read); scope(exit) f.close(); text = stripUTF8Bom(cast(string)f.readAll()); } - return parsePackageRecipe(text, file.toNativeString(), parent_name); + return parsePackageRecipe(text, filename.toNativeString(), parent_name); } /** Parses an in-memory package recipe. + + The file format (JSON/SDLang) will be determined from the file extension. + + Params: + contents = The contents of the recipe file + filename = Name associated with the package recipe - this is only used + to determine the file format from the file extension + parent_name = Optional name of the parent package (if this is a sub + package) + + Returns: Returns the package recipe contents + Throws: Throws an exception if an I/O or syntax error occurs */ PackageRecipe parsePackageRecipe(string contents, string filename, string parent_name = null) { diff --git a/source/dub/recipe/json.d b/source/dub/recipe/json.d index 8bba635..5f1603b 100644 --- a/source/dub/recipe/json.d +++ b/source/dub/recipe/json.d @@ -236,7 +236,7 @@ } } -Json toJson(in ref BuildSettingsTemplate bs) +private Json toJson(in ref BuildSettingsTemplate bs) { auto ret = Json.emptyObject; if( bs.dependencies !is null ){ diff --git a/source/dub/recipe/packagerecipe.d b/source/dub/recipe/packagerecipe.d index 85f48d5..10c9734 100644 --- a/source/dub/recipe/packagerecipe.d +++ b/source/dub/recipe/packagerecipe.d @@ -8,6 +8,7 @@ module dub.recipe.packagerecipe; import dub.compilers.compiler; +import dub.compilers.utils : warnOnSpecialCompilerFlags; import dub.dependency; import dub.internal.vibecompat.core.file; @@ -86,6 +87,7 @@ SubPackage[] subPackages; + deprecated("Use Package.dependencies or the dependencies of the individual BuildSettingsTemplates instead. Will be removed for version 1.0.0.") @property const(Dependency)[string] dependencies() const { Dependency[string] ret; diff --git a/source/dub/recipe/sdl.d b/source/dub/recipe/sdl.d index c29e374..0b4f05b 100644 --- a/source/dub/recipe/sdl.d +++ b/source/dub/recipe/sdl.d @@ -168,7 +168,7 @@ auto pkg = expandPackageName(t.values[0].get!string, package_name, t); enforceSDL(pkg !in bs.dependencies, "The dependency '"~pkg~"' is specified more than once.", t); - Dependency dep = Dependency.ANY; + Dependency dep = Dependency.any; auto attrs = t.attributes; auto pv = "version" in attrs; @@ -231,7 +231,7 @@ foreach (pack, d; bs.dependencies) { Attribute[] attribs; if (d.path.length) attribs ~= new Attribute(null, "path", Value(d.path.toString())); - else attribs ~= new Attribute(null, "version", Value(d.versionString)); + else attribs ~= new Attribute(null, "version", Value(d.versionSpec)); if (d.optional) attribs ~= new Attribute(null, "optional", Value(true)); ret ~= new Tag(null, "dependency", [Value(pack)], attribs); } @@ -423,8 +423,8 @@ assert(rec.subPackages[0].recipe.name == "subpackage1"); assert(rec.subPackages[1].path == ""); assert(rec.subPackages[1].recipe.name == "subpackage2"); - assert(rec.subPackages[1].recipe.dependencies.length == 1); - assert("projectname:subpackage1" in rec.subPackages[1].recipe.dependencies); + assert(rec.subPackages[1].recipe.buildSettings.dependencies.length == 1); + assert("projectname:subpackage1" in rec.subPackages[1].recipe.buildSettings.dependencies); assert(rec.subPackages[2].path == "pathsp3"); assert(rec.configurations.length == 2); assert(rec.configurations[0].name == "config1"); @@ -441,7 +441,7 @@ assert(rec.buildSettings.dependencies.length == 2); assert(rec.buildSettings.dependencies["projectname:subpackage1"].optional == false); assert(rec.buildSettings.dependencies["projectname:subpackage1"].path == Path(".")); - assert(rec.buildSettings.dependencies["somedep"].versionString == "1.0.0"); + assert(rec.buildSettings.dependencies["somedep"].versionSpec == "1.0.0"); assert(rec.buildSettings.dependencies["somedep"].optional == true); assert(rec.buildSettings.dependencies["somedep"].path.empty); assert(rec.buildSettings.systemDependencies == "system dependencies"); diff --git a/source/dub/semver.d b/source/dub/semver.d index 48924ec..448ac81 100644 --- a/source/dub/semver.d +++ b/source/dub/semver.d @@ -1,7 +1,15 @@ /** - Implementes version validation and comparison according to the semantic versioning specification. + Implementes version validation and comparison according to the semantic + versioning specification. - Copyright: © 2013 rejectedsoftware e.K. + The general format of a semantiv version is: a.b.c[-x.y...][+x.y...] + a/b/c must be integer numbers with no leading zeros, and x/y/... must be + either numbers or identifiers containing only ASCII alphabetic characters + or hyphens. Identifiers may not start with a digit. + + See_Also: http://semver.org/ + + Copyright: © 2013-2016 rejectedsoftware e.K. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ @@ -12,11 +20,6 @@ import std.algorithm : max; import std.conv; -/* - General format of SemVer: a.b.c[-x.y...][+x.y...] - a/b/c must be integer numbers with no leading zeros - x/y/... must be either numbers or identifiers containing only ASCII alphabetic characters or hyphens -*/ /** Validates a version string according to the SemVer specification. @@ -64,6 +67,7 @@ return true; } +/// unittest { assert(isValidVersion("1.9.0")); assert(isValidVersion("0.10.0")); @@ -93,6 +97,10 @@ assert(!isValidVersion("1.0-1.0")); } + +/** + Determines if a given valid SemVer version has a pre-release suffix. +*/ bool isPreReleaseVersion(string ver) in { assert(isValidVersion(ver)); } body { @@ -106,11 +114,25 @@ return isValidNumber(ver[0 .. di]); } +/// +unittest { + assert(isPreReleaseVersion("1.0.0-alpha")); + assert(isPreReleaseVersion("1.0.0-alpha+b1")); + assert(isPreReleaseVersion("0.9.0-beta.1")); + assert(!isPreReleaseVersion("0.9.0")); + assert(!isPreReleaseVersion("0.9.0+b1")); +} + /** Compares the precedence of two SemVer version strings. - The version strings must be validated using isValidVersion() before being - passed to this function. + The version strings must be validated using `isValidVersion` before being + passed to this function. Note that the build meta data suffix (if any) is + being ignored when comparing version numbers. + + Returns: + Returns a negative number if `a` is a lower version than `b`, `0` if they are + equal, and a positive number otherwise. */ int compareVersions(string a, string b) { @@ -145,6 +167,15 @@ return bempty - aempty; } +/// +unittest { + assert(compareVersions("1.0.0", "1.0.0") == 0); + assert(compareVersions("1.0.0+b1", "1.0.0+b2") == 0); + assert(compareVersions("1.0.0", "2.0.0") < 0); + assert(compareVersions("1.0.0-beta", "1.0.0") < 0); + assert(compareVersions("1.0.1", "1.0.0") > 0); +} + unittest { void assertLess(string a, string b) { assert(compareVersions(a, b) < 0, "Failed for "~a~" < "~b); @@ -177,19 +208,21 @@ /** - Given version string, increments the next to last version number. - Prerelease and build metadata information is ignored. - @param ver Does not need to be a valid semver version. - @return Valid semver version + Increments a given (partial) version number to the next higher version. + + Prerelease and build metadata information is ignored. The given version + can skip the minor and patch digits. If no digits are skipped, the next + minor version will be selected. If the patch or minor versions are skipped, + the next major version will be selected. + + This function corresponds to the semantivs of the "~>" comparison operator's + upper bound. The semantics of this are the same as for the "approximate" version specifier from rubygems. (https://github.com/rubygems/rubygems/tree/81d806d818baeb5dcb6398ca631d772a003d078e/lib/rubygems/version.rb) - Examples: - 1.5 -> 2.0 - 1.5.67 -> 1.6.0 - 1.5.67-a -> 1.6.0 + See_Also: `expandVersion` */ string bumpVersion(string ver) { // Cut off metadata and prerelease information. @@ -205,7 +238,7 @@ while (splitted.length < 3) splitted ~= "0"; return splitted.join("."); } - +/// unittest { assert("1.0.0" == bumpVersion("0")); assert("1.0.0" == bumpVersion("0.0")); @@ -217,8 +250,12 @@ } /** - Takes a abbreviated version and expands it to a valid SemVer version. - E.g. "1.0" -> "1.0.0" + Takes a partial version and expands it to a valid SemVer version. + + This function corresponds to the semantivs of the "~>" comparison operator's + lower bound. + + See_Also: `bumpVersion` */ string expandVersion(string ver) { auto mi = ver.indexOfAny("+-"); @@ -232,7 +269,7 @@ while (splitted.length < 3) splitted ~= "0"; return splitted.join(".") ~ sub; } - +/// unittest { assert("1.0.0" == expandVersion("1")); assert("1.0.0" == expandVersion("1.0"));