diff --git a/build-files.txt b/build-files.txt index 3a9ec14..182d017 100644 --- a/build-files.txt +++ b/build-files.txt @@ -6,6 +6,7 @@ source/dub/compilers/gdc.d source/dub/compilers/ldc.d source/dub/compilers/utils.d +source/dub/data/platform.d source/dub/data/settings.d source/dub/dependency.d source/dub/dependencyresolver.d diff --git a/source/app.d b/source/app.d index 50daaf1..e27ebd7 100644 --- a/source/app.d +++ b/source/app.d @@ -9,6 +9,20 @@ import dub.commandline; +// Set output path and options for coverage reports +version (DigitalMars) version (D_Coverage) +{ + shared static this() + { + import core.runtime, std.file, std.path, std.stdio; + dmd_coverSetMerge(true); + auto path = buildPath(dirName(thisExePath()), "../cov"); + if (!path.exists) + mkdir(path); + dmd_coverDestPath(path); + } +} + /** * Workaround https://github.com/dlang/dub/issues/1812 * diff --git a/source/dub/commandline.d b/source/dub/commandline.d index d3367cb..587095e 100644 --- a/source/dub/commandline.d +++ b/source/dub/commandline.d @@ -1987,7 +1987,6 @@ override int execute(Dub dub, string[] free_args, string[] app_args) { import dub.recipe.io : readPackageRecipe, writePackageRecipe; - import dub.internal.vibecompat.core.file : existsFile; enforceUsage(free_args.length != 0, "Expected one or more arguments."); enforceUsage(app_args.length == 0, "Unexpected application arguments."); @@ -2177,8 +2176,7 @@ } catch(Exception e){ logInfo("Getting a release version failed: %s", e.msg); - logInfo("Retry with ~master..."); - dub.fetch(name, VersionRange.fromString("~master"), location, fetchOpts); + return 1; } } return 0; @@ -2677,7 +2675,7 @@ void copyFolderRec(NativePath folder, NativePath dstfolder) { ensureDirectory(dstfolder); - foreach (de; iterateDirectory(folder.toNativeString())) { + foreach (de; iterateDirectory(folder)) { if (de.name.startsWith(".")) continue; if (de.isDirectory) { copyFolderRec(folder ~ de.name, dstfolder ~ de.name); @@ -2714,7 +2712,7 @@ foreach (ref subp; recipe.subPackages) if (subp.path.length) { auto sub_path = base_path ~ NativePath(subp.path); - auto pack = prj.packageManager.getOrLoadPackage(sub_path); + auto pack = dub.packageManager.getOrLoadPackage(sub_path); fixPathDependencies(pack.recipe, sub_path); pack.storeInfo(sub_path); } else fixPathDependencies(subp.recipe, base_path); diff --git a/source/dub/compilers/compiler.d b/source/dub/compilers/compiler.d index e98de64..6a0014e 100644 --- a/source/dub/compilers/compiler.d +++ b/source/dub/compilers/compiler.d @@ -9,7 +9,7 @@ public import dub.compilers.buildsettings; deprecated("Please `import dub.dependency : Dependency` instead") public import dub.dependency : Dependency; -public import dub.platform : BuildPlatform, matchesSpecification; +public import dub.data.platform : BuildPlatform, matchesSpecification; import dub.internal.vibecompat.inet.path; import dub.internal.vibecompat.core.file; diff --git a/source/dub/compilers/utils.d b/source/dub/compilers/utils.d index aac768a..b3bb1e6 100644 --- a/source/dub/compilers/utils.d +++ b/source/dub/compilers/utils.d @@ -258,54 +258,6 @@ if (got_preamble) logWarn(""); } -/** - Turn a DMD-like version (e.g. 2.082.1) into a SemVer-like version (e.g. 2.82.1). - The function accepts a dependency operator prefix and some text postfix. - Prefix and postfix are returned verbatim. - Params: - ver = version string, possibly with a dependency operator prefix and some - test postfix. - Returns: - A Semver compliant string -*/ -package(dub) string dmdLikeVersionToSemverLike(string ver) -{ - import std.algorithm : countUntil, joiner, map, skipOver, splitter; - import std.array : join, split; - import std.ascii : isDigit; - import std.conv : text; - import std.exception : enforce; - import std.functional : not; - import std.range : padRight; - - const start = ver.countUntil!isDigit; - enforce(start != -1, "Invalid semver: "~ver); - const prefix = ver[0 .. start]; - ver = ver[start .. $]; - - const end = ver.countUntil!(c => !c.isDigit && c != '.'); - const postfix = end == -1 ? null : ver[end .. $]; - auto verStr = ver[0 .. $-postfix.length]; - - auto comps = verStr - .splitter(".") - .map!((a) { if (a.length > 1) a.skipOver("0"); return a;}) - .padRight("0", 3); - - return text(prefix, comps.joiner("."), postfix); -} - -/// -unittest { - assert(dmdLikeVersionToSemverLike("2.082.1") == "2.82.1"); - assert(dmdLikeVersionToSemverLike("2.082.0") == "2.82.0"); - assert(dmdLikeVersionToSemverLike("2.082") == "2.82.0"); - assert(dmdLikeVersionToSemverLike("~>2.082") == "~>2.82.0"); - assert(dmdLikeVersionToSemverLike("~>2.082-beta1") == "~>2.82.0-beta1"); - assert(dmdLikeVersionToSemverLike("2.4.6") == "2.4.6"); - assert(dmdLikeVersionToSemverLike("2.4.6-alpha12") == "2.4.6-alpha12"); -} - private enum probeBeginMark = "__dub_probe_begin__"; private enum probeEndMark = "__dub_probe_end__"; diff --git a/source/dub/data/platform.d b/source/dub/data/platform.d new file mode 100644 index 0000000..fb0870e --- /dev/null +++ b/source/dub/data/platform.d @@ -0,0 +1,128 @@ +/******************************************************************************* + + Represent a target platform + + Platform informations can be embedded in recipe, such that some settings + only target a certain platform (e.g. sourceFiles, lflags, etc...). + The struct in this module represent that information, structured. + +*******************************************************************************/ + +module dub.data.platform; + +/// 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; + /// Compiler version e.g. "1.11.0" + string compilerVersion; + /// Frontend version string from frontendVersion + /// e.g: 2067 => "2.067" + string frontendVersionString() const + { + import std.format : format; + + const maj = frontendVersion / 1000; + const min = frontendVersion % 1000; + return format("%d.%03d", maj, min); + } + /// + unittest + { + BuildPlatform bp; + bp.frontendVersion = 2067; + assert(bp.frontendVersionString == "2.067"); + } + + /// Checks to see if platform field contains windows + bool isWindows() const { + import std.algorithm : canFind; + return this.platform.canFind("windows"); + } + /// + unittest { + BuildPlatform bp; + bp.platform = ["windows"]; + assert(bp.isWindows); + bp.platform = ["posix"]; + assert(!bp.isWindows); + } +} + +/** 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 against 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.range : empty; + import std.string : chompPrefix, format; + import std.algorithm : canFind, splitter; + import std.exception : enforce; + + if (specification.empty) return true; + if (platform == BuildPlatform.any) return true; + + auto splitted = specification.chompPrefix("-").splitter('-'); + 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: " ~ specification); + 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")); + + // Before PR#2279, a leading '-' was required + assert(platform.matchesSpecification("-x86_64")); +} diff --git a/source/dub/dependency.d b/source/dub/dependency.d index 4e8be93..c0562fc 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -7,13 +7,9 @@ */ module dub.dependency; -import dub.internal.utils; -import dub.internal.vibecompat.core.file; import dub.internal.vibecompat.data.json; import dub.internal.vibecompat.inet.path; -import dub.package_; import dub.semver; -import dub.internal.logging; import dub.internal.dyaml.stdsumtype; @@ -327,9 +323,6 @@ Dependency dep; if( verspec.type == Json.Type.object ){ if( auto pp = "path" in verspec ) { - if (auto pv = "version" in verspec) - logDiagnostic("Ignoring version specification (%s) for path based dependency %s", pv.get!string, pp.get!string); - dep = Dependency(NativePath(verspec["path"].get!string)); } else if (auto repository = "repository" in verspec) { enforce("version" in verspec, "No version field specified!"); @@ -643,8 +636,6 @@ assert(Dependency("1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict)); assert(Dependency("~>1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict)); assert(Dependency("~>1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict)); - - logDebug("Dependency unittest success."); } unittest { diff --git a/source/dub/dub.d b/source/dub/dub.d index b64a386..19c97e1 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -33,20 +33,6 @@ import std.range : assumeSorted, empty; import std.string; -// Set output path and options for coverage reports -version (DigitalMars) version (D_Coverage) -{ - shared static this() - { - import core.runtime, std.file, std.path, std.stdio; - dmd_coverSetMerge(true); - auto path = buildPath(dirName(thisExePath()), "../cov"); - if (!path.exists) - mkdir(path); - dmd_coverDestPath(path); - } -} - static this() { import dub.compilers.dmd : DMDCompiler; @@ -74,6 +60,7 @@ See_Also: `defaultRegistryURLs` */ +deprecated("This function wasn't intended for public use - open an issue with Dub if you need it") PackageSupplier[] defaultPackageSuppliers() { logDiagnostic("Using dub registry url '%s'", defaultRegistryURLs[0]); @@ -84,6 +71,7 @@ Allowed protocols are dub+http(s):// and maven+http(s)://. */ +deprecated("This function wasn't intended for public use - open an issue with Dub if you need it") PackageSupplier getRegistryPackageSupplier(string url) { switch (url.startsWith("dub+", "mvn+", "file://")) @@ -99,7 +87,7 @@ } } -unittest +deprecated unittest { auto dubRegistryPackageSupplier = getRegistryPackageSupplier("dub+https://code.dlang.org"); assert(dubRegistryPackageSupplier.description.canFind(" https://code.dlang.org")); @@ -121,7 +109,7 @@ the command line interface. */ class Dub { - private { + protected { bool m_dryRun = false; PackageManager m_packageManager; PackageSupplier[] m_packageSuppliers; @@ -149,23 +137,24 @@ 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. + base = A list of package suppliers that are always present + (regardless of `skip`) and take precedence over the default + and configured `PackageSupplier`. This setting is currently + not used by the dub application but useful for libraries. + skip = 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) + this(string root_path = ".", PackageSupplier[] base = null, + SkipPackageSuppliers skip = SkipPackageSuppliers.none) { m_rootPath = NativePath(root_path); if (!m_rootPath.absolute) m_rootPath = getWorkingDirectory() ~ m_rootPath; init(); - m_packageSuppliers = this.computePkgSuppliers(additional_package_suppliers, - skip_registry, environment.get("DUB_REGISTRY", null)); - m_packageManager = new PackageManager(m_rootPath, m_dirs.userPackages, m_dirs.systemSettings, false); + const registry_var = environment.get("DUB_REGISTRY", null); + m_packageSuppliers = this.makePackageSuppliers(base, skip, registry_var); + m_packageManager = this.makePackageManager(); auto ccps = m_config.customCachePaths; if (ccps.length) @@ -208,11 +197,24 @@ this(pkg_root, pkg_root); } - private void init() + /** + * Get the `PackageManager` instance to use for this `Dub` instance + * + * The `PackageManager` is a central component of `Dub` as it allows to + * store and retrieve packages from the file system. In unittests, or more + * generally in a library setup, one may wish to provide a custom + * implementation, which can be done by overriding this method. + */ + protected PackageManager makePackageManager() const + { + return new PackageManager(m_rootPath, m_dirs.userPackages, m_dirs.systemSettings, false); + } + + protected void init() { this.m_dirs = SpecialDirs.make(); - this.loadConfig(); - this.determineDefaultCompiler(); + this.m_config = this.loadConfig(this.m_dirs); + this.m_defaultCompiler = this.determineDefaultCompiler(); } /** @@ -220,12 +222,23 @@ * * This can be overloaded in child classes to prevent library / unittest * dub from doing any kind of file IO. + * As this routine is used during initialization, the only assumption made + * in the base implementation is that `m_rootPath` has been populated. + * Child implementation should not rely on any other field in the base + * class having been populated. + * + * Params: + * dirs = An instance of `SpecialDirs` to read from and write to, + * as the configurations being read might set a `dubHome`. + * + * Returns: + * A populated `Settings` instance. */ - protected void loadConfig() + protected Settings loadConfig(ref SpecialDirs dirs) const { import dub.internal.configy.Read; - void readSettingsFile (NativePath path_) + static void readSettingsFile (NativePath path_, ref Settings current) { // TODO: Remove `StrictMode.Warn` after v1.40 release // The default is to error, but as the previous parser wasn't @@ -234,10 +247,11 @@ if (path.exists) { auto newConf = parseConfigFileSimple!Settings(path, StrictMode.Warn); if (!newConf.isNull()) - this.m_config = this.m_config.merge(newConf.get()); + current = current.merge(newConf.get()); } } + Settings result; const dubFolderPath = NativePath(thisExePath).parentPath; // override default userSettings + userPackages if a $DPATH or @@ -254,17 +268,17 @@ if (dubHome.length) { overrideDubHomeFromEnv = true; - m_dirs.userSettings = NativePath(dubHome); - m_dirs.userPackages = m_dirs.userSettings; - m_dirs.cache = m_dirs.userPackages ~ "cache"; + dirs.userSettings = NativePath(dubHome); + dirs.userPackages = dirs.userSettings; + dirs.cache = dirs.userPackages ~ "cache"; } } - readSettingsFile(m_dirs.systemSettings ~ "settings.json"); - readSettingsFile(dubFolderPath ~ "../etc/dub/settings.json"); + readSettingsFile(dirs.systemSettings ~ "settings.json", result); + readSettingsFile(dubFolderPath ~ "../etc/dub/settings.json", result); version (Posix) { if (dubFolderPath.absolute && dubFolderPath.startsWith(NativePath("usr"))) - readSettingsFile(NativePath("/etc/dub/settings.json")); + readSettingsFile(NativePath("/etc/dub/settings.json"), result); } // Override user + local package path from system / binary settings @@ -273,39 +287,26 @@ // // Don't use it if either $DPATH or $DUB_HOME are set, as environment // variables usually take precedence over configuration. - if (!overrideDubHomeFromEnv && this.m_config.dubHome.set) { - m_dirs.userSettings = NativePath(this.m_config.dubHome.expandEnvironmentVariables); + if (!overrideDubHomeFromEnv && result.dubHome.set) { + dirs.userSettings = NativePath(result.dubHome.expandEnvironmentVariables); } // load user config: - readSettingsFile(m_dirs.userSettings ~ "settings.json"); + readSettingsFile(dirs.userSettings ~ "settings.json", result); // load per-package config: if (!this.m_rootPath.empty) - readSettingsFile(this.m_rootPath ~ "dub.settings.json"); + readSettingsFile(this.m_rootPath ~ "dub.settings.json", result); // same as userSettings above, but taking into account the // config loaded from user settings and per-package config as well. - if (!overrideDubHomeFromEnv && this.m_config.dubHome.set) { - m_dirs.userPackages = NativePath(this.m_config.dubHome.expandEnvironmentVariables); - m_dirs.cache = m_dirs.userPackages ~ "cache"; + if (!overrideDubHomeFromEnv && result.dubHome.set) { + dirs.userPackages = NativePath(result.dubHome.expandEnvironmentVariables); + dirs.cache = dirs.userPackages ~ "cache"; } - } - unittest - { - scope (exit) environment.remove("DUB_REGISTRY"); - auto dub = new TestDub(".", null, SkipPackageSuppliers.configured); - assert(dub.m_packageSuppliers.length == 0); - environment["DUB_REGISTRY"] = "http://example.com/"; - dub = new TestDub(".", null, SkipPackageSuppliers.configured); - assert(dub.m_packageSuppliers.length == 1); - environment["DUB_REGISTRY"] = "http://example.com/;http://foo.com/"; - dub = new TestDub(".", null, SkipPackageSuppliers.configured); - assert(dub.m_packageSuppliers.length == 2); - dub = new TestDub(".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured); - assert(dub.m_packageSuppliers.length == 3); - } + return result; + } /** Get the list of package suppliers. @@ -313,45 +314,100 @@ 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. + skip = Can be used to skip using the configured package suppliers, + as well as the default suppliers. */ deprecated("This is an implementation detail. " ~ "Use `packageSuppliers` to get the computed list of package " ~ "suppliers once a `Dub` instance has been constructed.") - public PackageSupplier[] getPackageSuppliers(PackageSupplier[] additional_package_suppliers, SkipPackageSuppliers skip_registry) + public PackageSupplier[] getPackageSuppliers(PackageSupplier[] base, SkipPackageSuppliers skip) { - return this.computePkgSuppliers(additional_package_suppliers, skip_registry, environment.get("DUB_REGISTRY", null)); + return this.makePackageSuppliers(base, skip, environment.get("DUB_REGISTRY", null)); } /// Ditto - private PackageSupplier[] computePkgSuppliers( - PackageSupplier[] additional_package_suppliers, SkipPackageSuppliers skip_registry, - string dub_registry_var) + protected PackageSupplier[] makePackageSuppliers(PackageSupplier[] base, + SkipPackageSuppliers skip, string registry_var) { - PackageSupplier[] ps = additional_package_suppliers; + PackageSupplier[] ps = base; - if (skip_registry < SkipPackageSuppliers.all) + if (skip < SkipPackageSuppliers.all) { - ps ~= dub_registry_var + ps ~= registry_var .splitter(";") - .map!(url => getRegistryPackageSupplier(url)) + .map!(url => this.makePackageSupplier(url)) .array; } - if (skip_registry < SkipPackageSuppliers.configured) + if (skip < SkipPackageSuppliers.configured) { ps ~= m_config.registryUrls - .map!(url => getRegistryPackageSupplier(url)) + .map!(url => this.makePackageSupplier(url)) .array; } - if (skip_registry < SkipPackageSuppliers.standard) - ps ~= defaultPackageSuppliers(); + if (skip < SkipPackageSuppliers.standard) + ps ~= new FallbackPackageSupplier( + defaultRegistryURLs.map!(url => this.makePackageSupplier(url)) + .array); return ps; } + // Note: This test rely on the environment, which is not how unittests should work. + // This should be removed / refactored to keep coverage without affecting the env. + unittest + { + import dub.test.base : TestDub; + + scope (exit) environment.remove("DUB_REGISTRY"); + auto dub = new TestDub(".", null, SkipPackageSuppliers.configured); + assert(dub.packageSuppliers.length == 0); + environment["DUB_REGISTRY"] = "http://example.com/"; + dub = new TestDub(".", null, SkipPackageSuppliers.configured); + assert(dub.packageSuppliers.length == 1); + environment["DUB_REGISTRY"] = "http://example.com/;http://foo.com/"; + dub = new TestDub(".", null, SkipPackageSuppliers.configured); + assert(dub.packageSuppliers.length == 2); + dub = new TestDub(".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured); + assert(dub.packageSuppliers.length == 3); + + dub = new TestDub(); + assert(dub.makePackageSuppliers(null, SkipPackageSuppliers.none, null).length == 1); + assert(dub.makePackageSuppliers(null, SkipPackageSuppliers.configured, null).length == 0); + assert(dub.makePackageSuppliers(null, SkipPackageSuppliers.standard, null).length == 0); + assert(dub.makePackageSuppliers(null, SkipPackageSuppliers.standard, "http://example.com/") + .length == 1); + } + + /** + * Instantiate a `PackageSupplier` according to a given URL + * + * This is a factory function for `PackageSupplier`. Child classes may + * wish to override this to implement their own `PackageSupplier` logic, + * be it by extending this method's ability or replacing it. + * + * Params: + * url = The URL of the `PackageSupplier`. + * + * Returns: + * A new instance of a `PackageSupplier`. + */ + protected PackageSupplier makePackageSupplier(string url) const + { + switch (url.startsWith("dub+", "mvn+", "file://")) + { + case 1: + return new RegistryPackageSupplier(URL(url[4..$])); + case 2: + return new MavenRegistryPackageSupplier(URL(url[4..$])); + case 3: + return new FileSystemPackageSupplier(NativePath(url[7..$])); + default: + return new RegistryPackageSupplier(URL(url)); + } + } + /// ditto deprecated("This is an implementation detail. " ~ "Use `packageSuppliers` to get the computed list of package " ~ @@ -361,18 +417,6 @@ return getPackageSuppliers(additional_package_suppliers, m_config.skipRegistry); } - unittest - { - auto dub = new TestDub(); - - assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.none, null).length == 1); - assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.configured, null).length == 0); - assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.standard, null).length == 0); - - assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.standard, "http://example.com/") - .length == 1); - } - @property bool dryRun() const { return m_dryRun; } @property void dryRun(bool v) { m_dryRun = v; } @@ -394,8 +438,8 @@ @property string mainRecipePath() const { return m_mainRecipePath; } /// Whenever the switch --recipe= is supplied, this member will be populated. - @property string mainRecipePath(string recipePath) - { + @property string mainRecipePath(string recipePath) + { return m_mainRecipePath = recipePath; } @@ -461,7 +505,8 @@ /// Loads a specific package as the main project package (can be a sub package) void loadPackage(Package pack) { - m_project = new Project(m_packageManager, pack); + auto selections = Project.loadSelections(pack); + m_project = new Project(m_packageManager, pack, selections); } /** Loads a single file package. @@ -736,8 +781,6 @@ /** Executes D-Scanner tests on the current project. **/ void lintProject(string[] args) { - import std.path : buildPath, buildNormalizedPath; - if (m_dryRun) return; auto tool = "dscanner"; @@ -758,14 +801,14 @@ auto cfgs = m_project.getPackageConfigs(settings.platform, null, true); auto buildSettings = dependencyPackage.getBuildSettings(settings.platform, cfgs[dependencyPackage.name]); foreach (importPath; buildSettings.importPaths) { - settings.runArgs ~= ["-I", buildNormalizedPath(dependencyPackage.path.toNativeString(), importPath.idup)]; + settings.runArgs ~= ["-I", (dependencyPackage.path ~ importPath).toNativeString()]; } foreach (cimportPath; buildSettings.cImportPaths) { - settings.runArgs ~= ["-I", buildNormalizedPath(dependencyPackage.path.toNativeString(), cimportPath.idup)]; + settings.runArgs ~= ["-I", (dependencyPackage.path ~ cimportPath).toNativeString()]; } } - string configFilePath = buildPath(m_project.rootPackage.path.toNativeString(), "dscanner.ini"); + string configFilePath = (m_project.rootPackage.path ~ "dscanner.ini").toNativeString(); if (!args.canFind("--config") && exists(configFilePath)) { settings.runArgs ~= ["--config", configFilePath]; } @@ -809,9 +852,7 @@ deprecated("Use `clean(Package)` instead") void cleanPackage(NativePath path) { - auto ppack = Package.findPackageFile(path); - enforce(!ppack.empty, "No package found.", path.toNativeString()); - this.clean(Package.load(path, ppack)); + this.clean(Package.load(path)); } /// Ditto @@ -862,7 +903,7 @@ PackageSupplier supplier; foreach(ps; m_packageSuppliers){ try { - pinfo = ps.fetchPackageRecipe(basePackageName, Dependency(range), (options & FetchOptions.usePrerelease) != 0); + pinfo = ps.fetchPackageRecipe(basePackageName, range, (options & FetchOptions.usePrerelease) != 0); if (pinfo.type == Json.Type.null_) continue; supplier = ps; @@ -872,7 +913,8 @@ logDebug("Full error: %s", e.toString().sanitize()); } } - enforce(pinfo.type != Json.Type.undefined, "No package "~packageId~" was found matching the dependency " ~ range.toString()); + enforce(!pinfo.type.among(Json.Type.undefined, Json.Type.null_), + "No package " ~ packageId ~ " was found matching the dependency " ~ range.toString()); Version ver = Version(pinfo["version"].get!string); // always upgrade branch based versions - TODO: actually check if there is a new commit available @@ -909,7 +951,7 @@ import std.zip : ZipException; auto path = getTempFile(basePackageName, ".zip"); - supplier.fetchPackage(path, basePackageName, Dependency(range), (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail? + supplier.fetchPackage(path, basePackageName, range, (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail? scope(exit) removeFile(path); logDiagnostic("Placing to %s...", location.toString()); @@ -1398,29 +1440,40 @@ return settings; } - private void determineDefaultCompiler() + /** + * Determine the default compiler to use for this instance + * + * The default compiler will be used unless --compiler is specified. + * The environment variable `DC` will take precedence over anything, + * followed by the configuration. If nothing is found, the folder in + * which `dub` is installed will be searched, and if nothing is found, + * the $PATH will be searched. + * In the majority of cases, as we distribute `dub` alongside the compiler, + * it will be found once the directory in which dub reside is searched. + * + * Returns: The value to use for the default compiler. + */ + protected string determineDefaultCompiler() const { import std.file : thisExePath; import std.path : buildPath, dirName, expandTilde, isAbsolute, isDirSeparator; import std.range : front; // Env takes precedence + string result; if (auto envCompiler = environment.get("DC")) - m_defaultCompiler = envCompiler; + result = envCompiler; else - m_defaultCompiler = m_config.defaultCompiler.expandTilde; - if (m_defaultCompiler.length && m_defaultCompiler.isAbsolute) - return; + result = this.m_config.defaultCompiler.expandTilde; + if (result.length && result.isAbsolute) + return result; static immutable BinaryPrefix = `$DUB_BINARY_PATH`; - if(m_defaultCompiler.startsWith(BinaryPrefix)) - { - m_defaultCompiler = thisExePath().dirName() ~ m_defaultCompiler[BinaryPrefix.length .. $]; - return; - } + if (result.startsWith(BinaryPrefix)) + return thisExePath().dirName() ~ result[BinaryPrefix.length .. $]; - if (!find!isDirSeparator(m_defaultCompiler).empty) - throw new Exception("defaultCompiler specified in a DUB config file cannot use an unqualified relative path:\n\n" ~ m_defaultCompiler ~ + if (!find!isDirSeparator(result).empty) + throw new Exception("defaultCompiler specified in a DUB config file cannot use an unqualified relative path:\n\n" ~ result ~ "\n\nUse \"$DUB_BINARY_PATH/../path/you/want\" instead."); version (Windows) enum sep = ";", exe = ".exe"; @@ -1429,43 +1482,39 @@ auto compilers = ["dmd", "gdc", "gdmd", "ldc2", "ldmd2"]; // If a compiler name is specified, look for it next to dub. // Otherwise, look for any of the common compilers adjacent to dub. - if (m_defaultCompiler.length) + if (result.length) { - string compilerPath = buildPath(thisExePath().dirName(), m_defaultCompiler ~ exe); + string compilerPath = buildPath(thisExePath().dirName(), result ~ exe); if (existsFile(compilerPath)) - { - m_defaultCompiler = compilerPath; - return; - } + return compilerPath; } else { auto nextFound = compilers.find!(bin => existsFile(buildPath(thisExePath().dirName(), bin ~ exe))); if (!nextFound.empty) - { - m_defaultCompiler = buildPath(thisExePath().dirName(), nextFound.front ~ exe); - return; - } + return buildPath(thisExePath().dirName(), nextFound.front ~ exe); } // If nothing found next to dub, search the user's PATH, starting // with the compiler name from their DUB config file, if specified. auto paths = environment.get("PATH", "").splitter(sep).map!NativePath; - if (m_defaultCompiler.length && paths.canFind!(p => existsFile(p ~ (m_defaultCompiler~exe)))) - return; + if (result.length && paths.canFind!(p => existsFile(p ~ (result ~ exe)))) + return result; foreach (p; paths) { auto res = compilers.find!(bin => existsFile(p ~ (bin~exe))); - if (!res.empty) { - m_defaultCompiler = res.front; - return; - } + if (!res.empty) + return res.front; } - m_defaultCompiler = compilers[0]; + return compilers[0]; } + // This test also relies on the environment and the filesystem, + // as the `makePackageSuppliers` does, and should be refactored. unittest { + import dub.test.base : TestDub; import std.path: buildPath, absolutePath; + auto dub = new TestDub(".", null, SkipPackageSuppliers.configured); immutable olddc = environment.get("DC", null); immutable oldpath = environment.get("PATH", null); @@ -1494,23 +1543,19 @@ std.file.write(ldcbin, null); environment["DC"] = dmdbin.absolutePath(); - dub.determineDefaultCompiler(); - assert(dub.m_defaultCompiler == dmdbin.absolutePath()); + assert(dub.determineDefaultCompiler() == dmdbin.absolutePath()); environment["DC"] = "dmd"; environment["PATH"] = dmdpath ~ sep ~ ldcpath; - dub.determineDefaultCompiler(); - assert(dub.m_defaultCompiler == "dmd"); + assert(dub.determineDefaultCompiler() == "dmd"); environment["DC"] = "ldc2"; environment["PATH"] = dmdpath ~ sep ~ ldcpath; - dub.determineDefaultCompiler(); - assert(dub.m_defaultCompiler == "ldc2"); + assert(dub.determineDefaultCompiler() == "ldc2"); environment.remove("DC"); environment["PATH"] = ldcpath ~ sep ~ dmdpath; - dub.determineDefaultCompiler(); - assert(dub.m_defaultCompiler == "ldc2"); + assert(dub.determineDefaultCompiler() == "ldc2"); } private NativePath makeAbsolute(NativePath p) const { return p.absolute ? p : m_rootPath ~ p; } @@ -1804,7 +1849,7 @@ foreach (ps; m_dub.m_packageSuppliers) { if (rootpack == name) { try { - auto desc = ps.fetchPackageRecipe(name, dep, prerelease); + auto desc = ps.fetchPackageRecipe(name, VersionRange(vers, vers), prerelease); if (desc.type == Json.Type.null_) continue; auto ret = new Package(desc); @@ -1841,27 +1886,7 @@ } } -/** - * An instance of Dub that does not rely on the environment - * - * This instance of dub should not read any environment variables, - * nor should it do any file IO, to make it usable and reliable in unittests. - * Currently it reads environment variables but does not read the configuration. - */ -package final class TestDub : Dub -{ - /// Forward to base constructor - public this (string root = ".", PackageSupplier[] extras = null, - SkipPackageSuppliers skip = SkipPackageSuppliers.none) - { - super(root, extras, skip); - } - - /// Avoid loading user configuration - protected override void loadConfig() { /* No-op */ } -} - -private struct SpecialDirs { +package struct SpecialDirs { /// The path where to store temporary files and directory NativePath temp; /// The system-wide dub-specific folder diff --git a/source/dub/generators/build.d b/source/dub/generators/build.d index 7cfc7cf..ed0f5f1 100644 --- a/source/dub/generators/build.d +++ b/source/dub/generators/build.d @@ -61,14 +61,12 @@ class BuildGenerator : ProjectGenerator { private { - PackageManager m_packageMan; NativePath[] m_temporaryFiles; } this(Project project) { super(project); - m_packageMan = project.packageManager; } override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets) diff --git a/source/dub/generators/generator.d b/source/dub/generators/generator.d index b602142..88d719e 100644 --- a/source/dub/generators/generator.d +++ b/source/dub/generators/generator.d @@ -1022,7 +1022,7 @@ void copyFolderRec(NativePath folder, NativePath dstfolder) { ensureDirectory(dstfolder); - foreach (de; iterateDirectory(folder.toNativeString())) { + foreach (de; iterateDirectory(folder)) { if (de.isDirectory) { copyFolderRec(folder ~ de.name, dstfolder ~ de.name); } else { diff --git a/source/dub/generators/visuald.d b/source/dub/generators/visuald.d index 1524a18..6a42633 100644 --- a/source/dub/generators/visuald.d +++ b/source/dub/generators/visuald.d @@ -33,14 +33,12 @@ class VisualDGenerator : ProjectGenerator { private { - PackageManager m_pkgMgr; string[string] m_projectUuids; } this(Project project) { super(project); - m_pkgMgr = project.packageManager; } override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets) diff --git a/source/dub/internal/vibecompat/core/file.d b/source/dub/internal/vibecompat/core/file.d index 6de9241..64faf3b 100644 --- a/source/dub/internal/vibecompat/core/file.d +++ b/source/dub/internal/vibecompat/core/file.d @@ -253,36 +253,18 @@ /** Enumerates all files in the specified directory. */ -void listDirectory(NativePath path, scope bool delegate(FileInfo info) del) -{ - foreach( DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow) ) - if( !del(makeFileInfo(ent)) ) - break; -} -/// ditto -void listDirectory(string path, scope bool delegate(FileInfo info) del) -{ - listDirectory(NativePath(path), del); -} -/// ditto int delegate(scope int delegate(ref FileInfo)) iterateDirectory(NativePath path) { int iterator(scope int delegate(ref FileInfo) del){ - int ret = 0; - listDirectory(path, (fi){ - ret = del(fi); - return ret == 0; - }); - return ret; + foreach (DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow)) { + auto fi = makeFileInfo(ent); + if (auto res = del(fi)) + return res; + } + return 0; } return &iterator; } -/// ditto -int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path) -{ - return iterateDirectory(NativePath(path)); -} - /** Returns the current working directory. diff --git a/source/dub/package_.d b/source/dub/package_.d index 27f696d..7e2eef9 100644 --- a/source/dub/package_.d +++ b/source/dub/package_.d @@ -51,6 +51,7 @@ ]; /// Returns a list of all recognized package recipe file names in descending order of precedence. +deprecated("Open an issue if this is needed") @property string[] packageInfoFilenames() { return packageInfoFiles.map!(f => cast(string)f.filename).array; } /// Returns the default package recile file name. @@ -669,7 +670,6 @@ private void checkDubRequirements() { - import dub.dependency : Dependency; import dub.semver : isValidVersion; import dub.version_ : dubVersion; import std.exception : enforce; @@ -677,16 +677,10 @@ const dep = m_info.toolchainRequirements.dub; static assert(dubVersion.length); - static if (dubVersion[0] == 'v') { - enum dv = dubVersion[1 .. $]; - } - else { - enum dv = dubVersion; - } - static assert(isValidVersion(dv)); + immutable dv = Version(dubVersion[(dubVersion[0] == 'v') .. $]); enforce(dep.matches(dv), - "dub-" ~ dv ~ " does not comply with toolchainRequirements.dub " + "dub-" ~ dv.toString() ~ " does not comply with toolchainRequirements.dub " ~ "specification: " ~ m_info.toolchainRequirements.dub.toString() ~ "\nPlease consider upgrading your DUB installation"); } diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index 0300ba1..7529869 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -61,7 +61,7 @@ /// The PackageManager can retrieve present packages and get / remove /// packages. class PackageManager { - private { + protected { /** * The 'internal' location, for packages not attributable to a location. * @@ -188,7 +188,7 @@ * Returns: * A `Package` if one was found, `null` if none exists. */ - private Package lookup (string name, Version vers) { + protected Package lookup (string name, Version vers) { if (!this.m_initialized) this.refresh(); @@ -440,6 +440,7 @@ } /// Ditto + deprecated("Use the overload that accepts a `Version` or a `VersionRange`") Package getBestPackage(string name, string range) { return this.getBestPackage(name, VersionRange.fromString(range)); @@ -994,10 +995,6 @@ p.normalize(); enforce(!p.absolute, "Sub package paths must be sub paths of the parent package."); auto path = pack.path ~ p; - if (!existsFile(path)) { - logError("Package %s declared a sub-package, definition file is missing: %s", pack.name, path.toNativeString()); - continue; - } sp = Package.load(path, NativePath.init, pack); } else sp = new Package(spr.recipe, pack.path, pack); @@ -1124,7 +1121,7 @@ * Additionally, each location host a config file, * which is not managed by this module, but by dub itself. */ -private struct Location { +package struct Location { /// The absolute path to the root of the location NativePath packagePath; @@ -1367,7 +1364,7 @@ * Returns: * A `Package` if one was found, `null` if none exists. */ - private inout(Package) lookup(string name, Version ver, PackageManager mgr) inout { + inout(Package) lookup(string name, Version ver, PackageManager mgr) inout { foreach (pkg; this.localPackages) if (pkg.name == name && pkg.version_.matches(ver, VersionMatchMode.standard)) return pkg; @@ -1394,7 +1391,7 @@ * Returns: * A `Package` if one was found, `null` if none exists. */ - private Package load (string name, Version vers, PackageManager mgr) + Package load (string name, Version vers, PackageManager mgr) { if (auto pkg = this.lookup(name, vers, mgr)) return pkg; @@ -1426,7 +1423,7 @@ * but this function returns `$BASE/$NAME-$VERSION/` * `$BASE` is `this.packagePath`. */ - private NativePath getPackagePath (string name, string vers) + NativePath getPackagePath (string name, string vers) { NativePath result = this.packagePath ~ name ~ vers; result.endsWithSlash = true; diff --git a/source/dub/packagesuppliers/fallback.d b/source/dub/packagesuppliers/fallback.d index 1103e37..b9235c7 100644 --- a/source/dub/packagesuppliers/fallback.d +++ b/source/dub/packagesuppliers/fallback.d @@ -29,8 +29,8 @@ // Workaround https://issues.dlang.org/show_bug.cgi?id=2525 abstract override Version[] getVersions(string package_id); - abstract override void fetchPackage(NativePath path, string package_id, Dependency dep, bool pre_release); - abstract override Json fetchPackageRecipe(string package_id, Dependency dep, bool pre_release); + abstract override void fetchPackage(NativePath path, string package_id, in VersionRange dep, bool pre_release); + abstract override Json fetchPackageRecipe(string package_id, in VersionRange dep, bool pre_release); abstract override SearchResult[] searchPackages(string query); } diff --git a/source/dub/packagesuppliers/filesystem.d b/source/dub/packagesuppliers/filesystem.d index fcb9efd..4af88c4 100644 --- a/source/dub/packagesuppliers/filesystem.d +++ b/source/dub/packagesuppliers/filesystem.d @@ -39,7 +39,7 @@ return ret; } - void fetchPackage(NativePath path, string packageId, Dependency dep, bool pre_release) + void fetchPackage(NativePath path, string packageId, in VersionRange dep, bool pre_release) { import dub.internal.vibecompat.core.file : copyFile, existsFile; enforce(path.absolute); @@ -49,7 +49,7 @@ copyFile(filename, path); } - Json fetchPackageRecipe(string packageId, Dependency dep, bool pre_release) + Json fetchPackageRecipe(string packageId, in VersionRange dep, bool pre_release) { import std.array : split; import std.path : stripExtension; @@ -76,7 +76,7 @@ return null; } - private NativePath bestPackageFile(string packageId, Dependency dep, bool pre_release) + private NativePath bestPackageFile(string packageId, in VersionRange dep, bool pre_release) { import std.algorithm.iteration : filter; import std.array : array; diff --git a/source/dub/packagesuppliers/maven.d b/source/dub/packagesuppliers/maven.d index 7673b2e..ecb29b1 100644 --- a/source/dub/packagesuppliers/maven.d +++ b/source/dub/packagesuppliers/maven.d @@ -47,7 +47,7 @@ return ret; } - void fetchPackage(NativePath path, string packageId, Dependency dep, bool pre_release) + void fetchPackage(NativePath path, string packageId, in VersionRange dep, bool pre_release) { import std.format : format; auto md = getMetadata(packageId); @@ -71,7 +71,7 @@ throw new Exception("Failed to download package %s from %s".format(packageId, url)); } - Json fetchPackageRecipe(string packageId, Dependency dep, bool pre_release) + Json fetchPackageRecipe(string packageId, in VersionRange dep, bool pre_release) { auto md = getMetadata(packageId); return getBestPackage(md, packageId, dep, pre_release); @@ -125,7 +125,7 @@ auto md = getMetadata(query); if (md.type == Json.Type.null_) return null; - auto json = getBestPackage(md, query, Dependency.any, true); + auto json = getBestPackage(md, query, VersionRange.Any, true); return [SearchResult(json["name"].opt!string, "", json["version"].opt!string)]; } } diff --git a/source/dub/packagesuppliers/packagesupplier.d b/source/dub/packagesuppliers/packagesupplier.d index c0a75d5..71f8b8d 100644 --- a/source/dub/packagesuppliers/packagesupplier.d +++ b/source/dub/packagesuppliers/packagesupplier.d @@ -1,6 +1,7 @@ module dub.packagesuppliers.packagesupplier; -public import dub.dependency : Dependency, Version; +public import dub.dependency : Dependency, Version, VersionRange; +import dub.dependency : visit; public import dub.internal.vibecompat.core.file : NativePath; public import dub.internal.vibecompat.data.json : Json; @@ -33,7 +34,19 @@ pre_release = If true, matches the latest pre-release version. Otherwise prefers stable versions. */ - void fetchPackage(NativePath path, string package_id, Dependency dep, bool pre_release); + void fetchPackage(NativePath path, string package_id, in VersionRange dep, bool pre_release); + + deprecated("Use the overload that accepts a `VersionRange` instead") + final void fetchPackage(NativePath path, string package_id, Dependency dep, bool pre_release) + { + return dep.visit!( + (const VersionRange rng) { + return this.fetchPackage(path, package_id, rng, pre_release); + }, (any) { + assert(0, "Trying to fetch a package with a non-version dependency: " ~ any.toString()); + }, + ); + } /** Retrieves only the recipe of a particular package. @@ -43,7 +56,19 @@ pre_release = If true, matches the latest pre-release version. Otherwise prefers stable versions. */ - Json fetchPackageRecipe(string package_id, Dependency dep, bool pre_release); + Json fetchPackageRecipe(string package_id, in VersionRange dep, bool pre_release); + + deprecated("Use the overload that accepts a `VersionRange` instead") + final Json fetchPackageRecipe(string package_id, Dependency dep, bool pre_release) + { + return dep.visit!( + (const VersionRange rng) { + return this.fetchPackageRecipe(package_id, rng, pre_release); + }, (any) { + return Json.init; + }, + ); + } /** Searches for packages matching the given search query term. @@ -59,7 +84,7 @@ // a package recipe instead of one (first get version list, then the // package recipe) -package Json getBestPackage(Json metadata, string packageId, Dependency dep, bool pre_release) +package Json getBestPackage(Json metadata, string packageId, in VersionRange dep, bool pre_release) { import std.exception : enforce; if (metadata.type == Json.Type.null_) diff --git a/source/dub/packagesuppliers/registry.d b/source/dub/packagesuppliers/registry.d index a5afa64..b509970 100644 --- a/source/dub/packagesuppliers/registry.d +++ b/source/dub/packagesuppliers/registry.d @@ -1,5 +1,6 @@ module dub.packagesuppliers.registry; +import dub.dependency; import dub.packagesuppliers.packagesupplier; package enum PackagesPath = "packages"; @@ -48,7 +49,7 @@ return ret; } - auto genPackageDownloadUrl(string packageId, Dependency dep, bool pre_release) + auto genPackageDownloadUrl(string packageId, in VersionRange dep, bool pre_release) { import std.array : replace; import std.format : format; @@ -64,7 +65,7 @@ return ret; } - void fetchPackage(NativePath path, string packageId, Dependency dep, bool pre_release) + void fetchPackage(NativePath path, string packageId, in VersionRange dep, bool pre_release) { import std.format : format; auto url = genPackageDownloadUrl(packageId, dep, pre_release); @@ -84,7 +85,7 @@ throw new Exception("Failed to download package %s from %s".format(packageId, url)); } - Json fetchPackageRecipe(string packageId, Dependency dep, bool pre_release) + Json fetchPackageRecipe(string packageId, in VersionRange dep, bool pre_release) { auto md = getMetadata(packageId); return getBestPackage(md, packageId, dep, pre_release); diff --git a/source/dub/platform.d b/source/dub/platform.d index 6ee31b2..833061a 100644 --- a/source/dub/platform.d +++ b/source/dub/platform.d @@ -15,6 +15,7 @@ module dub.platform; import std.array; +public import dub.data.platform; // archCheck, compilerCheck, and platformCheck are used below and in // generatePlatformProbeFile, so they've been extracted into these strings @@ -166,119 +167,3 @@ { mixin(compilerCheck); } - -/** 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 against 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 : chompPrefix, format; - import std.algorithm : canFind, splitter; - import std.exception : enforce; - - if (specification.empty) return true; - if (platform == BuildPlatform.any) return true; - - auto splitted = specification.chompPrefix("-").splitter('-'); - 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: " ~ specification); - 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")); - - // Before PR#2279, a leading '-' was required - assert(platform.matchesSpecification("-x86_64")); -} - -/// 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; - /// Compiler version e.g. "1.11.0" - string compilerVersion; - /// Frontend version string from frontendVersion - /// e.g: 2067 => "2.067" - string frontendVersionString() const - { - import std.format : format; - - const maj = frontendVersion / 1000; - const min = frontendVersion % 1000; - return format("%d.%03d", maj, min); - } - /// - unittest - { - BuildPlatform bp; - bp.frontendVersion = 2067; - assert(bp.frontendVersionString == "2.067"); - } - - /// Checks to see if platform field contains windows - bool isWindows() const { - import std.algorithm : canFind; - return this.platform.canFind("windows"); - } - /// - unittest { - BuildPlatform bp; - bp.platform = ["windows"]; - assert(bp.isWindows); - bp.platform = ["posix"]; - assert(!bp.isWindows); - } -} diff --git a/source/dub/project.d b/source/dub/project.d index 5abfd45..5826101 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -71,25 +71,56 @@ this(package_manager, pack); } - /// ditto + /// Ditto this(PackageManager package_manager, Package pack) { + auto selections = Project.loadSelections(pack); + this(package_manager, pack, selections); + } + + /// ditto + this(PackageManager package_manager, Package pack, SelectedVersions selections) + { m_packageManager = package_manager; m_rootPackage = pack; - - auto selverfile = (m_rootPackage.path ~ SelectedVersions.defaultFile).toNativeString(); - if (existsFile(selverfile)) { - // TODO: Remove `StrictMode.Warn` after v1.40 release - // The default is to error, but as the previous parser wasn't - // complaining, we should first warn the user. - auto selected = parseConfigFileSimple!Selected(selverfile, StrictMode.Warn); - m_selections = !selected.isNull() ? - new SelectedVersions(selected.get()) : new SelectedVersions(); - } else m_selections = new SelectedVersions; - + m_selections = selections; reinit(); } + /** + * Loads a project's `dub.selections.json` and returns it + * + * This function will load `dub.selections.json` from the path at which + * `pack` is located, and returned the resulting `SelectedVersions`. + * If no `dub.selections.json` is found, an empty `SelectedVersions` + * is returned. + * + * Params: + * pack = Package to load the selection file from. + * + * Returns: + * Always a non-null instance. + */ + static package SelectedVersions loadSelections(in Package pack) + { + auto selverfile = (pack.path ~ SelectedVersions.defaultFile).toNativeString(); + + // No file exists + if (!existsFile(selverfile)) + return new SelectedVersions(); + + // TODO: Remove `StrictMode.Warn` after v1.40 release + // The default is to error, but as the previous parser wasn't + // complaining, we should first warn the user. + auto selected = parseConfigFileSimple!Selected(selverfile, StrictMode.Warn); + + // Parsing error, it will be displayed to the user + if (selected.isNull()) + return new SelectedVersions(); + + return new SelectedVersions(selected.get()); + } + /** List of all resolved dependencies. This includes all direct and indirect dependencies of all configurations @@ -104,6 +135,7 @@ @property inout(SelectedVersions) selections() inout { return m_selections; } /// Package manager instance used by the project. + deprecated("Use `Dub.packageManager` instead") @property inout(PackageManager) packageManager() inout { return m_packageManager; } /** Determines if all dependencies necessary to build have been collected. @@ -460,7 +492,9 @@ m_missingDependencies = []; Package resolveSubPackage(Package p, string subname, bool silentFail) { - return subname.length ? m_packageManager.getSubPackage(p, subname, silentFail) : p; + if (!subname.length || p is null) + return p; + return m_packageManager.getSubPackage(p, subname, silentFail); } void collectDependenciesRec(Package pack, int depth = 0) @@ -525,6 +559,9 @@ if (!vspec.repository.empty) { p = m_packageManager.loadSCMPackage(basename, vspec.repository); resolveSubPackage(p, subname, false); + enforce(p !is null, + "Unable to fetch '%s@%s' using git - does the repository and version exists?".format( + dep.name, vspec.repository)); } else if (!vspec.path.empty && is_desired) { NativePath path = vspec.path; if (!path.absolute) path = pack.path ~ path; @@ -1703,8 +1740,8 @@ This is the runtime representation of the information contained in "dub.selections.json" within a package's directory. */ -final class SelectedVersions { - private { +public class SelectedVersions { + protected { enum FileVersion = 1; Selected m_selections; bool m_dirty = false; // has changes since last save diff --git a/source/dub/recipe/json.d b/source/dub/recipe/json.d index 2f1e3e6..505baff 100644 --- a/source/dub/recipe/json.d +++ b/source/dub/recipe/json.d @@ -332,11 +332,11 @@ private Json toJson(const scope ref ToolchainRequirements tr) { auto ret = Json.emptyObject; - if (tr.dub != Dependency.any) ret["dub"] = serializeToJson(tr.dub); - if (tr.frontend != Dependency.any) ret["frontend"] = serializeToJson(tr.frontend); - if (tr.dmd != Dependency.any) ret["dmd"] = serializeToJson(tr.dmd); - if (tr.ldc != Dependency.any) ret["ldc"] = serializeToJson(tr.ldc); - if (tr.gdc != Dependency.any) ret["gdc"] = serializeToJson(tr.gdc); + if (tr.dub != VersionRange.Any) ret["dub"] = serializeToJson(tr.dub); + if (tr.frontend != VersionRange.Any) ret["frontend"] = serializeToJson(tr.frontend); + if (tr.dmd != VersionRange.Any) ret["dmd"] = serializeToJson(tr.dmd); + if (tr.ldc != VersionRange.Any) ret["ldc"] = serializeToJson(tr.ldc); + if (tr.gdc != VersionRange.Any) ret["gdc"] = serializeToJson(tr.gdc); return ret; } diff --git a/source/dub/recipe/packagerecipe.d b/source/dub/recipe/packagerecipe.d index 30445d1..5573018 100644 --- a/source/dub/recipe/packagerecipe.d +++ b/source/dub/recipe/packagerecipe.d @@ -213,32 +213,32 @@ // currently it fails because `Dependency.opCmp` is not CTFE-able. /// DUB version requirement - @Optional @converter((scope ConfigParser!Dependency p) => p.node.as!string.parseDependency) - Dependency dub = Dependency.any; + @Optional @converter((scope ConfigParser!VersionRange p) => p.node.as!string.parseVersionRange) + VersionRange dub = VersionRange.Any; /// D front-end version requirement - @Optional @converter((scope ConfigParser!Dependency p) => p.node.as!string.parseDMDDependency) - Dependency frontend = Dependency.any; + @Optional @converter((scope ConfigParser!VersionRange p) => p.node.as!string.parseDMDDependency) + VersionRange frontend = VersionRange.Any; /// DMD version requirement - @Optional @converter((scope ConfigParser!Dependency p) => p.node.as!string.parseDMDDependency) - Dependency dmd = Dependency.any; + @Optional @converter((scope ConfigParser!VersionRange p) => p.node.as!string.parseDMDDependency) + VersionRange dmd = VersionRange.Any; /// LDC version requirement - @Optional @converter((scope ConfigParser!Dependency p) => p.node.as!string.parseDependency) - Dependency ldc = Dependency.any; + @Optional @converter((scope ConfigParser!VersionRange p) => p.node.as!string.parseVersionRange) + VersionRange ldc = VersionRange.Any; /// GDC version requirement - @Optional @converter((scope ConfigParser!Dependency p) => p.node.as!string.parseDependency) - Dependency gdc = Dependency.any; + @Optional @converter((scope ConfigParser!VersionRange p) => p.node.as!string.parseVersionRange) + VersionRange gdc = VersionRange.Any; /** Get the list of supported compilers. Returns: An array of couples of compiler name and compiler requirement */ - @property Tuple!(string, Dependency)[] supportedCompilers() const + @property Tuple!(string, VersionRange)[] supportedCompilers() const { - Tuple!(string, Dependency)[] res; - if (dmd != Dependency.invalid) res ~= Tuple!(string, Dependency)("dmd", dmd); - if (ldc != Dependency.invalid) res ~= Tuple!(string, Dependency)("ldc", ldc); - if (gdc != Dependency.invalid) res ~= Tuple!(string, Dependency)("gdc", gdc); + Tuple!(string, VersionRange)[] res; + if (dmd != VersionRange.Invalid) res ~= Tuple!(string, VersionRange)("dmd", dmd); + if (ldc != VersionRange.Invalid) res ~= Tuple!(string, VersionRange)("ldc", ldc); + if (gdc != VersionRange.Invalid) res ~= Tuple!(string, VersionRange)("gdc", gdc); return res; } @@ -246,7 +246,7 @@ const { import std.algorithm.searching : all; return only(dub, frontend, dmd, ldc, gdc) - .all!(r => r == Dependency.any); + .all!(r => r == VersionRange.Any); } } @@ -615,43 +615,44 @@ package(dub) void checkPlatform(const scope ref ToolchainRequirements tr, BuildPlatform platform, string package_name) { - import dub.compilers.utils : dmdLikeVersionToSemverLike; import std.algorithm.iteration : map; import std.format : format; - string compilerver; - Dependency compilerspec; + Version compilerver; + VersionRange compilerspec; switch (platform.compiler) { default: - compilerspec = Dependency.any; - compilerver = "0.0.0"; + compilerspec = VersionRange.Any; + compilerver = Version.minRelease; break; case "dmd": compilerspec = tr.dmd; compilerver = platform.compilerVersion.length - ? dmdLikeVersionToSemverLike(platform.compilerVersion) - : "0.0.0"; + ? Version(dmdLikeVersionToSemverLike(platform.compilerVersion)) + : Version.minRelease; break; case "ldc": compilerspec = tr.ldc; - compilerver = platform.compilerVersion; - if (!compilerver.length) compilerver = "0.0.0"; + compilerver = platform.compilerVersion.length + ? Version(platform.compilerVersion) + : Version.minRelease; break; case "gdc": compilerspec = tr.gdc; - compilerver = platform.compilerVersion; - if (!compilerver.length) compilerver = "0.0.0"; + compilerver = platform.compilerVersion.length + ? Version(platform.compilerVersion) + : Version.minRelease; break; } - enforce(compilerspec != Dependency.invalid, + enforce(compilerspec != VersionRange.Invalid, format( "Installed %s %s is not supported by %s. Supported compiler(s):\n%s", platform.compiler, platform.compilerVersion, package_name, tr.supportedCompilers.map!((cs) { auto str = " - " ~ cs[0]; - if (cs[1] != Dependency.any) str ~= ": " ~ cs[1].toString(); + if (cs[1] != VersionRange.Any) str ~= ": " ~ cs[1].toString(); return str; }).join("\n") ) @@ -666,7 +667,7 @@ ) ); - enforce(tr.frontend.matches(dmdLikeVersionToSemverLike(platform.frontendVersionString)), + enforce(tr.frontend.matches(Version(dmdLikeVersionToSemverLike(platform.frontendVersionString))), format( "Installed %s-%s with frontend %s does not comply with %s frontend requirement: %s\n" ~ "Please consider upgrading your installation.", @@ -680,34 +681,31 @@ { switch (name) { default: return false; - case "dub": req.dub = parseDependency(value); break; + case "dub": req.dub = parseVersionRange(value); break; case "frontend": req.frontend = parseDMDDependency(value); break; - case "ldc": req.ldc = parseDependency(value); break; - case "gdc": req.gdc = parseDependency(value); break; + case "ldc": req.ldc = parseVersionRange(value); break; + case "gdc": req.gdc = parseVersionRange(value); break; case "dmd": req.dmd = parseDMDDependency(value); break; } return true; } -private static Dependency parseDependency(string dep) +private VersionRange parseVersionRange(string dep) { - if (dep == "no") return Dependency.invalid; - return Dependency(dep); + if (dep == "no") return VersionRange.Invalid; + return VersionRange.fromString(dep); } -private static Dependency parseDMDDependency(string dep) +private VersionRange parseDMDDependency(string dep) { - import dub.compilers.utils : dmdLikeVersionToSemverLike; - import dub.dependency : Dependency; import std.algorithm : map, splitter; import std.array : join; - if (dep == "no") return Dependency.invalid; - return dep + if (dep == "no") return VersionRange.Invalid; + return VersionRange.fromString(dep .splitter(' ') .map!(r => dmdLikeVersionToSemverLike(r)) - .join(' ') - .Dependency; + .join(' ')); } private T clone(T)(ref const(T) val) @@ -785,3 +783,51 @@ fixDependenciesNames(root, aggr.tupleof[idx]); } } + +/** + Turn a DMD-like version (e.g. 2.082.1) into a SemVer-like version (e.g. 2.82.1). + The function accepts a dependency operator prefix and some text postfix. + Prefix and postfix are returned verbatim. + Params: + ver = version string, possibly with a dependency operator prefix and some + test postfix. + Returns: + A Semver compliant string +*/ +private string dmdLikeVersionToSemverLike(string ver) +{ + import std.algorithm : countUntil, joiner, map, skipOver, splitter; + import std.array : join, split; + import std.ascii : isDigit; + import std.conv : text; + import std.exception : enforce; + import std.functional : not; + import std.range : padRight; + + const start = ver.countUntil!isDigit; + enforce(start != -1, "Invalid semver: "~ver); + const prefix = ver[0 .. start]; + ver = ver[start .. $]; + + const end = ver.countUntil!(c => !c.isDigit && c != '.'); + const postfix = end == -1 ? null : ver[end .. $]; + auto verStr = ver[0 .. $-postfix.length]; + + auto comps = verStr + .splitter(".") + .map!((a) { if (a.length > 1) a.skipOver("0"); return a;}) + .padRight("0", 3); + + return text(prefix, comps.joiner("."), postfix); +} + +/// +unittest { + assert(dmdLikeVersionToSemverLike("2.082.1") == "2.82.1"); + assert(dmdLikeVersionToSemverLike("2.082.0") == "2.82.0"); + assert(dmdLikeVersionToSemverLike("2.082") == "2.82.0"); + assert(dmdLikeVersionToSemverLike("~>2.082") == "~>2.82.0"); + assert(dmdLikeVersionToSemverLike("~>2.082-beta1") == "~>2.82.0-beta1"); + assert(dmdLikeVersionToSemverLike("2.4.6") == "2.4.6"); + assert(dmdLikeVersionToSemverLike("2.4.6-alpha12") == "2.4.6-alpha12"); +} diff --git a/source/dub/recipe/sdl.d b/source/dub/recipe/sdl.d index 012268e..5d4d909 100644 --- a/source/dub/recipe/sdl.d +++ b/source/dub/recipe/sdl.d @@ -327,11 +327,11 @@ private Tag toSDL(const ref ToolchainRequirements tr) { Attribute[] attrs; - if (tr.dub != Dependency.any) attrs ~= new Attribute("dub", Value(tr.dub.toString())); - if (tr.frontend != Dependency.any) attrs ~= new Attribute("frontend", Value(tr.frontend.toString())); - if (tr.dmd != Dependency.any) attrs ~= new Attribute("dmd", Value(tr.dmd.toString())); - if (tr.ldc != Dependency.any) attrs ~= new Attribute("ldc", Value(tr.ldc.toString())); - if (tr.gdc != Dependency.any) attrs ~= new Attribute("gdc", Value(tr.gdc.toString())); + if (tr.dub != VersionRange.Any) attrs ~= new Attribute("dub", Value(tr.dub.toString())); + if (tr.frontend != VersionRange.Any) attrs ~= new Attribute("frontend", Value(tr.frontend.toString())); + if (tr.dmd != VersionRange.Any) attrs ~= new Attribute("dmd", Value(tr.dmd.toString())); + if (tr.ldc != VersionRange.Any) attrs ~= new Attribute("ldc", Value(tr.ldc.toString())); + if (tr.gdc != VersionRange.Any) attrs ~= new Attribute("gdc", Value(tr.gdc.toString())); return new Tag(null, "toolchainRequirements", null, attrs); } @@ -562,11 +562,11 @@ assert(rec.buildTypes.length == 2); assert(rec.buildTypes["debug"].dflags == ["": ["-g", "-debug"]]); assert(rec.buildTypes["release"].dflags == ["": ["-release", "-O"]]); - assert(rec.toolchainRequirements.dub == Dependency("~>1.11.0")); - assert(rec.toolchainRequirements.frontend == Dependency.any); - assert(rec.toolchainRequirements.dmd == Dependency("~>2.82.0")); - assert(rec.toolchainRequirements.ldc == Dependency.any); - assert(rec.toolchainRequirements.gdc == Dependency.any); + assert(rec.toolchainRequirements.dub == VersionRange.fromString("~>1.11.0")); + assert(rec.toolchainRequirements.frontend == VersionRange.Any); + assert(rec.toolchainRequirements.dmd == VersionRange.fromString("~>2.82.0")); + assert(rec.toolchainRequirements.ldc == VersionRange.Any); + assert(rec.toolchainRequirements.gdc == VersionRange.Any); assert(rec.ddoxFilterArgs == ["-arg1", "-arg2", "-arg3"], rec.ddoxFilterArgs.to!string); assert(rec.ddoxTool == "ddoxtool"); assert(rec.buildSettings.dependencies.length == 2); diff --git a/source/dub/recipe/selection.d b/source/dub/recipe/selection.d index 8ad0d25..1e127e3 100644 --- a/source/dub/recipe/selection.d +++ b/source/dub/recipe/selection.d @@ -4,7 +4,7 @@ module dub.recipe.selection; import dub.dependency; -import dub.internal.vibecompat.core.file : NativePath; +import dub.internal.vibecompat.inet.path : NativePath; import dub.internal.configy.Attributes; @@ -85,7 +85,6 @@ unittest { import dub.internal.configy.Read : parseConfigString; - import dub.internal.vibecompat.core.file : NativePath; immutable string content = `{ "fileVersion": 1, diff --git a/source/dub/test/base.d b/source/dub/test/base.d new file mode 100644 index 0000000..e5cd8e4 --- /dev/null +++ b/source/dub/test/base.d @@ -0,0 +1,434 @@ +/******************************************************************************* + + Base utilities (types, functions) used in tests + + The main type in this module is `TestDub`. `TestDub` is a class that + inherits from `Dub` and inject dependencies in it to avoid relying on IO. + First and foremost, by overriding `makePackageManager` and returning a + `TestPackageManager` instead, we avoid hitting the local filesystem and + instead present a view of the "local packages" that is fully in-memory. + Likewise, by providing a `MockPackageSupplier`, we can imitate the behavior + of the registry without relying on it. + + Leftover_IO: + Note that reliance on IO was originally all over the place in the Dub + codebase. For this reason, **new tests might find themselves doing I/O**. + When that happens, one should isolate the place which does I/O and refactor + the code to make dependency injection possible and practical. + An example of this is any place calling `Package.load`, `readPackageRecipe`, + or `Package.findPackageFile`. + + Supported_features: + In order to make writing tests possible and practical, not every features + where implemented in `TestDub`. Notably, path-based packages are not + supported at the moment, as they would need a better filesystem abstraction. + However, it would be desirable to add support for them at some point in the + future. + + Writing_tests: + `TestDub` exposes a few extra features to make writing tests easier. + Ideally, those extra features should be kept to a minimum, as a convenient + API for writing tests is likely to be a convenient API for library and + application developers as well. + It is expected that most tests will be centered about the `Project`, + also known as the "main package" that is loaded and drives Dub's logic + when common operations such as `dub build` are performed. + A minimalistic and documented unittest can be found in this module, + showing the various features of the test framework. + + Logging: + Dub writes to stdout / stderr in various places. While it would be desirable + to do dependency injection on it, the benefits brought by doing so currently + doesn't justify the amount of work required. If unittests for some reason + trigger messages being written to stdout/stderr, make sure that the logging + functions are being used instead of bare `write` / `writeln`. + +*******************************************************************************/ + +module dub.test.base; + +version (unittest): + +import std.array; +public import std.algorithm; + +import dub.data.settings; +public import dub.dependency; +public import dub.dub; +public import dub.package_; +import dub.packagemanager; +import dub.packagesuppliers.packagesupplier; +import dub.project; + +/// Example of a simple unittest for a project with a single dependency +unittest +{ + // `a` will be loaded as the project while `b` will be loaded + // as a simple package. The recipe files can be in JSON or SDL format, + // here we use both to demonstrate this. + const a = `{ "name": "a", "dependencies": { "b": "~>1.0" } }`; + const b = `name "b"`; + + // Enabling this would provide some more verbose output, which makes + // debugging a failing unittest much easier. + version (none) { + enableLogging(); + scope(exit) disableLogging(); + } + + scope dub = new TestDub(); + // Let the `PackageManager` know about the `b` package + dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); + // And about our main package + auto mainPackage = dub.addTestPackage(a, Version("1.0.0")); + // `Dub.loadPackage` will set this package as the project + // While not required, it follows the common Dub use case. + dub.loadPackage(mainPackage); + // This triggers the dependency resolution process that happens + // when one does not have a selection file in the project. + // Dub will resolve dependencies and generate the selection file + // (in memory). If your test has set dependencies / no dependencies, + // this will not be needed. + dub.upgrade(UpgradeOptions.select); + + // Simple tests can be performed using the public API + assert(dub.project.hasAllDependencies(), "project has missing dependencies"); + assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); + // While it is important to make your tests fail before you make them pass, + // as is common with TDD, it can also be useful to test simple assumptions + // as part of your basic tests. Here we want to make sure `getDependency` + // doesn't always return something regardless of its first argument. + // Note that this package segments modules by categories, e.g. dependencies, + // and tests are run serially in a module, so one may rely on previous tests + // having passed to avoid repeating some assumptions. + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); +} + +// TODO: Remove and handle logging the same way we handle other IO +import dub.internal.logging; + +public void enableLogging() +{ + setLogLevel(LogLevel.debug_); +} + +public void disableLogging() +{ + setLogLevel(LogLevel.none); +} + +/** + * An instance of Dub that does not rely on the environment + * + * This instance of dub should not read any environment variables, + * nor should it do any file IO, to make it usable and reliable in unittests. + * Currently it reads environment variables but does not read the configuration. + */ +public class TestDub : Dub +{ + /// Forward to base constructor + public this (string root = ".", PackageSupplier[] extras = null, + SkipPackageSuppliers skip = SkipPackageSuppliers.none) + { + super(root, extras, skip); + } + + /// Avoid loading user configuration + protected override Settings loadConfig(ref SpecialDirs dirs) const + { + // No-op + return Settings.init; + } + + /// + protected override PackageManager makePackageManager() const + { + return new TestPackageManager(); + } + + /// See `MockPackageSupplier` documentation for this class' implementation + protected override PackageSupplier makePackageSupplier(string url) const + { + return new MockPackageSupplier(url); + } + + /// Loads the package from the specified path as the main project package. + public override void loadPackage(NativePath path) + { + assert(0, "Not implemented"); + } + + /// Loads a specific package as the main project package (can be a sub package) + public override void loadPackage(Package pack) + { + m_project = new Project(m_packageManager, pack, new TestSelectedVersions()); + } + + /// Reintroduce parent overloads + public alias loadPackage = Dub.loadPackage; + + /** + * Returns a fully typed `TestPackageManager` + * + * This exposes the fully typed `PackageManager`, so that client + * can call convenience functions on it directly. + */ + public override @property inout(TestPackageManager) packageManager() inout + { + return cast(inout(TestPackageManager)) this.m_packageManager; + } + + /** + * Creates a package with the provided recipe + * + * This is a convenience function provided to create a package based on + * a given recipe. This is to allow test-cases to be written based off + * issues more easily. + * + * In order for the `Package` to be visible to `Dub`, use `addTestPackage`, + * as `makeTestPackage` simply creates the `Package` without adding it. + * + * Params: + * str = The string representation of the `PackageRecipe` + * recipe = The `PackageRecipe` to use + * vers = The version the package is at, e.g. `Version("1.0.0")` + * fmt = The format `str` is in, either JSON or SDL + * + * Returns: + * The created `Package` instance + */ + public Package makeTestPackage(string str, Version vers, PackageFormat fmt = PackageFormat.json) + { + import dub.recipe.io; + final switch (fmt) { + case PackageFormat.json: + auto recipe = parsePackageRecipe(str, "dub.json"); + recipe.version_ = vers.toString(); + return new Package(recipe); + case PackageFormat.sdl: + auto recipe = parsePackageRecipe(str, "dub.sdl"); + recipe.version_ = vers.toString(); + return new Package(recipe); + } + } + + /// Ditto + public Package addTestPackage(string str, Version vers, PackageFormat fmt = PackageFormat.json) + { + return this.packageManager.add(this.makeTestPackage(str, vers, fmt)); + } +} + +/** + * + */ +public class TestSelectedVersions : SelectedVersions { + import dub.recipe.selection; + + /// Forward to parent's constructor + public this(uint version_ = FileVersion) @safe pure nothrow @nogc + { + super(version_); + } + + /// Ditto + public this(Selected data) @safe pure nothrow @nogc + { + super(data); + } + + /// Do not do IO + public override void save(NativePath path) + { + // No-op + } +} + +/** + * A `PackageManager` suitable to be used in unittests + * + * This `PackageManager` does not perform any IO. It imitates the base + * `PackageManager`, exposing 3 locations, but loading of packages is not + * automatic and needs to be done by passing a `Package` instance. + */ +package class TestPackageManager : PackageManager +{ + /// List of all SCM packages that can be fetched by this instance + protected Package[Repository] scm; + + this() + { + NativePath pkg = NativePath("/tmp/dub-testsuite-nonexistant/packages/"); + NativePath user = NativePath("/tmp/dub-testsuite-nonexistant/user/"); + NativePath system = NativePath("/tmp/dub-testsuite-nonexistant/system/"); + super(pkg, user, system, false); + } + + /// Disabled as semantic are not implementable unless a virtual FS is created + public override @property void customCachePaths(NativePath[] custom_cache_paths) + { + assert(0, "Function not implemented"); + } + + /// Ditto + public override Package store(NativePath src, PlacementLocation dest, string name, Version vers) + { + assert(0, "Function not implemented"); + } + + /** + * This function usually scans the filesystem for packages. + * + * We don't want to do IO access and rely on users adding the packages + * before the test starts instead. + * + * Note: Deprecated `refresh(bool)` does IO, but it's deprecated + */ + public override void refresh() + { + // Do nothing + } + + /** + * Looks up a specific package + * + * Unlike its parent class, no lazy loading is performed. + * Additionally, as they are already deprecated, overrides are + * disabled and not available. + */ + public override Package getPackage(string name, Version vers, bool enable_overrides = false) + { + //assert(!enable_overrides, "Overrides are not implemented for TestPackageManager"); + + // Implementation inspired from `PackageManager.lookup`, + // except we replaced `load` with `lookup`. + if (auto pkg = this.m_internal.lookup(name, vers, this)) + return pkg; + + foreach (ref location; this.m_repositories) + if (auto p = location.lookup(name, vers, this)) + return p; + + return null; + } + + /** + * Re-Implementation of `loadSCMPackage`. + * + * The base implementation will do a `git` clone, which we would like to avoid. + * Instead, we allow unittests to explicitly define what packages should be + * reachable in a given test. + */ + public override Package loadSCMPackage(string name, Repository repo) + { + import std.string : chompPrefix; + + // We're trying to match `loadGitPackage` as much as possible + if (!repo.ref_.startsWith("~") && !repo.ref_.isGitHash) + return null; + + string gitReference = repo.ref_.chompPrefix("~"); + NativePath destination = this.getPackagePath(PlacementLocation.user, name, repo.ref_); + destination ~= name; + destination.endsWithSlash = true; + + foreach (p; getPackageIterator(name)) + if (p.path == destination) + return p; + + return this.loadSCMRepository(name, repo); + } + + /// The private part of `loadSCMPackage` + protected Package loadSCMRepository(string name, Repository repo) + { + if (auto prepo = repo in this.scm) { + this.add(*prepo); + return *prepo; + } + return null; + } + + /** + * Adds a `Package` to this `PackageManager` + * + * This is currently only available in unittests as it is a convenience + * function used by `TestDub`, but could be generalized once IO has been + * abstracted away from this class. + */ + public Package add(Package pkg) + { + // See `PackageManager.addPackages` for inspiration. + assert(!pkg.subPackages.length, "Subpackages are not yet supported"); + this.m_internal.fromPath ~= pkg; + return pkg; + } + + /// Add a reachable SCM package to this `PackageManager` + public void addTestSCMPackage(Repository repo, Package pkg) + { + this.scm[repo] = pkg; + } +} + +/** + * Implements a `PackageSupplier` that doesn't do any IO + * + * This `PackageSupplier` needs to be pre-loaded with `Package` it can + * find during the setup phase of the unittest. + */ +public class MockPackageSupplier : PackageSupplier +{ + /// Mapping of package name to packages, ordered by `Version` + protected Package[][string] pkgs; + + /// URL this was instantiated with + protected string url; + + /// + public this(string url) + { + this.url = url; + } + + /// + public override @property string description() + { + return "unittest PackageSupplier for: " ~ this.url; + } + + /// + public override Version[] getVersions(string package_id) + { + if (auto ppkgs = package_id in this.pkgs) + return (*ppkgs).map!(pkg => pkg.version_).array; + return null; + } + + /// + public override void fetchPackage( + NativePath path, string package_id, in VersionRange dep, bool pre_release) + { + assert(0, this.url ~ " - fetchPackage not implemented for: " ~ package_id); + } + + /// + public override Json fetchPackageRecipe( + string package_id, in VersionRange dep, bool pre_release) + { + import dub.recipe.json; + + if (auto ppkgs = package_id in this.pkgs) + foreach_reverse (pkg; *ppkgs) + if ((!pkg.version_.isPreRelease || pre_release) && + dep.matches(pkg.version_)) + return toJson(pkg.recipe); + return Json.init; + } + + /// + public override SearchResult[] searchPackages(string query) + { + assert(0, this.url ~ " - searchPackages not implemented for: " ~ query); + } +} diff --git a/source/dub/test/dependencies.d b/source/dub/test/dependencies.d new file mode 100644 index 0000000..d9f78ed --- /dev/null +++ b/source/dub/test/dependencies.d @@ -0,0 +1,133 @@ +/******************************************************************************* + + Test for dependencies + + This module is mostly concerned with dependency resolutions and visible user + behavior. Tests that check how different recipe would interact with one + another, and how conflicts are resolved or reported, belong here. + + The project (the loaded package) is usually named 'a' and dependencies use + single-letter, increasing name, for simplicity. Version 1.0.0 is used where + versions do not matter. Packages are usually created in reverse dependency + order when possible, unless the creation order matters. + + Test that deal with dependency resolution should not concern themselves with + the registry: instead, packages are added to the `PackageManager`, as that + makes testing the core logic more robust without adding a layer + of complexity brought by the `PackageSupplier`. + + Most tests have 3 parts: First, setup the various packages. Then, run + `dub.upgrade(UpgradeOptions.select)` to create the selection. Finally, + run tests on the resulting state. + +*******************************************************************************/ + +module dub.test.dependencies; + +version (unittest): + +import dub.test.base; + +// Ensure that simple dependencies get resolved correctly +unittest +{ + const a = `name "a" +dependency "b" version="*" +dependency "c" version="*" +`; + const b = `name "b"`; + const c = `name "c"`; + + scope dub = new TestDub(); + dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl); + dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); + dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + + dub.upgrade(UpgradeOptions.select); + + assert(dub.project.hasAllDependencies(), "project has missing dependencies"); + assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); + assert(dub.project.getDependency("c", true), "Missing 'c' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); +} + +// Test that indirect dependencies get resolved correctly +unittest +{ + const a = `name "a" +dependency "b" version="*" +`; + const b = `name "b" +dependency "c" version="*" +`; + const c = `name "c"`; + + scope dub = new TestDub(); + dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl); + dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); + dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + + dub.upgrade(UpgradeOptions.select); + + assert(dub.project.hasAllDependencies(), "project has missing dependencies"); + assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); + assert(dub.project.getDependency("c", true), "Missing 'c' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); +} + +// Simple diamond dependency +unittest +{ + const a = `name "a" +dependency "b" version="*" +dependency "c" version="*" +`; + const b = `name "b" +dependency "d" version="*" +`; + const c = `name "c" +dependency "d" version="*" +`; + const d = `name "d"`; + + scope dub = new TestDub(); + dub.addTestPackage(d, Version("1.0.0"), PackageFormat.sdl); + dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl); + dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); + dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + + dub.upgrade(UpgradeOptions.select); + + assert(dub.project.hasAllDependencies(), "project has missing dependencies"); + assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); + assert(dub.project.getDependency("c", true), "Missing 'c' dependency"); + assert(dub.project.getDependency("c", true), "Missing 'd' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); +} + +// Missing dependencies trigger an error +unittest +{ + const a = `name "a" +dependency "b" version="*" +`; + + scope dub = new TestDub(); + dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + + try + dub.upgrade(UpgradeOptions.select); + catch (Exception exc) + assert(exc.message() == `Failed to find any versions for package b, referenced by a 1.0.0`); + + assert(!dub.project.hasAllDependencies(), "project should have missing dependencies"); + assert(dub.project.getDependency("b", true) is null, "Found 'b' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); + + // Add the missing dependency to our PackageManager + dub.addTestPackage(`name "b"`, Version("1.0.0"), PackageFormat.sdl); + dub.upgrade(UpgradeOptions.select); + assert(dub.project.hasAllDependencies(), "project have missing dependencies"); + assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); +} diff --git a/source/dub/test/other.d b/source/dub/test/other.d new file mode 100644 index 0000000..9407fb2 --- /dev/null +++ b/source/dub/test/other.d @@ -0,0 +1,50 @@ +/******************************************************************************* + + Tests that don't fit in existing categories + +*******************************************************************************/ + +module dub.test.others; + +version (unittest): + +import std.algorithm; +import std.format; +import dub.test.base; + +// https://github.com/dlang/dub/issues/2696 +unittest +{ + const ValidURL = `git+https://example.com/dlang/dub`; + // Taken from a commit in the dub repository + const ValidHash = "54339dff7ce9ec24eda550f8055354f712f15800"; + const Template = `{"name": "%s", "dependencies": { +"dep1": { "repository": "%s", "version": "%s" }}}`; + + scope dub = new TestDub(); + dub.packageManager.addTestSCMPackage( + Repository(ValidURL, ValidHash), + // Note: SCM package are always marked as using `~master` + dub.makeTestPackage(`{ "name": "dep1" }`, Version(`~master`)), + ); + + // Invalid URL, valid hash + const a = Template.format("a", "git+https://nope.nope", ValidHash); + try + dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"))); + catch (Exception exc) + assert(exc.message.canFind("Unable to fetch")); + + // Valid URL, invalid hash + const b = Template.format("b", ValidURL, "invalid"); + try + dub.loadPackage(dub.addTestPackage(b, Version("1.0.0"))); + catch (Exception exc) + assert(exc.message.canFind("Unable to fetch")); + + // Valid URL, valid hash + const c = Template.format("c", ValidURL, ValidHash); + dub.loadPackage(dub.addTestPackage(c, Version("1.0.0"))); + assert(dub.project.hasAllDependencies()); + assert(dub.project.getDependency("dep1", true), "Missing 'dep1' dependency"); +}