1 /**
2 	A package manager.
3 
4 	Copyright: © 2012-2013 Matthias Dondorff, 2012-2016 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff, Sönke Ludwig
7 */
8 module dub.dub;
9 
10 import dub.compilers.compiler;
11 import dub.data.settings : SPS = SkipPackageSuppliers, Settings;
12 import dub.dependency;
13 import dub.dependencyresolver;
14 import dub.internal.utils;
15 import dub.internal.vibecompat.core.file;
16 import dub.internal.vibecompat.data.json;
17 import dub.internal.vibecompat.inet.url;
18 import dub.internal.logging;
19 import dub.package_;
20 import dub.packagemanager;
21 import dub.packagesuppliers;
22 import dub.project;
23 import dub.generators.generator;
24 import dub.init;
25 
26 import std.algorithm;
27 import std.array : array, replace;
28 import std.conv : text, to;
29 import std.encoding : sanitize;
30 import std.exception : enforce;
31 import std.file;
32 import std.process : environment;
33 import std.range : assumeSorted, empty;
34 import std.string;
35 
36 // Set output path and options for coverage reports
37 version (DigitalMars) version (D_Coverage)
38 {
39 	shared static this()
40 	{
41 		import core.runtime, std.file, std.path, std.stdio;
42 		dmd_coverSetMerge(true);
43 		auto path = buildPath(dirName(thisExePath()), "../cov");
44 		if (!path.exists)
45 			mkdir(path);
46 		dmd_coverDestPath(path);
47 	}
48 }
49 
50 static this()
51 {
52 	import dub.compilers.dmd : DMDCompiler;
53 	import dub.compilers.gdc : GDCCompiler;
54 	import dub.compilers.ldc : LDCCompiler;
55 	registerCompiler(new DMDCompiler);
56 	registerCompiler(new GDCCompiler);
57 	registerCompiler(new LDCCompiler);
58 }
59 
60 deprecated("use defaultRegistryURLs") enum defaultRegistryURL = defaultRegistryURLs[0];
61 
62 /// The URL to the official package registry and it's default fallback registries.
63 static immutable string[] defaultRegistryURLs = [
64 	"https://code.dlang.org/",
65 	"https://codemirror.dlang.org/",
66 	"https://dub.bytecraft.nl/",
67 	"https://code-mirror.dlang.io/",
68 ];
69 
70 /** Returns a default list of package suppliers.
71 
72 	This will contain a single package supplier that points to the official
73 	package registry.
74 
75 	See_Also: `defaultRegistryURLs`
76 */
77 PackageSupplier[] defaultPackageSuppliers()
78 {
79 	logDiagnostic("Using dub registry url '%s'", defaultRegistryURLs[0]);
80 	return [new FallbackPackageSupplier(defaultRegistryURLs.map!getRegistryPackageSupplier.array)];
81 }
82 
83 /** Returns a registry package supplier according to protocol.
84 
85 	Allowed protocols are dub+http(s):// and maven+http(s)://.
86 */
87 PackageSupplier getRegistryPackageSupplier(string url)
88 {
89 	switch (url.startsWith("dub+", "mvn+", "file://"))
90 	{
91 		case 1:
92 			return new RegistryPackageSupplier(URL(url[4..$]));
93 		case 2:
94 			return new MavenRegistryPackageSupplier(URL(url[4..$]));
95 		case 3:
96 			return new FileSystemPackageSupplier(NativePath(url[7..$]));
97 		default:
98 			return new RegistryPackageSupplier(URL(url));
99 	}
100 }
101 
102 unittest
103 {
104 	auto dubRegistryPackageSupplier = getRegistryPackageSupplier("dub+https://code.dlang.org");
105 	assert(dubRegistryPackageSupplier.description.canFind(" https://code.dlang.org"));
106 
107 	dubRegistryPackageSupplier = getRegistryPackageSupplier("https://code.dlang.org");
108 	assert(dubRegistryPackageSupplier.description.canFind(" https://code.dlang.org"));
109 
110 	auto mavenRegistryPackageSupplier = getRegistryPackageSupplier("mvn+http://localhost:8040/maven/libs-release/dubpackages");
111 	assert(mavenRegistryPackageSupplier.description.canFind(" http://localhost:8040/maven/libs-release/dubpackages"));
112 
113 	auto fileSystemPackageSupplier = getRegistryPackageSupplier("file:///etc/dubpackages");
114 	assert(fileSystemPackageSupplier.description.canFind(" " ~ NativePath("/etc/dubpackages").toNativeString));
115 }
116 
117 /** Provides a high-level entry point for DUB's functionality.
118 
119 	This class provides means to load a certain project (a root package with
120 	all of its dependencies) and to perform high-level operations as found in
121 	the command line interface.
122 */
123 class Dub {
124 	private {
125 		bool m_dryRun = false;
126 		PackageManager m_packageManager;
127 		PackageSupplier[] m_packageSuppliers;
128 		NativePath m_rootPath;
129 		SpecialDirs m_dirs;
130 		Settings m_config;
131 		Project m_project;
132 		string m_defaultCompiler;
133 	}
134 
135 	/** The default placement location of fetched packages.
136 
137 		This property can be altered, so that packages which are downloaded as part
138 		of the normal upgrade process are stored in a certain location. This is
139 		how the "--local" and "--system" command line switches operate.
140 	*/
141 	PlacementLocation defaultPlacementLocation = PlacementLocation.user;
142 
143 
144 	/** Initializes the instance for use with a specific root package.
145 
146 		Note that a package still has to be loaded using one of the
147 		`loadPackage` overloads.
148 
149 		Params:
150 			root_path = Path to the root package
151 			additional_package_suppliers = A list of package suppliers to try
152 				before the suppliers found in the configurations files and the
153 				`defaultPackageSuppliers`.
154 			skip_registry = Can be used to skip using the configured package
155 				suppliers, as well as the default suppliers.
156 	*/
157 	this(string root_path = ".", PackageSupplier[] additional_package_suppliers = null,
158 			SkipPackageSuppliers skip_registry = SkipPackageSuppliers.none)
159 	{
160 		m_rootPath = NativePath(root_path);
161 		if (!m_rootPath.absolute) m_rootPath = getWorkingDirectory() ~ m_rootPath;
162 
163 		init();
164 
165 		m_packageSuppliers = this.computePkgSuppliers(additional_package_suppliers,
166 			skip_registry, environment.get("DUB_REGISTRY", null));
167 		m_packageManager = new PackageManager(m_rootPath, m_dirs.userPackages, m_dirs.systemSettings, false);
168 
169 		auto ccps = m_config.customCachePaths;
170 		if (ccps.length)
171 			m_packageManager.customCachePaths = ccps;
172 
173 		// TODO: Move this environment read out of the ctor
174 		if (auto p = environment.get("DUBPATH")) {
175 			version(Windows) enum pathsep = ";";
176 			else enum pathsep = ":";
177 			NativePath[] paths = p.split(pathsep)
178 				.map!(p => NativePath(p))().array();
179 			m_packageManager.searchPath = paths;
180 		}
181 	}
182 
183 	/** Initializes the instance with a single package search path, without
184 		loading a package.
185 
186 		This constructor corresponds to the "--bare" option of the command line
187 		interface.
188 
189 		Params:
190 		  root = The root path of the Dub instance itself.
191 		  pkg_root = The root of the location where packages are located
192 					 Only packages under this location will be accessible.
193 					 Note that packages at the top levels will be ignored.
194 	*/
195 	this(NativePath root, NativePath pkg_root)
196 	{
197 		// Note: We're doing `init()` before setting the `rootPath`,
198 		// to prevent `init` from reading the project's settings.
199 		init();
200 		this.m_rootPath = root;
201 		m_packageManager = new PackageManager(pkg_root);
202 	}
203 
204 	deprecated("Use the overload that takes `(NativePath pkg_root, NativePath root)`")
205 	this(NativePath pkg_root)
206 	{
207 		this(pkg_root, pkg_root);
208 	}
209 
210 	private void init()
211 	{
212 		this.m_dirs = SpecialDirs.make();
213 		this.loadConfig();
214 		this.determineDefaultCompiler();
215 	}
216 
217 	/**
218 	 * Load user configuration for this instance
219 	 *
220 	 * This can be overloaded in child classes to prevent library / unittest
221 	 * dub from doing any kind of file IO.
222 	 */
223 	protected void loadConfig()
224 	{
225 		import dub.internal.configy.Read;
226 
227 		void readSettingsFile (NativePath path_)
228 		{
229 			// TODO: Remove `StrictMode.Warn` after v1.40 release
230 			// The default is to error, but as the previous parser wasn't
231 			// complaining, we should first warn the user.
232 			const path = path_.toNativeString();
233 			if (path.exists) {
234 				auto newConf = parseConfigFileSimple!Settings(path, StrictMode.Warn);
235 				if (!newConf.isNull())
236 					this.m_config = this.m_config.merge(newConf.get());
237 			}
238 		}
239 
240 		const dubFolderPath = NativePath(thisExePath).parentPath;
241 
242 		// override default userSettings + userPackages if a $DPATH or
243 		// $DUB_HOME environment variable is set.
244 		bool overrideDubHomeFromEnv;
245 		{
246 			string dubHome = environment.get("DUB_HOME");
247 			if (!dubHome.length) {
248 				auto dpath = environment.get("DPATH");
249 				if (dpath.length)
250 					dubHome = (NativePath(dpath) ~ "dub/").toNativeString();
251 
252 			}
253 			if (dubHome.length) {
254 				overrideDubHomeFromEnv = true;
255 
256 				m_dirs.userSettings = NativePath(dubHome);
257 				m_dirs.userPackages = m_dirs.userSettings;
258 				m_dirs.cache = m_dirs.userPackages ~ "cache";
259 			}
260 		}
261 
262 		readSettingsFile(m_dirs.systemSettings ~ "settings.json");
263 		readSettingsFile(dubFolderPath ~ "../etc/dub/settings.json");
264 		version (Posix) {
265 			if (dubFolderPath.absolute && dubFolderPath.startsWith(NativePath("usr")))
266 				readSettingsFile(NativePath("/etc/dub/settings.json"));
267 		}
268 
269 		// Override user + local package path from system / binary settings
270 		// Then continues loading local settings from these folders. (keeping
271 		// global /etc/dub/settings.json settings intact)
272 		//
273 		// Don't use it if either $DPATH or $DUB_HOME are set, as environment
274 		// variables usually take precedence over configuration.
275 		if (!overrideDubHomeFromEnv && this.m_config.dubHome.set) {
276 			m_dirs.userSettings = NativePath(this.m_config.dubHome.expandEnvironmentVariables);
277 		}
278 
279 		// load user config:
280 		readSettingsFile(m_dirs.userSettings ~ "settings.json");
281 
282 		// load per-package config:
283 		if (!this.m_rootPath.empty)
284 			readSettingsFile(this.m_rootPath ~ "dub.settings.json");
285 
286 		// same as userSettings above, but taking into account the
287 		// config loaded from user settings and per-package config as well.
288 		if (!overrideDubHomeFromEnv && this.m_config.dubHome.set) {
289 			m_dirs.userPackages = NativePath(this.m_config.dubHome.expandEnvironmentVariables);
290 			m_dirs.cache = m_dirs.userPackages ~ "cache";
291 		}
292 	}
293 
294 	unittest
295 	{
296 		scope (exit) environment.remove("DUB_REGISTRY");
297 		auto dub = new TestDub(".", null, SkipPackageSuppliers.configured);
298 		assert(dub.m_packageSuppliers.length == 0);
299 		environment["DUB_REGISTRY"] = "http://example.com/";
300 		dub = new TestDub(".", null, SkipPackageSuppliers.configured);
301 		assert(dub.m_packageSuppliers.length == 1);
302 		environment["DUB_REGISTRY"] = "http://example.com/;http://foo.com/";
303 		dub = new TestDub(".", null, SkipPackageSuppliers.configured);
304 		assert(dub.m_packageSuppliers.length == 2);
305 		dub = new TestDub(".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured);
306 		assert(dub.m_packageSuppliers.length == 3);
307 	}
308 
309 	/** Get the list of package suppliers.
310 
311 		Params:
312 			additional_package_suppliers = A list of package suppliers to try
313 				before the suppliers found in the configurations files and the
314 				`defaultPackageSuppliers`.
315 			skip_registry = Can be used to skip using the configured package
316 				suppliers, as well as the default suppliers.
317 	*/
318 	deprecated("This is an implementation detail. " ~
319 		"Use `packageSuppliers` to get the computed list of package " ~
320 		"suppliers once a `Dub` instance has been constructed.")
321 	public PackageSupplier[] getPackageSuppliers(PackageSupplier[] additional_package_suppliers, SkipPackageSuppliers skip_registry)
322 	{
323 		return this.computePkgSuppliers(additional_package_suppliers, skip_registry, environment.get("DUB_REGISTRY", null));
324 	}
325 
326 	/// Ditto
327 	private PackageSupplier[] computePkgSuppliers(
328 		PackageSupplier[] additional_package_suppliers, SkipPackageSuppliers skip_registry,
329 		string dub_registry_var)
330 	{
331 		PackageSupplier[] ps = additional_package_suppliers;
332 
333 		if (skip_registry < SkipPackageSuppliers.all)
334 		{
335 			ps ~= dub_registry_var
336 				.splitter(";")
337 				.map!(url => getRegistryPackageSupplier(url))
338 				.array;
339 		}
340 
341 		if (skip_registry < SkipPackageSuppliers.configured)
342 		{
343 			ps ~= m_config.registryUrls
344 				.map!(url => getRegistryPackageSupplier(url))
345 				.array;
346 		}
347 
348 		if (skip_registry < SkipPackageSuppliers.standard)
349 			ps ~= defaultPackageSuppliers();
350 
351 		return ps;
352 	}
353 
354 	/// ditto
355 	deprecated("This is an implementation detail. " ~
356 		"Use `packageSuppliers` to get the computed list of package " ~
357 		"suppliers once a `Dub` instance has been constructed.")
358 	public PackageSupplier[] getPackageSuppliers(PackageSupplier[] additional_package_suppliers)
359 	{
360 		return getPackageSuppliers(additional_package_suppliers, m_config.skipRegistry);
361 	}
362 
363 	unittest
364 	{
365 		auto dub = new TestDub();
366 
367 		assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.none, null).length == 1);
368 		assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.configured, null).length == 0);
369 		assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.standard, null).length == 0);
370 
371 		assert(dub.computePkgSuppliers(null, SkipPackageSuppliers.standard, "http://example.com/")
372 			.length == 1);
373 	}
374 
375 	@property bool dryRun() const { return m_dryRun; }
376 	@property void dryRun(bool v) { m_dryRun = v; }
377 
378 	/** Returns the root path (usually the current working directory).
379 	*/
380 	@property NativePath rootPath() const { return m_rootPath; }
381 	/// ditto
382 	deprecated("Changing the root path is deprecated as it has non-obvious pitfalls " ~
383 			   "(e.g. settings aren't reloaded). Instantiate a new `Dub` instead")
384 	@property void rootPath(NativePath root_path)
385 	{
386 		m_rootPath = root_path;
387 		if (!m_rootPath.absolute) m_rootPath = getWorkingDirectory() ~ m_rootPath;
388 	}
389 
390 	/// Returns the name listed in the dub.json of the current
391 	/// application.
392 	@property string projectName() const { return m_project.name; }
393 
394 	@property NativePath projectPath() const { return this.m_project.rootPackage.path; }
395 
396 	@property string[] configurations() const { return m_project.configurations; }
397 
398 	@property inout(PackageManager) packageManager() inout { return m_packageManager; }
399 
400 	@property inout(Project) project() inout { return m_project; }
401 
402 	@property inout(PackageSupplier)[] packageSuppliers() inout { return m_packageSuppliers; }
403 
404 	/** Returns the default compiler binary to use for building D code.
405 
406 		If set, the "defaultCompiler" field of the DUB user or system
407 		configuration file will be used. Otherwise the PATH environment variable
408 		will be searched for files named "dmd", "gdc", "gdmd", "ldc2", "ldmd2"
409 		(in that order, taking into account operating system specific file
410 		extensions) and the first match is returned. If no match is found, "dmd"
411 		will be used.
412 	*/
413 	@property string defaultCompiler() const { return m_defaultCompiler; }
414 
415 	/** Returns the default architecture to use for building D code.
416 
417 		If set, the "defaultArchitecture" field of the DUB user or system
418 		configuration file will be used. Otherwise null will be returned.
419 	*/
420 	@property string defaultArchitecture() const { return this.m_config.defaultArchitecture; }
421 
422 	/** Returns the default low memory option to use for building D code.
423 
424 		If set, the "defaultLowMemory" field of the DUB user or system
425 		configuration file will be used. Otherwise false will be returned.
426 	*/
427 	@property bool defaultLowMemory() const { return this.m_config.defaultLowMemory; }
428 
429 	@property const(string[string]) defaultEnvironments() const { return this.m_config.defaultEnvironments; }
430 	@property const(string[string]) defaultBuildEnvironments() const { return this.m_config.defaultBuildEnvironments; }
431 	@property const(string[string]) defaultRunEnvironments() const { return this.m_config.defaultRunEnvironments; }
432 	@property const(string[string]) defaultPreGenerateEnvironments() const { return this.m_config.defaultPreGenerateEnvironments; }
433 	@property const(string[string]) defaultPostGenerateEnvironments() const { return this.m_config.defaultPostGenerateEnvironments; }
434 	@property const(string[string]) defaultPreBuildEnvironments() const { return this.m_config.defaultPreBuildEnvironments; }
435 	@property const(string[string]) defaultPostBuildEnvironments() const { return this.m_config.defaultPostBuildEnvironments; }
436 	@property const(string[string]) defaultPreRunEnvironments() const { return this.m_config.defaultPreRunEnvironments; }
437 	@property const(string[string]) defaultPostRunEnvironments() const { return this.m_config.defaultPostRunEnvironments; }
438 
439 	/** Loads the package that resides within the configured `rootPath`.
440 	*/
441 	void loadPackage()
442 	{
443 		loadPackage(m_rootPath);
444 	}
445 
446 	/// Loads the package from the specified path as the main project package.
447 	void loadPackage(NativePath path)
448 	{
449 		m_project = new Project(m_packageManager, path);
450 	}
451 
452 	/// Loads a specific package as the main project package (can be a sub package)
453 	void loadPackage(Package pack)
454 	{
455 		m_project = new Project(m_packageManager, pack);
456 	}
457 
458 	/** Loads a single file package.
459 
460 		Single-file packages are D files that contain a package receipe comment
461 		at their top. A recipe comment must be a nested `/+ ... +/` style
462 		comment, containing the virtual recipe file name and a colon, followed by the
463 		recipe contents (what would normally be in dub.sdl/dub.json).
464 
465 		Example:
466 		---
467 		/+ dub.sdl:
468 		   name "test"
469 		   dependency "vibe-d" version="~>0.7.29"
470 		+/
471 		import vibe.http.server;
472 
473 		void main()
474 		{
475 			auto settings = new HTTPServerSettings;
476 			settings.port = 8080;
477 			listenHTTP(settings, &hello);
478 		}
479 
480 		void hello(HTTPServerRequest req, HTTPServerResponse res)
481 		{
482 			res.writeBody("Hello, World!");
483 		}
484 		---
485 
486 		The script above can be invoked with "dub --single test.d".
487 	*/
488 	void loadSingleFilePackage(NativePath path)
489 	{
490 		import dub.recipe.io : parsePackageRecipe;
491 		import std.file : readText;
492 		import std.path : baseName, stripExtension;
493 
494 		path = makeAbsolute(path);
495 
496 		string file_content = readText(path.toNativeString());
497 
498 		if (file_content.startsWith("#!")) {
499 			auto idx = file_content.indexOf('\n');
500 			enforce(idx > 0, "The source fine doesn't contain anything but a shebang line.");
501 			file_content = file_content[idx+1 .. $];
502 		}
503 
504 		file_content = file_content.strip();
505 
506 		string recipe_content;
507 
508 		if (file_content.startsWith("/+")) {
509 			file_content = file_content[2 .. $];
510 			auto idx = file_content.indexOf("+/");
511 			enforce(idx >= 0, "Missing \"+/\" to close comment.");
512 			recipe_content = file_content[0 .. idx].strip();
513 		} else throw new Exception("The source file must start with a recipe comment.");
514 
515 		auto nidx = recipe_content.indexOf('\n');
516 
517 		auto idx = recipe_content.indexOf(':');
518 		enforce(idx > 0 && (nidx < 0 || nidx > idx),
519 			"The first line of the recipe comment must list the recipe file name followed by a colon (e.g. \"/+ dub.sdl:\").");
520 		auto recipe_filename = recipe_content[0 .. idx];
521 		recipe_content = recipe_content[idx+1 .. $];
522 		auto recipe_default_package_name = path.toString.baseName.stripExtension.strip;
523 
524 		auto recipe = parsePackageRecipe(recipe_content, recipe_filename, null, recipe_default_package_name);
525 		enforce(recipe.buildSettings.sourceFiles.length == 0, "Single-file packages are not allowed to specify source files.");
526 		enforce(recipe.buildSettings.sourcePaths.length == 0, "Single-file packages are not allowed to specify source paths.");
527 		enforce(recipe.buildSettings.importPaths.length == 0, "Single-file packages are not allowed to specify import paths.");
528 		recipe.buildSettings.sourceFiles[""] = [path.toNativeString()];
529 		recipe.buildSettings.sourcePaths[""] = [];
530 		recipe.buildSettings.importPaths[""] = [];
531 		recipe.buildSettings.mainSourceFile = path.toNativeString();
532 		if (recipe.buildSettings.targetType == TargetType.autodetect)
533 			recipe.buildSettings.targetType = TargetType.executable;
534 
535 		auto pack = new Package(recipe, path.parentPath, null, "~master");
536 		loadPackage(pack);
537 	}
538 	/// ditto
539 	void loadSingleFilePackage(string path)
540 	{
541 		loadSingleFilePackage(NativePath(path));
542 	}
543 
544 	/** Gets the default configuration for a particular build platform.
545 
546 		This forwards to `Project.getDefaultConfiguration` and requires a
547 		project to be loaded.
548 	*/
549 	string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library_configs = true) const { return m_project.getDefaultConfiguration(platform, allow_non_library_configs); }
550 
551 	/** Attempts to upgrade the dependency selection of the loaded project.
552 
553 		Params:
554 			options = Flags that control how the upgrade is carried out
555 			packages_to_upgrade = Optional list of packages. If this list
556 				contains one or more packages, only those packages will
557 				be upgraded. Otherwise, all packages will be upgraded at
558 				once.
559 	*/
560 	void upgrade(UpgradeOptions options, string[] packages_to_upgrade = null)
561 	{
562 		// clear non-existent version selections
563 		if (!(options & UpgradeOptions.upgrade)) {
564 			next_pack:
565 			foreach (p; m_project.selections.selectedPackages) {
566 				auto dep = m_project.selections.getSelectedVersion(p);
567 				if (!dep.path.empty) {
568 					auto path = dep.path;
569 					if (!path.absolute) path = this.rootPath ~ path;
570 					try if (m_packageManager.getOrLoadPackage(path)) continue;
571 					catch (Exception e) { logDebug("Failed to load path based selection: %s", e.toString().sanitize); }
572 				} else if (!dep.repository.empty) {
573 					if (m_packageManager.loadSCMPackage(getBasePackageName(p), dep.repository))
574 						continue;
575 				} else {
576 					if (m_packageManager.getPackage(p, dep.version_)) continue;
577 					foreach (ps; m_packageSuppliers) {
578 						try {
579 							auto versions = ps.getVersions(p);
580 							if (versions.canFind!(v => dep.matches(v, VersionMatchMode.strict)))
581 								continue next_pack;
582 						} catch (Exception e) {
583 							logWarn("Error querying versions for %s, %s: %s", p, ps.description, e.msg);
584 							logDebug("Full error: %s", e.toString().sanitize());
585 						}
586 					}
587 				}
588 
589 				logWarn("Selected package %s %s doesn't exist. Using latest matching version instead.", p, dep);
590 				m_project.selections.deselectVersion(p);
591 			}
592 		}
593 
594 		auto resolver = new DependencyVersionResolver(
595 			this, options, m_project.rootPackage, m_project.selections);
596 		Dependency[string] versions = resolver.resolve(packages_to_upgrade);
597 
598 		if (options & UpgradeOptions.dryRun) {
599 			bool any = false;
600 			string rootbasename = getBasePackageName(m_project.rootPackage.name);
601 
602 			foreach (p, ver; versions) {
603 				if (!ver.path.empty || !ver.repository.empty) continue;
604 
605 				auto basename = getBasePackageName(p);
606 				if (basename == rootbasename) continue;
607 
608 				if (!m_project.selections.hasSelectedVersion(basename)) {
609 					logInfo("Upgrade", Color.cyan,
610 						"Package %s would be selected with version %s", basename, ver);
611 					any = true;
612 					continue;
613 				}
614 				auto sver = m_project.selections.getSelectedVersion(basename);
615 				if (!sver.path.empty || !sver.repository.empty) continue;
616 				if (ver.version_ <= sver.version_) continue;
617 				logInfo("Upgrade", Color.cyan,
618 					"%s would be upgraded from %s to %s.",
619 					basename.color(Mode.bold), sver, ver);
620 				any = true;
621 			}
622 			if (any) logInfo("Use \"%s\" to perform those changes", "dub upgrade".color(Mode.bold));
623 			return;
624 		}
625 
626 		foreach (p, ver; versions) {
627 			assert(!p.canFind(":"), "Resolved packages contain a sub package!?: "~p);
628 			Package pack;
629 			if (!ver.path.empty) {
630 				try pack = m_packageManager.getOrLoadPackage(ver.path);
631 				catch (Exception e) {
632 					logDebug("Failed to load path based selection: %s", e.toString().sanitize);
633 					continue;
634 				}
635 			} else if (!ver.repository.empty) {
636 				pack = m_packageManager.loadSCMPackage(p, ver.repository);
637 			} else {
638 				assert(ver.isExactVersion, "Resolved dependency is neither path, nor repository, nor exact version based!?");
639 				pack = m_packageManager.getPackage(p, ver.version_);
640 				if (pack && m_packageManager.isManagedPackage(pack)
641 					&& ver.version_.isBranch && (options & UpgradeOptions.upgrade) != 0)
642 				{
643 					// TODO: only re-install if there is actually a new commit available
644 					logInfo("Re-installing branch based dependency %s %s", p, ver.toString());
645 					m_packageManager.remove(pack);
646 					pack = null;
647 				}
648 			}
649 
650 			FetchOptions fetchOpts;
651 			fetchOpts |= (options & UpgradeOptions.preRelease) != 0 ? FetchOptions.usePrerelease : FetchOptions.none;
652 			if (!pack) fetch(p, ver.version_, defaultPlacementLocation, fetchOpts, "getting selected version");
653 			if ((options & UpgradeOptions.select) && p != m_project.rootPackage.name) {
654 				if (!ver.repository.empty) {
655 					m_project.selections.selectVersion(p, ver.repository);
656 				} else if (ver.path.empty) {
657 					m_project.selections.selectVersion(p, ver.version_);
658 				} else {
659 					NativePath relpath = ver.path;
660 					if (relpath.absolute) relpath = relpath.relativeTo(m_project.rootPackage.path);
661 					m_project.selections.selectVersion(p, relpath);
662 				}
663 			}
664 		}
665 
666 		string[] missingDependenciesBeforeReinit = m_project.missingDependencies;
667 		m_project.reinit();
668 
669 		if (!m_project.hasAllDependencies) {
670 			auto resolvedDependencies = setDifference(
671 					assumeSorted(missingDependenciesBeforeReinit),
672 					assumeSorted(m_project.missingDependencies)
673 				);
674 			if (!resolvedDependencies.empty)
675 				upgrade(options, m_project.missingDependencies);
676 		}
677 
678 		if ((options & UpgradeOptions.select) && !(options & (UpgradeOptions.noSaveSelections | UpgradeOptions.dryRun)))
679 			m_project.saveSelections();
680 	}
681 
682 	/** Generate project files for a specified generator.
683 
684 		Any existing project files will be overridden.
685 	*/
686 	void generateProject(string ide, GeneratorSettings settings)
687 	{
688 		settings.cache = this.m_dirs.cache;
689 		if (settings.overrideToolWorkingDirectory is NativePath.init)
690 			settings.overrideToolWorkingDirectory = m_rootPath;
691 		// With a requested `unittest` config, switch to the special test runner
692 		// config (which doesn't require an existing `unittest` configuration).
693 		if (settings.config == "unittest") {
694 			const test_config = m_project.addTestRunnerConfiguration(settings, !m_dryRun);
695 			if (test_config) settings.config = test_config;
696 		}
697 
698 		auto generator = createProjectGenerator(ide, m_project);
699 		if (m_dryRun) return; // TODO: pass m_dryRun to the generator
700 		generator.generate(settings);
701 	}
702 
703 	/** Generate project files using the special test runner (`dub test`) configuration.
704 
705 		Any existing project files will be overridden.
706 	*/
707 	void testProject(GeneratorSettings settings, string config, NativePath custom_main_file)
708 	{
709 		settings.cache = this.m_dirs.cache;
710 		if (settings.overrideToolWorkingDirectory is NativePath.init)
711 			settings.overrideToolWorkingDirectory = m_rootPath;
712 		if (!custom_main_file.empty && !custom_main_file.absolute) custom_main_file = m_rootPath ~ custom_main_file;
713 
714 		const test_config = m_project.addTestRunnerConfiguration(settings, !m_dryRun, config, custom_main_file);
715 		if (!test_config) return; // target type "none"
716 
717 		settings.config = test_config;
718 
719 		auto generator = createProjectGenerator("build", m_project);
720 		generator.generate(settings);
721 	}
722 
723 	/** Executes D-Scanner tests on the current project. **/
724 	void lintProject(string[] args)
725 	{
726 		import std.path : buildPath, buildNormalizedPath;
727 
728 		if (m_dryRun) return;
729 
730 		auto tool = "dscanner";
731 
732 		auto tool_pack = m_packageManager.getBestPackage(tool);
733 		if (!tool_pack) {
734 			logInfo("Hint", Color.light_blue, "%s is not present, getting and storing it user wide", tool);
735 			tool_pack = fetch(tool, VersionRange.Any, defaultPlacementLocation, FetchOptions.none);
736 		}
737 
738 		auto dscanner_dub = new Dub(null, m_packageSuppliers);
739 		dscanner_dub.loadPackage(tool_pack);
740 		dscanner_dub.upgrade(UpgradeOptions.select);
741 
742 		GeneratorSettings settings = this.makeAppSettings();
743 		foreach (dependencyPackage; m_project.dependencies)
744 		{
745 			auto cfgs = m_project.getPackageConfigs(settings.platform, null, true);
746 			auto buildSettings = dependencyPackage.getBuildSettings(settings.platform, cfgs[dependencyPackage.name]);
747 			foreach (importPath; buildSettings.importPaths) {
748 				settings.runArgs ~= ["-I", buildNormalizedPath(dependencyPackage.path.toNativeString(), importPath.idup)];
749 			}
750 		}
751 
752 		string configFilePath = buildPath(m_project.rootPackage.path.toNativeString(), "dscanner.ini");
753 		if (!args.canFind("--config") && exists(configFilePath)) {
754 			settings.runArgs ~= ["--config", configFilePath];
755 		}
756 
757 		settings.runArgs ~= args ~ [m_project.rootPackage.path.toNativeString()];
758 		dscanner_dub.generateProject("build", settings);
759 	}
760 
761 	/** Prints the specified build settings necessary for building the root package.
762 	*/
763 	void listProjectData(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type)
764 	{
765 		import std.stdio;
766 		import std.ascii : newline;
767 
768 		if (settings.overrideToolWorkingDirectory is NativePath.init)
769 			settings.overrideToolWorkingDirectory = m_rootPath;
770 
771 		// Split comma-separated lists
772 		string[] requestedDataSplit =
773 			requestedData
774 			.map!(a => a.splitter(",").map!strip)
775 			.joiner()
776 			.array();
777 
778 		auto data = m_project.listBuildSettings(settings, requestedDataSplit, list_type);
779 
780 		string delimiter;
781 		final switch (list_type) with (ListBuildSettingsFormat) {
782 			case list: delimiter = newline ~ newline; break;
783 			case listNul: delimiter = "\0\0"; break;
784 			case commandLine: delimiter = " "; break;
785 			case commandLineNul: delimiter = "\0\0"; break;
786 		}
787 
788 		write(data.joiner(delimiter));
789 		if (delimiter != "\0\0") writeln();
790 	}
791 
792 	/// Cleans intermediate/cache files of the given package (or all packages)
793 	deprecated("Use `clean(Package)` instead")
794 	void cleanPackage(NativePath path)
795 	{
796 		auto ppack = Package.findPackageFile(path);
797 		enforce(!ppack.empty, "No package found.", path.toNativeString());
798 		this.clean(Package.load(path, ppack));
799 	}
800 
801 	/// Ditto
802 	void clean()
803 	{
804 		const cache = this.m_dirs.cache;
805 		logInfo("Cleaning", Color.green, "all artifacts at %s",
806 			cache.toNativeString().color(Mode.bold));
807 		if (existsFile(cache))
808 			rmdirRecurse(cache.toNativeString());
809 	}
810 
811 	/// Ditto
812 	void clean(Package pack)
813 	{
814 		const cache = this.packageCache(pack);
815 		logInfo("Cleaning", Color.green, "artifacts for package %s at %s",
816 			pack.name.color(Mode.bold),
817 			cache.toNativeString().color(Mode.bold));
818 
819 		// TODO: clear target files and copy files
820 		if (existsFile(cache))
821 			rmdirRecurse(cache.toNativeString());
822 	}
823 
824 	/// Fetches the package matching the dependency and places it in the specified location.
825 	deprecated("Use the overload that accepts either a `Version` or a `VersionRange` as second argument")
826 	Package fetch(string packageId, const Dependency dep, PlacementLocation location, FetchOptions options, string reason = "")
827 	{
828 		const vrange = dep.visit!(
829 			(VersionRange range) => range,
830 			function VersionRange (any) { throw new Exception("Cannot call `dub.fetch` with a " ~ typeof(any).stringof ~ " dependency"); }
831 		);
832 		return this.fetch(packageId, vrange, location, options, reason);
833 	}
834 
835 	/// Ditto
836 	Package fetch(string packageId, in Version vers, PlacementLocation location, FetchOptions options, string reason = "")
837 	{
838 		return this.fetch(packageId, VersionRange(vers, vers), location, options, reason);
839 	}
840 
841 	/// Ditto
842 	Package fetch(string packageId, in VersionRange range, PlacementLocation location, FetchOptions options, string reason = "")
843 	{
844 		auto basePackageName = getBasePackageName(packageId);
845 		Json pinfo;
846 		PackageSupplier supplier;
847 		foreach(ps; m_packageSuppliers){
848 			try {
849 				pinfo = ps.fetchPackageRecipe(basePackageName, Dependency(range), (options & FetchOptions.usePrerelease) != 0);
850 				if (pinfo.type == Json.Type.null_)
851 					continue;
852 				supplier = ps;
853 				break;
854 			} catch(Exception e) {
855 				logWarn("Package %s not found for %s: %s", packageId, ps.description, e.msg);
856 				logDebug("Full error: %s", e.toString().sanitize());
857 			}
858 		}
859 		enforce(pinfo.type != Json.Type.undefined, "No package "~packageId~" was found matching the dependency " ~ range.toString());
860 		Version ver = Version(pinfo["version"].get!string);
861 
862 		// always upgrade branch based versions - TODO: actually check if there is a new commit available
863 		Package existing = m_packageManager.getPackage(packageId, ver, location);
864 		if (options & FetchOptions.printOnly) {
865 			if (existing && existing.version_ != ver)
866 				logInfo("A new version for %s is available (%s -> %s). Run \"%s\" to switch.",
867 					packageId.color(Mode.bold), existing.version_, ver,
868 					text("dub upgrade ", packageId).color(Mode.bold));
869 			return null;
870 		}
871 
872 		if (existing) {
873 			if (!ver.isBranch() || !(options & FetchOptions.forceBranchUpgrade) || location == PlacementLocation.local) {
874 				// TODO: support git working trees by performing a "git pull" instead of this
875 				logDiagnostic("Package %s %s (in %s packages) is already present with the latest version, skipping upgrade.",
876 					packageId, ver, location.toString);
877 				return existing;
878 			} else {
879 				logInfo("Removing", Color.yellow, "%s %s to prepare replacement with a new version", packageId.color(Mode.bold), ver);
880 				if (!m_dryRun) m_packageManager.remove(existing);
881 			}
882 		}
883 
884 		if (reason.length) logInfo("Fetching", Color.yellow, "%s %s (%s)", packageId.color(Mode.bold), ver, reason);
885 		else logInfo("Fetching", Color.yellow, "%s %s", packageId.color(Mode.bold), ver);
886 		if (m_dryRun) return null;
887 
888 		logDebug("Acquiring package zip file");
889 
890 		// repeat download on corrupted zips, see #1336
891 		foreach_reverse (i; 0..3)
892 		{
893 			import std.zip : ZipException;
894 
895 			auto path = getTempFile(basePackageName, ".zip");
896 			supplier.fetchPackage(path, basePackageName, Dependency(range), (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail?
897 			scope(exit) removeFile(path);
898 			logDiagnostic("Placing to %s...", location.toString());
899 
900 			try {
901 				return m_packageManager.store(path, location, basePackageName, ver);
902 			} catch (ZipException e) {
903 				logInfo("Failed to extract zip archive for %s %s...", packageId, ver);
904 				// rethrow the exception at the end of the loop
905 				if (i == 0)
906 					throw e;
907 			}
908 		}
909 		assert(0, "Should throw a ZipException instead.");
910 	}
911 
912 	/** Removes a specific locally cached package.
913 
914 		This will delete the package files from disk and removes the
915 		corresponding entry from the list of known packages.
916 
917 		Params:
918 			pack = Package instance to remove
919 	*/
920 	void remove(in Package pack)
921 	{
922 		logInfo("Removing", Color.yellow, "%s (in %s)", pack.name.color(Mode.bold), pack.path.toNativeString());
923 		if (!m_dryRun) m_packageManager.remove(pack);
924 	}
925 
926 	/// Compatibility overload. Use the version without a `force_remove` argument instead.
927 	deprecated("Use `remove(pack)` directly instead, the boolean has no effect")
928 	void remove(in Package pack, bool force_remove)
929 	{
930 		remove(pack);
931 	}
932 
933 	/// @see remove(string, string, RemoveLocation)
934 	enum RemoveVersionWildcard = "*";
935 
936 	/** Removes one or more versions of a locally cached package.
937 
938 		This will remove a given package with a specified version from the
939 		given location. It will remove at most one package, unless `version_`
940 		is set to `RemoveVersionWildcard`.
941 
942 		Params:
943 			package_id = Name of the package to be removed
944 			location_ = Specifies the location to look for the given package
945 				name/version.
946 			resolve_version = Callback to select package version.
947 	*/
948 	void remove(string package_id, PlacementLocation location,
949 				scope size_t delegate(in Package[] packages) resolve_version)
950 	{
951 		enforce(!package_id.empty);
952 		if (location == PlacementLocation.local) {
953 			logInfo("To remove a locally placed package, make sure you don't have any data"
954 					~ "\nleft in it's directory and then simply remove the whole directory.");
955 			throw new Exception("dub cannot remove locally installed packages.");
956 		}
957 
958 		Package[] packages;
959 
960 		// Retrieve packages to be removed.
961 		foreach(pack; m_packageManager.getPackageIterator(package_id))
962 			if (m_packageManager.isManagedPackage(pack))
963 				packages ~= pack;
964 
965 		// Check validity of packages to be removed.
966 		if(packages.empty) {
967 			throw new Exception("Cannot find package to remove. ("
968 				~ "id: '" ~ package_id ~ "', location: '" ~ to!string(location) ~ "'"
969 				~ ")");
970 		}
971 
972 		// Sort package list in ascending version order
973 		packages.sort!((a, b) => a.version_ < b.version_);
974 
975 		immutable idx = resolve_version(packages);
976 		if (idx == size_t.max)
977 			return;
978 		else if (idx != packages.length)
979 			packages = packages[idx .. idx + 1];
980 
981 		logDebug("Removing %s packages.", packages.length);
982 		foreach(pack; packages) {
983 			try {
984 				remove(pack);
985 			} catch (Exception e) {
986 				logError("Failed to remove %s %s: %s", package_id, pack.version_, e.msg);
987 				logInfo("Continuing with other packages (if any).");
988 			}
989 		}
990 	}
991 
992 	/// Compatibility overload. Use the version without a `force_remove` argument instead.
993 	deprecated("Use the overload without the 3rd argument (`force_remove`) instead")
994 	void remove(string package_id, PlacementLocation location, bool force_remove,
995 				scope size_t delegate(in Package[] packages) resolve_version)
996 	{
997 		remove(package_id, location, resolve_version);
998 	}
999 
1000 	/** Removes a specific version of a package.
1001 
1002 		Params:
1003 			package_id = Name of the package to be removed
1004 			version_ = Identifying a version or a wild card. If an empty string
1005 				is passed, the package will be removed from the location, if
1006 				there is only one version retrieved. This will throw an
1007 				exception, if there are multiple versions retrieved.
1008 			location_ = Specifies the location to look for the given package
1009 				name/version.
1010 	 */
1011 	void remove(string package_id, string version_, PlacementLocation location)
1012 	{
1013 		remove(package_id, location, (in packages) {
1014 			if (version_ == RemoveVersionWildcard || version_.empty)
1015 				return packages.length;
1016 
1017 			foreach (i, p; packages) {
1018 				if (p.version_ == Version(version_))
1019 					return i;
1020 			}
1021 			throw new Exception("Cannot find package to remove. ("
1022 				~ "id: '" ~ package_id ~ "', version: '" ~ version_ ~ "', location: '" ~ to!string(location) ~ "'"
1023 				~ ")");
1024 		});
1025 	}
1026 
1027 	/// Compatibility overload. Use the version without a `force_remove` argument instead.
1028 	deprecated("Use the overload without force_remove instead")
1029 	void remove(string package_id, string version_, PlacementLocation location, bool force_remove)
1030 	{
1031 		remove(package_id, version_, location);
1032 	}
1033 
1034 	/** Adds a directory to the list of locally known packages.
1035 
1036 		Forwards to `PackageManager.addLocalPackage`.
1037 
1038 		Params:
1039 			path = Path to the package
1040 			ver = Optional version to associate with the package (can be left
1041 				empty)
1042 			system = Make the package known system wide instead of user wide
1043 				(requires administrator privileges).
1044 
1045 		See_Also: `removeLocalPackage`
1046 	*/
1047 	void addLocalPackage(string path, string ver, bool system)
1048 	{
1049 		if (m_dryRun) return;
1050 		m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? PlacementLocation.system : PlacementLocation.user);
1051 	}
1052 
1053 	/** Removes a directory from the list of locally known packages.
1054 
1055 		Forwards to `PackageManager.removeLocalPackage`.
1056 
1057 		Params:
1058 			path = Path to the package
1059 			system = Make the package known system wide instead of user wide
1060 				(requires administrator privileges).
1061 
1062 		See_Also: `addLocalPackage`
1063 	*/
1064 	void removeLocalPackage(string path, bool system)
1065 	{
1066 		if (m_dryRun) return;
1067 		m_packageManager.removeLocalPackage(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user);
1068 	}
1069 
1070 	/** Registers a local directory to search for packages to use for satisfying
1071 		dependencies.
1072 
1073 		Params:
1074 			path = Path to a directory containing package directories
1075 			system = Make the package known system wide instead of user wide
1076 				(requires administrator privileges).
1077 
1078 		See_Also: `removeSearchPath`
1079 	*/
1080 	void addSearchPath(string path, bool system)
1081 	{
1082 		if (m_dryRun) return;
1083 		m_packageManager.addSearchPath(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user);
1084 	}
1085 
1086 	/** Unregisters a local directory search path.
1087 
1088 		Params:
1089 			path = Path to a directory containing package directories
1090 			system = Make the package known system wide instead of user wide
1091 				(requires administrator privileges).
1092 
1093 		See_Also: `addSearchPath`
1094 	*/
1095 	void removeSearchPath(string path, bool system)
1096 	{
1097 		if (m_dryRun) return;
1098 		m_packageManager.removeSearchPath(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user);
1099 	}
1100 
1101 	/** Queries all package suppliers with the given query string.
1102 
1103 		Returns a list of tuples, where the first entry is the human readable
1104 		name of the package supplier and the second entry is the list of
1105 		matched packages.
1106 
1107 		Params:
1108 		  query = the search term to match packages on
1109 
1110 		See_Also: `PackageSupplier.searchPackages`
1111 	*/
1112 	auto searchPackages(string query)
1113 	{
1114 		import std.typecons : Tuple, tuple;
1115 		Tuple!(string, PackageSupplier.SearchResult[])[] results;
1116 		foreach (ps; this.m_packageSuppliers) {
1117 			try
1118 				results ~= tuple(ps.description, ps.searchPackages(query));
1119 			catch (Exception e) {
1120 				logWarn("Searching %s for '%s' failed: %s", ps.description, query, e.msg);
1121 			}
1122 		}
1123 		return results.filter!(tup => tup[1].length);
1124 	}
1125 
1126 	/** Returns a list of all available versions (including branches) for a
1127 		particular package.
1128 
1129 		The list returned is based on the registered package suppliers. Local
1130 		packages are not queried in the search for versions.
1131 
1132 		See_also: `getLatestVersion`
1133 	*/
1134 	Version[] listPackageVersions(string name)
1135 	{
1136 		Version[] versions;
1137 		auto basePackageName = getBasePackageName(name);
1138 		foreach (ps; this.m_packageSuppliers) {
1139 			try versions ~= ps.getVersions(basePackageName);
1140 			catch (Exception e) {
1141 				logWarn("Failed to get versions for package %s on provider %s: %s", name, ps.description, e.msg);
1142 			}
1143 		}
1144 		return versions.sort().uniq.array;
1145 	}
1146 
1147 	/** Returns the latest available version for a particular package.
1148 
1149 		This function returns the latest numbered version of a package. If no
1150 		numbered versions are available, it will return an available branch,
1151 		preferring "~master".
1152 
1153 		Params:
1154 			package_name: The name of the package in question.
1155 			prefer_stable: If set to `true` (the default), returns the latest
1156 				stable version, even if there are newer pre-release versions.
1157 
1158 		See_also: `listPackageVersions`
1159 	*/
1160 	Version getLatestVersion(string package_name, bool prefer_stable = true)
1161 	{
1162 		auto vers = listPackageVersions(package_name);
1163 		enforce(!vers.empty, "Failed to find any valid versions for a package name of '"~package_name~"'.");
1164 		auto final_versions = vers.filter!(v => !v.isBranch && !v.isPreRelease).array;
1165 		if (prefer_stable && final_versions.length) return final_versions[$-1];
1166 		else return vers[$-1];
1167 	}
1168 
1169 	/** Initializes a directory with a package skeleton.
1170 
1171 		Params:
1172 			path = Path of the directory to create the new package in. The
1173 				directory will be created if it doesn't exist.
1174 			deps = List of dependencies to add to the package recipe.
1175 			type = Specifies the type of the application skeleton to use.
1176 			format = Determines the package recipe format to use.
1177 			recipe_callback = Optional callback that can be used to
1178 				customize the recipe before it gets written.
1179 	*/
1180 	void createEmptyPackage(NativePath path, string[] deps, string type,
1181 		PackageFormat format = PackageFormat.sdl,
1182 		scope void delegate(ref PackageRecipe, ref PackageFormat) recipe_callback = null,
1183 		string[] app_args = [])
1184 	{
1185 		if (!path.absolute) path = m_rootPath ~ path;
1186 		path.normalize();
1187 
1188 		VersionRange[string] depVers;
1189 		string[] notFound; // keep track of any failed packages in here
1190 		foreach (dep; deps) {
1191 			try {
1192 				Version ver = getLatestVersion(dep);
1193 				if (ver.isBranch())
1194 					depVers[dep] = VersionRange(ver);
1195 				else
1196 					depVers[dep] = VersionRange.fromString("~>" ~ ver.toString());
1197 			} catch (Exception e) {
1198 				notFound ~= dep;
1199 			}
1200 		}
1201 
1202 		if(notFound.length > 1){
1203 			throw new Exception(.format("Couldn't find packages: %-(%s, %).", notFound));
1204 		}
1205 		else if(notFound.length == 1){
1206 			throw new Exception(.format("Couldn't find package: %-(%s, %).", notFound));
1207 		}
1208 
1209 		if (m_dryRun) return;
1210 
1211 		initPackage(path, depVers, type, format, recipe_callback);
1212 
1213 		if (!["vibe.d", "deimos", "minimal"].canFind(type)) {
1214 			runCustomInitialization(path, type, app_args);
1215 		}
1216 
1217 		//Act smug to the user.
1218 		logInfo("Success", Color.green, "created empty project in %s", path.toNativeString().color(Mode.bold));
1219 	}
1220 
1221 	private void runCustomInitialization(NativePath path, string type, string[] runArgs)
1222 	{
1223 		string packageName = type;
1224 		auto template_pack = m_packageManager.getBestPackage(packageName);
1225 		if (!template_pack) {
1226 			logInfo("%s is not present, getting and storing it user wide", packageName);
1227 			template_pack = fetch(packageName, VersionRange.Any, defaultPlacementLocation, FetchOptions.none);
1228 		}
1229 
1230 		Package initSubPackage = m_packageManager.getSubPackage(template_pack, "init-exec", false);
1231 		auto template_dub = new Dub(null, m_packageSuppliers);
1232 		template_dub.loadPackage(initSubPackage);
1233 
1234 		GeneratorSettings settings = this.makeAppSettings();
1235 		settings.runArgs = runArgs;
1236 
1237 		initSubPackage.recipe.buildSettings.workingDirectory = path.toNativeString();
1238 		template_dub.generateProject("build", settings);
1239 	}
1240 
1241 	/** Converts the package recipe of the loaded root package to the given format.
1242 
1243 		Params:
1244 			destination_file_ext = The file extension matching the desired
1245 				format. Possible values are "json" or "sdl".
1246 			print_only = Print the converted recipe instead of writing to disk
1247 	*/
1248 	void convertRecipe(string destination_file_ext, bool print_only = false)
1249 	{
1250 		import std.path : extension;
1251 		import std.stdio : stdout;
1252 		import dub.recipe.io : serializePackageRecipe, writePackageRecipe;
1253 
1254 		if (print_only) {
1255 			auto dst = stdout.lockingTextWriter;
1256 			serializePackageRecipe(dst, m_project.rootPackage.rawRecipe, "dub."~destination_file_ext);
1257 			return;
1258 		}
1259 
1260 		auto srcfile = m_project.rootPackage.recipePath;
1261 		auto srcext = srcfile.head.name.extension;
1262 		if (srcext == "."~destination_file_ext) {
1263 			// no logging before this point
1264 			tagWidth.push(5);
1265 			logError("Package format is already %s.", destination_file_ext);
1266 			return;
1267 		}
1268 
1269 		writePackageRecipe(srcfile.parentPath ~ ("dub."~destination_file_ext), m_project.rootPackage.rawRecipe);
1270 		removeFile(srcfile);
1271 	}
1272 
1273 	/** Runs DDOX to generate or serve documentation.
1274 
1275 		Params:
1276 			run = If set to true, serves documentation on a local web server.
1277 				Otherwise generates actual HTML files.
1278 			generate_args = Additional command line arguments to pass to
1279 				"ddox generate-html" or "ddox serve-html".
1280 	*/
1281 	void runDdox(bool run, string[] generate_args = null)
1282 	{
1283 		import std.process : browse;
1284 
1285 		if (m_dryRun) return;
1286 
1287 		// allow to choose a custom ddox tool
1288 		auto tool = m_project.rootPackage.recipe.ddoxTool;
1289 		if (tool.empty) tool = "ddox";
1290 
1291 		auto tool_pack = m_packageManager.getBestPackage(tool);
1292 		if (!tool_pack) {
1293 			logInfo("%s is not present, getting and storing it user wide", tool);
1294 			tool_pack = fetch(tool, VersionRange.Any, defaultPlacementLocation, FetchOptions.none);
1295 		}
1296 
1297 		auto ddox_dub = new Dub(null, m_packageSuppliers);
1298 		ddox_dub.loadPackage(tool_pack);
1299 		ddox_dub.upgrade(UpgradeOptions.select);
1300 
1301 		GeneratorSettings settings = this.makeAppSettings();
1302 
1303 		auto filterargs = m_project.rootPackage.recipe.ddoxFilterArgs.dup;
1304 		if (filterargs.empty) filterargs = ["--min-protection=Protected", "--only-documented"];
1305 
1306 		settings.runArgs = "filter" ~ filterargs ~ "docs.json";
1307 		ddox_dub.generateProject("build", settings);
1308 
1309 		auto p = tool_pack.path;
1310 		p.endsWithSlash = true;
1311 		auto tool_path = p.toNativeString();
1312 
1313 		if (run) {
1314 			settings.runArgs = ["serve-html", "--navigation-type=ModuleTree", "docs.json", "--web-file-dir="~tool_path~"public"] ~ generate_args;
1315 			browse("http://127.0.0.1:8080/");
1316 		} else {
1317 			settings.runArgs = ["generate-html", "--navigation-type=ModuleTree", "docs.json", "docs"] ~ generate_args;
1318 		}
1319 		ddox_dub.generateProject("build", settings);
1320 
1321 		if (!run) {
1322 			// TODO: ddox should copy those files itself
1323 			version(Windows) runCommand(`xcopy /S /D "`~tool_path~`public\*" docs\`, null, m_rootPath.toNativeString());
1324 			else runCommand("rsync -ru '"~tool_path~"public/' docs/", null, m_rootPath.toNativeString());
1325 		}
1326 	}
1327 
1328 	/**
1329 	 * Compute and returns the path were artifacts are stored
1330 	 *
1331 	 * Expose `dub.generator.generator : packageCache` with this instance's
1332 	 * configured cache.
1333 	 */
1334 	protected NativePath packageCache (Package pkg) const
1335 	{
1336 		return .packageCache(this.m_dirs.cache, pkg);
1337 	}
1338 
1339 	/// Exposed because `commandLine` replicates `generateProject` for `dub describe`
1340 	/// instead of treating it like a regular generator... Remove this once the
1341 	/// flaw is fixed, and don't add more calls to this function!
1342 	package(dub) NativePath cachePathDontUse () const @safe pure nothrow @nogc
1343 	{
1344 		return this.m_dirs.cache;
1345 	}
1346 
1347 	/// Make a `GeneratorSettings` suitable to generate tools (DDOC, DScanner, etc...)
1348 	private GeneratorSettings makeAppSettings () const
1349 	{
1350 		GeneratorSettings settings;
1351 		auto compiler_binary = this.defaultCompiler;
1352 
1353 		settings.config = "application";
1354 		settings.buildType = "debug";
1355 		settings.compiler = getCompiler(compiler_binary);
1356 		settings.platform = settings.compiler.determinePlatform(
1357 			settings.buildSettings, compiler_binary, this.defaultArchitecture);
1358 		if (this.defaultLowMemory)
1359 			settings.buildSettings.options |= BuildOption.lowmem;
1360 		if (this.defaultEnvironments)
1361 			settings.buildSettings.addEnvironments(this.defaultEnvironments);
1362 		if (this.defaultBuildEnvironments)
1363 			settings.buildSettings.addBuildEnvironments(this.defaultBuildEnvironments);
1364 		if (this.defaultRunEnvironments)
1365 			settings.buildSettings.addRunEnvironments(this.defaultRunEnvironments);
1366 		if (this.defaultPreGenerateEnvironments)
1367 			settings.buildSettings.addPreGenerateEnvironments(this.defaultPreGenerateEnvironments);
1368 		if (this.defaultPostGenerateEnvironments)
1369 			settings.buildSettings.addPostGenerateEnvironments(this.defaultPostGenerateEnvironments);
1370 		if (this.defaultPreBuildEnvironments)
1371 			settings.buildSettings.addPreBuildEnvironments(this.defaultPreBuildEnvironments);
1372 		if (this.defaultPostBuildEnvironments)
1373 			settings.buildSettings.addPostBuildEnvironments(this.defaultPostBuildEnvironments);
1374 		if (this.defaultPreRunEnvironments)
1375 			settings.buildSettings.addPreRunEnvironments(this.defaultPreRunEnvironments);
1376 		if (this.defaultPostRunEnvironments)
1377 			settings.buildSettings.addPostRunEnvironments(this.defaultPostRunEnvironments);
1378 		settings.run = true;
1379 		settings.overrideToolWorkingDirectory = m_rootPath;
1380 
1381 		return settings;
1382 	}
1383 
1384 	private void determineDefaultCompiler()
1385 	{
1386 		import std.file : thisExePath;
1387 		import std.path : buildPath, dirName, expandTilde, isAbsolute, isDirSeparator;
1388 		import std.range : front;
1389 
1390 		// Env takes precedence
1391 		if (auto envCompiler = environment.get("DC"))
1392 			m_defaultCompiler = envCompiler;
1393 		else
1394 			m_defaultCompiler = m_config.defaultCompiler.expandTilde;
1395 		if (m_defaultCompiler.length && m_defaultCompiler.isAbsolute)
1396 			return;
1397 
1398 		static immutable BinaryPrefix = `$DUB_BINARY_PATH`;
1399 		if(m_defaultCompiler.startsWith(BinaryPrefix))
1400 		{
1401 			m_defaultCompiler = thisExePath().dirName() ~ m_defaultCompiler[BinaryPrefix.length .. $];
1402 			return;
1403 		}
1404 
1405 		if (!find!isDirSeparator(m_defaultCompiler).empty)
1406 			throw new Exception("defaultCompiler specified in a DUB config file cannot use an unqualified relative path:\n\n" ~ m_defaultCompiler ~
1407 			"\n\nUse \"$DUB_BINARY_PATH/../path/you/want\" instead.");
1408 
1409 		version (Windows) enum sep = ";", exe = ".exe";
1410 		version (Posix) enum sep = ":", exe = "";
1411 
1412 		auto compilers = ["dmd", "gdc", "gdmd", "ldc2", "ldmd2"];
1413 		// If a compiler name is specified, look for it next to dub.
1414 		// Otherwise, look for any of the common compilers adjacent to dub.
1415 		if (m_defaultCompiler.length)
1416 		{
1417 			string compilerPath = buildPath(thisExePath().dirName(), m_defaultCompiler ~ exe);
1418 			if (existsFile(compilerPath))
1419 			{
1420 				m_defaultCompiler = compilerPath;
1421 				return;
1422 			}
1423 		}
1424 		else
1425 		{
1426 			auto nextFound = compilers.find!(bin => existsFile(buildPath(thisExePath().dirName(), bin ~ exe)));
1427 			if (!nextFound.empty)
1428 			{
1429 				m_defaultCompiler = buildPath(thisExePath().dirName(),  nextFound.front ~ exe);
1430 				return;
1431 			}
1432 		}
1433 
1434 		// If nothing found next to dub, search the user's PATH, starting
1435 		// with the compiler name from their DUB config file, if specified.
1436 		auto paths = environment.get("PATH", "").splitter(sep).map!NativePath;
1437 		if (m_defaultCompiler.length && paths.canFind!(p => existsFile(p ~ (m_defaultCompiler~exe))))
1438 			return;
1439 		foreach (p; paths) {
1440 			auto res = compilers.find!(bin => existsFile(p ~ (bin~exe)));
1441 			if (!res.empty) {
1442 				m_defaultCompiler = res.front;
1443 				return;
1444 			}
1445 		}
1446 		m_defaultCompiler = compilers[0];
1447 	}
1448 
1449 	unittest
1450 	{
1451 		import std.path: buildPath, absolutePath;
1452 		auto dub = new TestDub(".", null, SkipPackageSuppliers.configured);
1453 		immutable olddc = environment.get("DC", null);
1454 		immutable oldpath = environment.get("PATH", null);
1455 		immutable testdir = "test-determineDefaultCompiler";
1456 		void repairenv(string name, string var)
1457 		{
1458 			if (var !is null)
1459 				environment[name] = var;
1460 			else if (name in environment)
1461 				environment.remove(name);
1462 		}
1463 		scope (exit) repairenv("DC", olddc);
1464 		scope (exit) repairenv("PATH", oldpath);
1465 		scope (exit) rmdirRecurse(testdir);
1466 
1467 		version (Windows) enum sep = ";", exe = ".exe";
1468 		version (Posix) enum sep = ":", exe = "";
1469 
1470 		immutable dmdpath = testdir.buildPath("dmd", "bin");
1471 		immutable ldcpath = testdir.buildPath("ldc", "bin");
1472 		mkdirRecurse(dmdpath);
1473 		mkdirRecurse(ldcpath);
1474 		immutable dmdbin = dmdpath.buildPath("dmd"~exe);
1475 		immutable ldcbin = ldcpath.buildPath("ldc2"~exe);
1476 		std.file.write(dmdbin, null);
1477 		std.file.write(ldcbin, null);
1478 
1479 		environment["DC"] = dmdbin.absolutePath();
1480 		dub.determineDefaultCompiler();
1481 		assert(dub.m_defaultCompiler == dmdbin.absolutePath());
1482 
1483 		environment["DC"] = "dmd";
1484 		environment["PATH"] = dmdpath ~ sep ~ ldcpath;
1485 		dub.determineDefaultCompiler();
1486 		assert(dub.m_defaultCompiler == "dmd");
1487 
1488 		environment["DC"] = "ldc2";
1489 		environment["PATH"] = dmdpath ~ sep ~ ldcpath;
1490 		dub.determineDefaultCompiler();
1491 		assert(dub.m_defaultCompiler == "ldc2");
1492 
1493 		environment.remove("DC");
1494 		environment["PATH"] = ldcpath ~ sep ~ dmdpath;
1495 		dub.determineDefaultCompiler();
1496 		assert(dub.m_defaultCompiler == "ldc2");
1497 	}
1498 
1499 	private NativePath makeAbsolute(NativePath p) const { return p.absolute ? p : m_rootPath ~ p; }
1500 	private NativePath makeAbsolute(string p) const { return makeAbsolute(NativePath(p)); }
1501 }
1502 
1503 
1504 /// Option flags for `Dub.fetch`
1505 enum FetchOptions
1506 {
1507 	none = 0,
1508 	forceBranchUpgrade = 1<<0,
1509 	usePrerelease = 1<<1,
1510 	forceRemove = 1<<2, /// Deprecated, does nothing.
1511 	printOnly = 1<<3,
1512 }
1513 
1514 /// Option flags for `Dub.upgrade`
1515 enum UpgradeOptions
1516 {
1517 	none = 0,
1518 	upgrade = 1<<1, /// Upgrade existing packages
1519 	preRelease = 1<<2, /// inclde pre-release versions in upgrade
1520 	forceRemove = 1<<3, /// Deprecated, does nothing.
1521 	select = 1<<4, /// Update the dub.selections.json file with the upgraded versions
1522 	dryRun = 1<<5, /// Instead of downloading new packages, just print a message to notify the user of their existence
1523 	/*deprecated*/ printUpgradesOnly = dryRun, /// deprecated, use dryRun instead
1524 	/*deprecated*/ useCachedResult = 1<<6, /// deprecated, has no effect
1525 	noSaveSelections = 1<<7, /// Don't store updated selections on disk
1526 }
1527 
1528 /// Determines which of the default package suppliers are queried for packages.
1529 public alias SkipPackageSuppliers = SPS;
1530 
1531 private class DependencyVersionResolver : DependencyResolver!(Dependency, Dependency) {
1532 	protected {
1533 		Dub m_dub;
1534 		UpgradeOptions m_options;
1535 		Dependency[][string] m_packageVersions;
1536 		Package[string] m_remotePackages;
1537 		SelectedVersions m_selectedVersions;
1538 		Package m_rootPackage;
1539 		bool[string] m_packagesToUpgrade;
1540 		Package[PackageDependency] m_packages;
1541 		TreeNodes[][TreeNode] m_children;
1542 	}
1543 
1544 
1545 	this(Dub dub, UpgradeOptions options, Package root, SelectedVersions selected_versions)
1546 	{
1547 		assert(dub !is null);
1548 		assert(root !is null);
1549 		assert(selected_versions !is null);
1550 
1551 		if (environment.get("DUB_NO_RESOLVE_LIMIT") !is null)
1552 			super(ulong.max);
1553 		else
1554 		    super(1_000_000);
1555 
1556 		m_dub = dub;
1557 		m_options = options;
1558 		m_rootPackage = root;
1559 		m_selectedVersions = selected_versions;
1560 	}
1561 
1562 	Dependency[string] resolve(string[] filter)
1563 	{
1564 		foreach (name; filter)
1565 			m_packagesToUpgrade[name] = true;
1566 		return super.resolve(TreeNode(m_rootPackage.name, Dependency(m_rootPackage.version_)),
1567 			(m_options & UpgradeOptions.dryRun) == 0);
1568 	}
1569 
1570 	protected bool isFixedPackage(string pack)
1571 	{
1572 		return m_packagesToUpgrade !is null && pack !in m_packagesToUpgrade;
1573 	}
1574 
1575 	protected override Dependency[] getAllConfigs(string pack)
1576 	{
1577 		if (auto pvers = pack in m_packageVersions)
1578 			return *pvers;
1579 
1580 		if ((!(m_options & UpgradeOptions.upgrade) || isFixedPackage(pack)) && m_selectedVersions.hasSelectedVersion(pack)) {
1581 			auto ret = [m_selectedVersions.getSelectedVersion(pack)];
1582 			logDiagnostic("Using fixed selection %s %s", pack, ret[0]);
1583 			m_packageVersions[pack] = ret;
1584 			return ret;
1585 		}
1586 
1587 		logDiagnostic("Search for versions of %s (%s package suppliers)", pack, m_dub.m_packageSuppliers.length);
1588 		Version[] versions;
1589 		foreach (p; m_dub.packageManager.getPackageIterator(pack))
1590 			versions ~= p.version_;
1591 
1592 		foreach (ps; m_dub.m_packageSuppliers) {
1593 			try {
1594 				auto vers = ps.getVersions(pack);
1595 				vers.reverse();
1596 				if (!vers.length) {
1597 					logDiagnostic("No versions for %s for %s", pack, ps.description);
1598 					continue;
1599 				}
1600 
1601 				versions ~= vers;
1602 				break;
1603 			} catch (Exception e) {
1604 				logWarn("Package %s not found in %s: %s", pack, ps.description, e.msg);
1605 				logDebug("Full error: %s", e.toString().sanitize);
1606 			}
1607 		}
1608 
1609 		// sort by version, descending, and remove duplicates
1610 		versions = versions.sort!"a>b".uniq.array;
1611 
1612 		// move pre-release versions to the back of the list if no preRelease flag is given
1613 		if (!(m_options & UpgradeOptions.preRelease))
1614 			versions = versions.filter!(v => !v.isPreRelease).array ~ versions.filter!(v => v.isPreRelease).array;
1615 
1616 		// filter out invalid/unreachable dependency specs
1617 		versions = versions.filter!((v) {
1618 				bool valid = getPackage(pack, Dependency(v)) !is null;
1619 				if (!valid) logDiagnostic("Excluding invalid dependency specification %s %s from dependency resolution process.", pack, v);
1620 				return valid;
1621 			}).array;
1622 
1623 		if (!versions.length) logDiagnostic("Nothing found for %s", pack);
1624 		else logDiagnostic("Return for %s: %s", pack, versions);
1625 
1626 		auto ret = versions.map!(v => Dependency(v)).array;
1627 		m_packageVersions[pack] = ret;
1628 		return ret;
1629 	}
1630 
1631 	protected override Dependency[] getSpecificConfigs(string pack, TreeNodes nodes)
1632 	{
1633 		if (!nodes.configs.path.empty || !nodes.configs.repository.empty) {
1634 			if (getPackage(pack, nodes.configs)) return [nodes.configs];
1635 			else return null;
1636 		}
1637 		else return null;
1638 	}
1639 
1640 
1641 	protected override TreeNodes[] getChildren(TreeNode node)
1642 	{
1643 		if (auto pc = node in m_children)
1644 			return *pc;
1645 		auto ret = getChildrenRaw(node);
1646 		m_children[node] = ret;
1647 		return ret;
1648 	}
1649 
1650 	private final TreeNodes[] getChildrenRaw(TreeNode node)
1651 	{
1652 		import std.array : appender;
1653 		auto ret = appender!(TreeNodes[]);
1654 		auto pack = getPackage(node.pack, node.config);
1655 		if (!pack) {
1656 			// this can hapen when the package description contains syntax errors
1657 			logDebug("Invalid package in dependency tree: %s %s", node.pack, node.config);
1658 			return null;
1659 		}
1660 		auto basepack = pack.basePackage;
1661 
1662 		foreach (d; pack.getAllDependenciesRange()) {
1663 			auto dbasename = getBasePackageName(d.name);
1664 
1665 			// detect dependencies to the root package (or sub packages thereof)
1666 			if (dbasename == basepack.name) {
1667 				auto absdeppath = d.spec.mapToPath(pack.path).path;
1668 				absdeppath.endsWithSlash = true;
1669 				auto subpack = m_dub.m_packageManager.getSubPackage(basepack, getSubPackageName(d.name), true);
1670 				if (subpack) {
1671 					auto desireddeppath = basepack.path;
1672 					desireddeppath.endsWithSlash = true;
1673 
1674 					auto altdeppath = d.name == dbasename ? basepack.path : subpack.path;
1675 					altdeppath.endsWithSlash = true;
1676 
1677 					if (!d.spec.path.empty && absdeppath != desireddeppath)
1678 						logWarn("Sub package %s, referenced by %s %s must be referenced using the path to its base package",
1679 							subpack.name, pack.name, pack.version_);
1680 
1681 					enforce(d.spec.path.empty || absdeppath == desireddeppath || absdeppath == altdeppath,
1682 						format("Dependency from %s to %s uses wrong path: %s vs. %s",
1683 							node.pack, subpack.name, absdeppath.toNativeString(), desireddeppath.toNativeString()));
1684 				}
1685 				ret ~= TreeNodes(d.name, node.config);
1686 				continue;
1687 			}
1688 
1689 			DependencyType dt;
1690 			if (d.spec.optional) {
1691 				if (d.spec.default_) dt = DependencyType.optionalDefault;
1692 				else dt = DependencyType.optional;
1693 			} else dt = DependencyType.required;
1694 
1695 			Dependency dspec = d.spec.mapToPath(pack.path);
1696 
1697 			// if not upgrading, use the selected version
1698 			if (!(m_options & UpgradeOptions.upgrade) && m_selectedVersions.hasSelectedVersion(dbasename))
1699 				dspec = m_selectedVersions.getSelectedVersion(dbasename);
1700 
1701 			// keep selected optional dependencies and avoid non-selected optional-default dependencies by default
1702 			if (!m_selectedVersions.bare) {
1703 				if (dt == DependencyType.optionalDefault && !m_selectedVersions.hasSelectedVersion(dbasename))
1704 					dt = DependencyType.optional;
1705 				else if (dt == DependencyType.optional && m_selectedVersions.hasSelectedVersion(dbasename))
1706 					dt = DependencyType.optionalDefault;
1707 			}
1708 
1709 			ret ~= TreeNodes(d.name, dspec, dt);
1710 		}
1711 		return ret.data;
1712 	}
1713 
1714 	protected override bool matches(Dependency configs, Dependency config)
1715 	{
1716 		if (!configs.path.empty) return configs.path == config.path;
1717 		return configs.merge(config).valid;
1718 	}
1719 
1720 	private Package getPackage(string name, Dependency dep)
1721 	{
1722 		auto key = PackageDependency(name, dep);
1723 		if (auto pp = key in m_packages)
1724 			return *pp;
1725 		auto p = getPackageRaw(name, dep);
1726 		m_packages[key] = p;
1727 		return p;
1728 	}
1729 
1730 	private Package getPackageRaw(string name, Dependency dep)
1731 	{
1732 		auto basename = getBasePackageName(name);
1733 
1734 		// for sub packages, first try to get them from the base package
1735 		if (basename != name) {
1736 			auto subname = getSubPackageName(name);
1737 			auto basepack = getPackage(basename, dep);
1738 			if (!basepack) return null;
1739 			if (auto sp = m_dub.m_packageManager.getSubPackage(basepack, subname, true)) {
1740 				return sp;
1741 			} else if (!basepack.subPackages.canFind!(p => p.path.length)) {
1742 				// note: external sub packages are handled further below
1743 				auto spr = basepack.getInternalSubPackage(subname);
1744 				if (!spr.isNull) {
1745 					auto sp = new Package(spr.get, basepack.path, basepack);
1746 					m_remotePackages[sp.name] = sp;
1747 					return sp;
1748 				} else {
1749 					logDiagnostic("Sub package %s doesn't exist in %s %s.", name, basename, dep.version_);
1750 					return null;
1751 				}
1752 			} else {
1753 				logDiagnostic("External sub package %s %s not found.", name, dep.version_);
1754 				return null;
1755 			}
1756 		}
1757 
1758 		// shortcut if the referenced package is the root package
1759 		if (basename == m_rootPackage.basePackage.name)
1760 			return m_rootPackage.basePackage;
1761 
1762 		if (!dep.repository.empty) {
1763 			auto ret = m_dub.packageManager.loadSCMPackage(name, dep.repository);
1764 			return ret !is null && dep.matches(ret.version_) ? ret : null;
1765 		} else if (!dep.path.empty) {
1766 			try {
1767 				return m_dub.packageManager.getOrLoadPackage(dep.path);
1768 			} catch (Exception e) {
1769 				logDiagnostic("Failed to load path based dependency %s: %s", name, e.msg);
1770 				logDebug("Full error: %s", e.toString().sanitize);
1771 				return null;
1772 			}
1773 		}
1774 		const vers = dep.version_;
1775 
1776 		if (auto ret = m_dub.m_packageManager.getBestPackage(name, vers))
1777 			return ret;
1778 
1779 		auto key = name ~ ":" ~ vers.toString();
1780 		if (auto ret = key in m_remotePackages)
1781 			return *ret;
1782 
1783 		auto prerelease = (m_options & UpgradeOptions.preRelease) != 0;
1784 
1785 		auto rootpack = name.split(":")[0];
1786 
1787 		foreach (ps; m_dub.m_packageSuppliers) {
1788 			if (rootpack == name) {
1789 				try {
1790 					auto desc = ps.fetchPackageRecipe(name, dep, prerelease);
1791 					if (desc.type == Json.Type.null_)
1792 						continue;
1793 					auto ret = new Package(desc);
1794 					m_remotePackages[key] = ret;
1795 					return ret;
1796 				} catch (Exception e) {
1797 					logDiagnostic("Metadata for %s %s could not be downloaded from %s: %s", name, vers, ps.description, e.msg);
1798 					logDebug("Full error: %s", e.toString().sanitize);
1799 				}
1800 			} else {
1801 				logDiagnostic("Package %s not found in base package description (%s). Downloading whole package.", name, vers.toString());
1802 				try {
1803 					FetchOptions fetchOpts;
1804 					fetchOpts |= prerelease ? FetchOptions.usePrerelease : FetchOptions.none;
1805 					m_dub.fetch(rootpack, vers, m_dub.defaultPlacementLocation, fetchOpts, "need sub package description");
1806 					auto ret = m_dub.m_packageManager.getBestPackage(name, vers);
1807 					if (!ret) {
1808 						logWarn("Package %s %s doesn't have a sub package %s", rootpack, dep.version_, name);
1809 						return null;
1810 					}
1811 					m_remotePackages[key] = ret;
1812 					return ret;
1813 				} catch (Exception e) {
1814 					logDiagnostic("Package %s could not be downloaded from %s: %s", rootpack, ps.description, e.msg);
1815 					logDebug("Full error: %s", e.toString().sanitize);
1816 				}
1817 			}
1818 		}
1819 
1820 		m_remotePackages[key] = null;
1821 
1822 		logWarn("Package %s %s could not be loaded either locally, or from the configured package registries.", name, dep);
1823 		return null;
1824 	}
1825 }
1826 
1827 /**
1828  * An instance of Dub that does not rely on the environment
1829  *
1830  * This instance of dub should not read any environment variables,
1831  * nor should it do any file IO, to make it usable and reliable in unittests.
1832  * Currently it reads environment variables but does not read the configuration.
1833  */
1834 package final class TestDub : Dub
1835 {
1836     /// Forward to base constructor
1837     public this (string root = ".", PackageSupplier[] extras = null,
1838                  SkipPackageSuppliers skip = SkipPackageSuppliers.none)
1839     {
1840         super(root, extras, skip);
1841     }
1842 
1843     /// Avoid loading user configuration
1844     protected override void loadConfig() { /* No-op */ }
1845 }
1846 
1847 private struct SpecialDirs {
1848 	/// The path where to store temporary files and directory
1849 	NativePath temp;
1850 	/// The system-wide dub-specific folder
1851 	NativePath systemSettings;
1852 	/// The dub-specific folder in the user home directory
1853 	NativePath userSettings;
1854 	/**
1855 	 * User location where to install packages
1856 	 *
1857 	 * On Windows, this folder, unlike `userSettings`, does not roam,
1858 	 * so an account on a company network will not save the content of this data,
1859 	 * unlike `userSettings`.
1860 	 *
1861 	 * On Posix, this is currently equivalent to `userSettings`.
1862 	 *
1863 	 * See_Also: https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid
1864 	 */
1865 	NativePath userPackages;
1866 
1867 	/**
1868 	 * Location at which build/generation artifact will be written
1869 	 *
1870 	 * All build artifacts are stored under a single build cache,
1871 	 * which is usually located under `$HOME/.dub/cache/` on POSIX,
1872 	 * and `%LOCALAPPDATA%/dub/cache` on Windows.
1873 	 *
1874 	 * Versions of dub prior to v1.31.0 used to store  artifact under the
1875 	 * project directory, but this led to issues with packages stored on
1876 	 * read-only filesystem / location, and lingering artifacts scattered
1877 	 * through the filesystem.
1878 	 */
1879 	NativePath cache;
1880 
1881 	/// Returns: An instance of `SpecialDirs` initialized from the environment
1882 	public static SpecialDirs make () {
1883 		import std.file : tempDir;
1884 
1885 		SpecialDirs result;
1886 		result.temp = NativePath(tempDir);
1887 
1888 		version(Windows) {
1889 			result.systemSettings = NativePath(environment.get("ProgramData")) ~ "dub/";
1890 			immutable appDataDir = environment.get("APPDATA");
1891 			result.userSettings = NativePath(appDataDir) ~ "dub/";
1892 			// LOCALAPPDATA is not defined before Windows Vista
1893 			result.userPackages = NativePath(environment.get("LOCALAPPDATA", appDataDir)) ~ "dub";
1894 		} else version(Posix) {
1895 			result.systemSettings = NativePath("/var/lib/dub/");
1896 			result.userSettings = NativePath(environment.get("HOME")) ~ ".dub/";
1897 			if (!result.userSettings.absolute)
1898 				result.userSettings = getWorkingDirectory() ~ result.userSettings;
1899 			result.userPackages = result.userSettings;
1900 		}
1901 		result.cache = result.userPackages ~ "cache";
1902 		return result;
1903 	}
1904 }