1 /**
2 	Representing a full project, with a root Package and several dependencies.
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.project;
9 
10 import dub.compilers.compiler;
11 import dub.dependency;
12 import dub.description;
13 import dub.generators.generator;
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.path;
18 import dub.internal.logging;
19 import dub.package_;
20 import dub.packagemanager;
21 import dub.recipe.selection;
22 
23 import dub.internal.configy.Read;
24 
25 import std.algorithm;
26 import std.array;
27 import std.conv : to;
28 import std.datetime;
29 import std.encoding : sanitize;
30 import std.exception : enforce;
31 import std.string;
32 
33 /**
34 	Represents a full project, a root package with its dependencies and package
35 	selection.
36 
37 	All dependencies must be available locally so that the package dependency
38 	graph can be built. Use `Project.reinit` if necessary for reloading
39 	dependencies after more packages are available.
40 */
41 class Project {
42 	private {
43 		PackageManager m_packageManager;
44 		Package m_rootPackage;
45 		Package[] m_dependencies;
46 		Package[][Package] m_dependees;
47 		SelectedVersions m_selections;
48 		string[] m_missingDependencies;
49 		string[string] m_overriddenConfigs;
50 	}
51 
52 	/** Loads a project.
53 
54 		Params:
55 			package_manager = Package manager instance to use for loading
56 				dependencies
57 			project_path = Path of the root package to load
58 			pack = An existing `Package` instance to use as the root package
59 	*/
60 	deprecated("Load the package using `PackageManager.getOrLoadPackage` then call the `(PackageManager, Package)` overload")
61 	this(PackageManager package_manager, NativePath project_path)
62 	{
63 		Package pack;
64 		auto packageFile = Package.findPackageFile(project_path);
65 		if (packageFile.empty) {
66 			logWarn("There was no package description found for the application in '%s'.", project_path.toNativeString());
67 			pack = new Package(PackageRecipe.init, project_path);
68 		} else {
69 			pack = package_manager.getOrLoadPackage(project_path, packageFile, false, StrictMode.Warn);
70 		}
71 
72 		this(package_manager, pack);
73 	}
74 
75 	/// Ditto
76 	this(PackageManager package_manager, Package pack)
77 	{
78 		auto selections = Project.loadSelections(pack);
79 		this(package_manager, pack, selections);
80 	}
81 
82 	/// ditto
83 	this(PackageManager package_manager, Package pack, SelectedVersions selections)
84 	{
85 		m_packageManager = package_manager;
86 		m_rootPackage = pack;
87 		m_selections = selections;
88 		reinit();
89 	}
90 
91 	/**
92 	 * Loads a project's `dub.selections.json` and returns it
93 	 *
94 	 * This function will load `dub.selections.json` from the path at which
95 	 * `pack` is located, and returned the resulting `SelectedVersions`.
96 	 * If no `dub.selections.json` is found, an empty `SelectedVersions`
97 	 * is returned.
98 	 *
99 	 * Params:
100 	 *	 pack = Package to load the selection file from.
101 	 *
102 	 * Returns:
103 	 *	 Always a non-null instance.
104 	 */
105 	static package SelectedVersions loadSelections(in Package pack)
106 	{
107 		import dub.version_;
108 		import dub.internal.dyaml.stdsumtype;
109 
110 		auto selverfile = (pack.path ~ SelectedVersions.defaultFile).toNativeString();
111 
112 		// No file exists
113 		if (!existsFile(selverfile))
114 			return new SelectedVersions();
115 
116 		// TODO: Remove `StrictMode.Warn` after v1.40 release
117 		// The default is to error, but as the previous parser wasn't
118 		// complaining, we should first warn the user.
119 		auto selected = parseConfigFileSimple!SelectionsFile(selverfile, StrictMode.Warn);
120 
121 		// Parsing error, it will be displayed to the user
122 		if (selected.isNull())
123 			return new SelectedVersions();
124 
125 		return selected.get().content.match!(
126 			(Selections!0 s) {
127 				logWarnTag("Unsupported version",
128 					"File %s has fileVersion %s, which is not yet supported by DUB %s.",
129 					selverfile, s.fileVersion, dubVersion);
130 				logWarn("Ignoring selections file. Use a newer DUB version " ~
131 					"and set the appropriate toolchainRequirements in your recipe file");
132 				return new SelectedVersions();
133 			},
134 			(Selections!1 s) => new SelectedVersions(s),
135 		);
136 	}
137 
138 	/** List of all resolved dependencies.
139 
140 		This includes all direct and indirect dependencies of all configurations
141 		combined. Optional dependencies that were not chosen are not included.
142 	*/
143 	@property const(Package[]) dependencies() const { return m_dependencies; }
144 
145 	/// The root package of the project.
146 	@property inout(Package) rootPackage() inout { return m_rootPackage; }
147 
148 	/// The versions to use for all dependencies. Call reinit() after changing these.
149 	@property inout(SelectedVersions) selections() inout { return m_selections; }
150 
151 	/// Package manager instance used by the project.
152 	deprecated("Use `Dub.packageManager` instead")
153 	@property inout(PackageManager) packageManager() inout { return m_packageManager; }
154 
155 	/** Determines if all dependencies necessary to build have been collected.
156 
157 		If this function returns `false`, it may be necessary to add more entries
158 		to `selections`, or to use `Dub.upgrade` to automatically select all
159 		missing dependencies.
160 	*/
161 	bool hasAllDependencies() const { return m_missingDependencies.length == 0; }
162 
163 	/// Sorted list of missing dependencies.
164 	string[] missingDependencies() { return m_missingDependencies; }
165 
166 	/** Allows iteration of the dependency tree in topological order
167 	*/
168 	int delegate(int delegate(ref Package)) getTopologicalPackageList(bool children_first = false, Package root_package = null, string[string] configs = null)
169 	{
170 		// ugly way to avoid code duplication since inout isn't compatible with foreach type inference
171 		return cast(int delegate(int delegate(ref Package)))(cast(const)this).getTopologicalPackageList(children_first, root_package, configs);
172 	}
173 	/// ditto
174 	int delegate(int delegate(ref const Package)) getTopologicalPackageList(bool children_first = false, in Package root_package = null, string[string] configs = null)
175 	const {
176 		const(Package) rootpack = root_package ? root_package : m_rootPackage;
177 
178 		int iterator(int delegate(ref const Package) del)
179 		{
180 			int ret = 0;
181 			bool[const(Package)] visited;
182 			void perform_rec(in Package p){
183 				if( p in visited ) return;
184 				visited[p] = true;
185 
186 				if( !children_first ){
187 					ret = del(p);
188 					if( ret ) return;
189 				}
190 
191 				auto cfg = configs.get(p.name, null);
192 
193 				PackageDependency[] deps;
194 				if (!cfg.length) deps = p.getAllDependencies();
195 				else {
196 					auto depmap = p.getDependencies(cfg);
197 					deps = depmap.byKey.map!(k => PackageDependency(PackageName(k), depmap[k])).array;
198 				}
199 				deps.sort!((a, b) => a.name.toString() < b.name.toString());
200 
201 				foreach (d; deps) {
202 					auto dependency = getDependency(d.name.toString(), true);
203 					assert(dependency || d.spec.optional,
204 						format("Non-optional dependency '%s' of '%s' not found in dependency tree!?.", d.name, p.name));
205 					if(dependency) perform_rec(dependency);
206 					if( ret ) return;
207 				}
208 
209 				if( children_first ){
210 					ret = del(p);
211 					if( ret ) return;
212 				}
213 			}
214 			perform_rec(rootpack);
215 			return ret;
216 		}
217 
218 		return &iterator;
219 	}
220 
221 	/** Retrieves a particular dependency by name.
222 
223 		Params:
224 			name = (Qualified) package name of the dependency
225 			is_optional = If set to true, will return `null` for unsatisfiable
226 				dependencies instead of throwing an exception.
227 	*/
228 	inout(Package) getDependency(string name, bool is_optional)
229 	inout {
230 		foreach(dp; m_dependencies)
231 			if( dp.name == name )
232 				return dp;
233 		if (!is_optional) throw new Exception("Unknown dependency: "~name);
234 		else return null;
235 	}
236 
237 	/** Returns the name of the default build configuration for the specified
238 		target platform.
239 
240 		Params:
241 			platform = The target build platform
242 			allow_non_library_configs = If set to true, will use the first
243 				possible configuration instead of the first "executable"
244 				configuration.
245 	*/
246 	string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library_configs = true)
247 	const {
248 		auto cfgs = getPackageConfigs(platform, null, allow_non_library_configs);
249 		return cfgs[m_rootPackage.name];
250 	}
251 
252 	/** Overrides the configuration chosen for a particular package in the
253 		dependency graph.
254 
255 		Setting a certain configuration here is equivalent to removing all
256 		but one configuration from the package.
257 
258 		Params:
259 			package_ = The package for which to force selecting a certain
260 				dependency
261 			config = Name of the configuration to force
262 	*/
263 	void overrideConfiguration(string package_, string config)
264 	{
265 		auto p = getDependency(package_, true);
266 		enforce(p !is null,
267 			format("Package '%s', marked for configuration override, is not present in dependency graph.", package_));
268 		enforce(p.configurations.canFind(config),
269 			format("Package '%s' does not have a configuration named '%s'.", package_, config));
270 		m_overriddenConfigs[package_] = config;
271 	}
272 
273 	/** Adds a test runner configuration for the root package.
274 
275 		Params:
276 			settings = The generator settings to use
277 			generate_main = Whether to generate the main.d file
278 			base_config = Optional base configuration
279 			custom_main_file = Optional path to file with custom main entry point
280 
281 		Returns:
282 			Name of the added test runner configuration, or null for base configurations with target type `none`
283 	*/
284 	string addTestRunnerConfiguration(in GeneratorSettings settings, bool generate_main = true, string base_config = "", NativePath custom_main_file = NativePath())
285 	{
286 		if (base_config.length == 0) {
287 			// if a custom main file was given, favor the first library configuration, so that it can be applied
288 			if (!custom_main_file.empty) base_config = getDefaultConfiguration(settings.platform, false);
289 			// else look for a "unittest" configuration
290 			if (!base_config.length && rootPackage.configurations.canFind("unittest")) base_config = "unittest";
291 			// if not found, fall back to the first "library" configuration
292 			if (!base_config.length) base_config = getDefaultConfiguration(settings.platform, false);
293 			// if still nothing found, use the first executable configuration
294 			if (!base_config.length) base_config = getDefaultConfiguration(settings.platform, true);
295 		}
296 
297 		BuildSettings lbuildsettings = settings.buildSettings.dup;
298 		addBuildSettings(lbuildsettings, settings, base_config, null, true);
299 
300 		if (lbuildsettings.targetType == TargetType.none) {
301 			logInfo(`Configuration '%s' has target type "none". Skipping test runner configuration.`, base_config);
302 			return null;
303 		}
304 
305 		if (lbuildsettings.targetType == TargetType.executable && base_config == "unittest") {
306 			if (!custom_main_file.empty) logWarn("Ignoring custom main file.");
307 			return base_config;
308 		}
309 
310 		if (lbuildsettings.sourceFiles.empty) {
311 			logInfo(`No source files found in configuration '%s'. Falling back to default configuration for test runner.`, base_config);
312 			if (!custom_main_file.empty) logWarn("Ignoring custom main file.");
313 			return getDefaultConfiguration(settings.platform);
314 		}
315 
316 		const config = format("%s-test-%s", rootPackage.name.replace(".", "-").replace(":", "-"), base_config);
317 		logInfo(`Generating test runner configuration '%s' for '%s' (%s).`, config, base_config, lbuildsettings.targetType);
318 
319 		BuildSettingsTemplate tcinfo = rootPackage.recipe.getConfiguration(base_config).buildSettings.dup;
320 		tcinfo.targetType = TargetType.executable;
321 
322 		// set targetName unless specified explicitly in unittest base configuration
323 		if (tcinfo.targetName.empty || base_config != "unittest")
324 			tcinfo.targetName = config;
325 
326 		auto mainfil = tcinfo.mainSourceFile;
327 		if (!mainfil.length) mainfil = rootPackage.recipe.buildSettings.mainSourceFile;
328 
329 		string custommodname;
330 		if (!custom_main_file.empty) {
331 			import std.path;
332 			tcinfo.sourceFiles[""] ~= custom_main_file.relativeTo(rootPackage.path).toNativeString();
333 			tcinfo.importPaths[""] ~= custom_main_file.parentPath.toNativeString();
334 			custommodname = custom_main_file.head.name.baseName(".d");
335 		}
336 
337 		// prepare the list of tested modules
338 
339 		string[] import_modules;
340 		if (settings.single)
341 			lbuildsettings.importPaths ~= NativePath(mainfil).parentPath.toNativeString;
342 		bool firstTimePackage = true;
343 		foreach (file; lbuildsettings.sourceFiles) {
344 			if (file.endsWith(".d")) {
345 				auto fname = NativePath(file).head.name;
346 				NativePath msf = NativePath(mainfil);
347 				if (msf.absolute)
348 					msf = msf.relativeTo(rootPackage.path);
349 				if (!settings.single && NativePath(file).relativeTo(rootPackage.path) == msf) {
350 					logWarn("Excluding main source file %s from test.", mainfil);
351 					tcinfo.excludedSourceFiles[""] ~= mainfil;
352 					continue;
353 				}
354 				if (fname == "package.d") {
355 					if (firstTimePackage) {
356 						firstTimePackage = false;
357 						logDiagnostic("Excluding package.d file from test due to https://issues.dlang.org/show_bug.cgi?id=11847");
358 					}
359 					continue;
360 				}
361 				import_modules ~= dub.internal.utils.determineModuleName(lbuildsettings, NativePath(file), rootPackage.path);
362 			}
363 		}
364 
365 		NativePath mainfile;
366 		if (settings.tempBuild)
367 			mainfile = getTempFile("dub_test_root", ".d");
368 		else {
369 			import dub.generators.build : computeBuildName;
370 			mainfile = packageCache(settings.cache, this.rootPackage) ~
371 				format("code/%s/dub_test_root.d",
372 					computeBuildName(config, settings, import_modules));
373 		}
374 
375 		auto escapedMainFile = mainfile.toNativeString().replace("$", "$$");
376 		tcinfo.sourceFiles[""] ~= escapedMainFile;
377 		tcinfo.mainSourceFile = escapedMainFile;
378 		if (!settings.tempBuild) {
379 			// add the directory containing dub_test_root.d to the import paths
380 			tcinfo.importPaths[""] ~= NativePath(escapedMainFile).parentPath.toNativeString();
381 		}
382 
383 		if (generate_main && (settings.force || !existsFile(mainfile))) {
384 		    ensureDirectory(mainfile.parentPath);
385 
386 			const runnerCode = custommodname.length ?
387 				format("import %s;", custommodname) : DefaultTestRunnerCode;
388 			const content = TestRunnerTemplate.format(
389 				import_modules, import_modules, runnerCode);
390 			writeFile(mainfile, content);
391 		}
392 
393 		rootPackage.recipe.configurations ~= ConfigurationInfo(config, tcinfo);
394 
395 		return config;
396 	}
397 
398 	/** Performs basic validation of various aspects of the package.
399 
400 		This will emit warnings to `stderr` if any discouraged names or
401 		dependency patterns are found.
402 	*/
403 	void validate()
404 	{
405 		bool isSDL = !m_rootPackage.recipePath.empty
406 			&& m_rootPackage.recipePath.head.name.endsWith(".sdl");
407 
408 		// some basic package lint
409 		m_rootPackage.warnOnSpecialCompilerFlags();
410 		string nameSuggestion() {
411 			string ret;
412 			ret ~= `Please modify the "name" field in %s accordingly.`.format(m_rootPackage.recipePath.toNativeString());
413 			if (!m_rootPackage.recipe.buildSettings.targetName.length) {
414 				if (isSDL) {
415 					ret ~= ` You can then add 'targetName "%s"' to keep the current executable name.`.format(m_rootPackage.name);
416 				} else {
417 					ret ~= ` You can then add '"targetName": "%s"' to keep the current executable name.`.format(m_rootPackage.name);
418 				}
419 			}
420 			return ret;
421 		}
422 		if (m_rootPackage.name != m_rootPackage.name.toLower()) {
423 			logWarn(`DUB package names should always be lower case. %s`, nameSuggestion());
424 		} else if (!m_rootPackage.recipe.name.all!(ch => ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '-' || ch == '_')) {
425 			logWarn(`DUB package names may only contain alphanumeric characters, `
426 				~ `as well as '-' and '_'. %s`, nameSuggestion());
427 		}
428 		enforce(!m_rootPackage.name.canFind(' '), "Aborting due to the package name containing spaces.");
429 
430 		foreach (d; m_rootPackage.getAllDependencies())
431 			if (d.spec.isExactVersion && d.spec.version_.isBranch) {
432 				string suggestion = isSDL
433 					? format(`dependency "%s" repository="git+<git url>" version="<commit>"`, d.name)
434 					: format(`"%s": {"repository": "git+<git url>", "version": "<commit>"}`, d.name);
435 				logWarn("Dependency '%s' depends on git branch '%s', which is deprecated.",
436 					d.name.toString().color(Mode.bold),
437 					d.spec.version_.toString.color(Mode.bold));
438 				logWarnTag("", "Specify the git repository and commit hash in your %s:",
439 					(isSDL ? "dub.sdl" : "dub.json").color(Mode.bold));
440 				logWarnTag("", "%s", suggestion.color(Mode.bold));
441 			}
442 
443 		// search for orphan sub configurations
444 		void warnSubConfig(string pack, string config) {
445 			logWarn("The sub configuration directive \"%s\" -> [%s] "
446 				~ "references a package that is not specified as a dependency "
447 				~ "and will have no effect.", pack.color(Mode.bold), config.color(Color.blue));
448 		}
449 
450 		void checkSubConfig(in PackageName name, string config) {
451 			auto p = getDependency(name.toString(), true);
452 			if (p && !p.configurations.canFind(config)) {
453 				logWarn("The sub configuration directive \"%s\" -> [%s] "
454 					~ "references a configuration that does not exist.",
455 					name.toString().color(Mode.bold), config.color(Color.red));
456 			}
457 		}
458 		auto globalbs = m_rootPackage.getBuildSettings();
459 		foreach (p, c; globalbs.subConfigurations) {
460 			if (p !in globalbs.dependencies) warnSubConfig(p, c);
461 			else checkSubConfig(PackageName(p), c);
462 		}
463 		foreach (c; m_rootPackage.configurations) {
464 			auto bs = m_rootPackage.getBuildSettings(c);
465 			foreach (p, subConf; bs.subConfigurations) {
466 				if (p !in bs.dependencies && p !in globalbs.dependencies)
467 					warnSubConfig(p, subConf);
468 				else checkSubConfig(PackageName(p), subConf);
469 			}
470 		}
471 
472 		// check for version specification mismatches
473 		bool[Package] visited;
474 		void validateDependenciesRec(Package pack) {
475 			// perform basic package linting
476 			pack.simpleLint();
477 
478 			foreach (d; pack.getAllDependencies()) {
479 				auto basename = d.name.main;
480 				d.spec.visit!(
481 					(NativePath path) { /* Valid */ },
482 					(Repository repo) { /* Valid */ },
483 					(VersionRange vers) {
484 						if (m_selections.hasSelectedVersion(basename)) {
485 							auto selver = m_selections.getSelectedVersion(basename);
486 							if (d.spec.merge(selver) == Dependency.Invalid) {
487 								logWarn(`Selected package %s@%s does not match ` ~
488 								   `the dependency specification %s in ` ~
489 								   `package %s. Need to "%s"?`,
490 									basename.toString().color(Mode.bold), selver,
491 									vers, pack.name.color(Mode.bold),
492 									"dub upgrade".color(Mode.bold));
493 							}
494 						}
495 					},
496 				);
497 
498 				auto deppack = getDependency(d.name.toString(), true);
499 				if (deppack in visited) continue;
500 				visited[deppack] = true;
501 				if (deppack) validateDependenciesRec(deppack);
502 			}
503 		}
504 		validateDependenciesRec(m_rootPackage);
505 	}
506 
507 	/**
508 	 * Reloads dependencies
509 	 *
510 	 * This function goes through the project and make sure that all
511 	 * required packages are loaded. To do so, it uses information
512 	 * both from the recipe file (`dub.json`) and from the selections
513 	 * file (`dub.selections.json`).
514 	 *
515 	 * In the process, it populates the `dependencies`, `missingDependencies`,
516 	 * and `hasAllDependencies` properties, which can only be relied on
517 	 * once this has run once (the constructor always calls this).
518 	 */
519 	void reinit()
520 	{
521 		m_dependencies = null;
522 		m_missingDependencies = [];
523 		collectDependenciesRec(m_rootPackage);
524 		m_missingDependencies.sort();
525 	}
526 
527 	/// Implementation of `reinit`
528 	private void collectDependenciesRec(Package pack, int depth = 0)
529 	{
530 		auto indent = replicate("  ", depth);
531 		logDebug("%sCollecting dependencies for %s", indent, pack.name);
532 		indent ~= "  ";
533 
534 		foreach (dep; pack.getAllDependencies()) {
535 			Dependency vspec = dep.spec;
536 			Package p;
537 
538 			auto basename = dep.name.main;
539 			auto subname = dep.name.sub;
540 
541 			// non-optional and optional-default dependencies (if no selections file exists)
542 			// need to be satisfied
543 			bool is_desired = !vspec.optional || m_selections.hasSelectedVersion(basename) || (vspec.default_ && m_selections.bare);
544 
545 			if (dep.name.toString() == m_rootPackage.basePackage.name) {
546 				vspec = Dependency(m_rootPackage.version_);
547 				p = m_rootPackage.basePackage;
548 			} else if (basename.toString() == m_rootPackage.basePackage.name) {
549 				vspec = Dependency(m_rootPackage.version_);
550 				try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, subname, false);
551 				catch (Exception e) {
552 					logDiagnostic("%sError getting sub package %s: %s", indent, dep.name, e.msg);
553 					if (is_desired) m_missingDependencies ~= dep.name.toString();
554 					continue;
555 				}
556 			} else if (m_selections.hasSelectedVersion(basename)) {
557 				vspec = m_selections.getSelectedVersion(basename);
558 				p = vspec.visit!(
559 					(NativePath path_) {
560 						auto path = path_.absolute ? path_ : m_rootPackage.path ~ path_;
561 						auto tmp = m_packageManager.getOrLoadPackage(path, NativePath.init, true);
562 						return resolveSubPackage(tmp, subname, true);
563 					},
564 					(Repository repo) {
565 						auto tmp = m_packageManager.loadSCMPackage(basename, repo);
566 						return resolveSubPackage(tmp, subname, true);
567 					},
568 					(VersionRange range) {
569 						// See `dub.recipe.selection : SelectedDependency.fromYAML`
570 						assert(range.isExactVersion());
571 						return m_packageManager.getPackage(dep.name, vspec.version_);
572 					},
573 				);
574 			} else if (m_dependencies.canFind!(d => PackageName(d.name).main == basename)) {
575 				auto idx = m_dependencies.countUntil!(d => PackageName(d.name).main == basename);
576 				auto bp = m_dependencies[idx].basePackage;
577 				vspec = Dependency(bp.path);
578 				p = resolveSubPackage(bp, subname, false);
579 			} else {
580 				logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.",
581 					indent, basename, dep.name, pack.name);
582 			}
583 
584 			// We didn't find the package
585 			if (p is null)
586 			{
587 				if (!vspec.repository.empty) {
588 					p = m_packageManager.loadSCMPackage(basename, vspec.repository);
589 					resolveSubPackage(p, subname, false);
590 					enforce(p !is null,
591 						"Unable to fetch '%s@%s' using git - does the repository and version exists?".format(
592 							dep.name, vspec.repository));
593 				} else if (!vspec.path.empty && is_desired) {
594 					NativePath path = vspec.path;
595 					if (!path.absolute) path = pack.path ~ path;
596 					logDiagnostic("%sAdding local %s in %s", indent, dep.name, path);
597 					p = m_packageManager.getOrLoadPackage(path, NativePath.init, true);
598 					if (p.parentPackage !is null) {
599 						logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name);
600 						p = p.parentPackage;
601 					}
602 					p = resolveSubPackage(p, subname, false);
603 					enforce(p.name == dep.name.toString(),
604 						format("Path based dependency %s is referenced with a wrong name: %s vs. %s",
605 							path.toNativeString(), dep.name, p.name));
606 				} else {
607 					logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name);
608 					if (is_desired) m_missingDependencies ~= dep.name.toString();
609 					continue;
610 				}
611 			}
612 
613 			if (!m_dependencies.canFind(p)) {
614 				logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString());
615 				m_dependencies ~= p;
616 				if (basename.toString() == m_rootPackage.basePackage.name)
617 					p.warnOnSpecialCompilerFlags();
618 				collectDependenciesRec(p, depth+1);
619 			}
620 
621 			m_dependees[p] ~= pack;
622 			//enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString());
623 		}
624 	}
625 
626 	/// Convenience function used by `reinit`
627 	private Package resolveSubPackage(Package p, string subname, bool silentFail) {
628 		if (!subname.length || p is null)
629 			return p;
630 		return m_packageManager.getSubPackage(p, subname, silentFail);
631 	}
632 
633 	/// Returns the name of the root package.
634 	@property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; }
635 
636 	/// Returns the names of all configurations of the root package.
637 	@property string[] configurations() const { return m_rootPackage.configurations; }
638 
639 	/// Returns the names of all built-in and custom build types of the root package.
640 	/// The default built-in build type is the first item in the list.
641 	@property string[] builds() const { return builtinBuildTypes ~ m_rootPackage.customBuildTypes; }
642 
643 	/// Returns a map with the configuration for all packages in the dependency tree.
644 	string[string] getPackageConfigs(in BuildPlatform platform, string config, bool allow_non_library = true)
645 	const {
646 		struct Vertex { string pack, config; }
647 		struct Edge { size_t from, to; }
648 
649 		Vertex[] configs;
650 		Edge[] edges;
651 		string[][string] parents;
652 		parents[m_rootPackage.name] = null;
653 		foreach (p; getTopologicalPackageList())
654 			foreach (d; p.getAllDependencies())
655 				parents[d.name.toString()] ~= p.name;
656 
657 		size_t createConfig(string pack, string config) {
658 			foreach (i, v; configs)
659 				if (v.pack == pack && v.config == config)
660 					return i;
661 			assert(pack !in m_overriddenConfigs || config == m_overriddenConfigs[pack]);
662 			logDebug("Add config %s %s", pack, config);
663 			configs ~= Vertex(pack, config);
664 			return configs.length-1;
665 		}
666 
667 		bool haveConfig(string pack, string config) {
668 			return configs.any!(c => c.pack == pack && c.config == config);
669 		}
670 
671 		size_t createEdge(size_t from, size_t to) {
672 			auto idx = edges.countUntil(Edge(from, to));
673 			if (idx >= 0) return idx;
674 			logDebug("Including %s %s -> %s %s", configs[from].pack, configs[from].config, configs[to].pack, configs[to].config);
675 			edges ~= Edge(from, to);
676 			return edges.length-1;
677 		}
678 
679 		void removeConfig(size_t i) {
680 			logDebug("Eliminating config %s for %s", configs[i].config, configs[i].pack);
681 			auto had_dep_to_pack = new bool[configs.length];
682 			auto still_has_dep_to_pack = new bool[configs.length];
683 
684 			edges = edges.filter!((e) {
685 					if (e.to == i) {
686 						had_dep_to_pack[e.from] = true;
687 						return false;
688 					} else if (configs[e.to].pack == configs[i].pack) {
689 						still_has_dep_to_pack[e.from] = true;
690 					}
691 					if (e.from == i) return false;
692 					return true;
693 				}).array;
694 
695 			configs[i] = Vertex.init; // mark config as removed
696 
697 			// also remove any configs that cannot be satisfied anymore
698 			foreach (j; 0 .. configs.length)
699 				if (j != i && had_dep_to_pack[j] && !still_has_dep_to_pack[j])
700 					removeConfig(j);
701 		}
702 
703 		bool isReachable(string pack, string conf) {
704 			if (pack == configs[0].pack && configs[0].config == conf) return true;
705 			foreach (e; edges)
706 				if (configs[e.to].pack == pack && configs[e.to].config == conf)
707 					return true;
708 			return false;
709 			//return (pack == configs[0].pack && conf == configs[0].config) || edges.canFind!(e => configs[e.to].pack == pack && configs[e.to].config == config);
710 		}
711 
712 		bool isReachableByAllParentPacks(size_t cidx) {
713 			bool[string] r;
714 			foreach (p; parents[configs[cidx].pack]) r[p] = false;
715 			foreach (e; edges) {
716 				if (e.to != cidx) continue;
717 				if (auto pp = configs[e.from].pack in r) *pp = true;
718 			}
719 			foreach (bool v; r) if (!v) return false;
720 			return true;
721 		}
722 
723 		string[] allconfigs_path;
724 
725 		void determineDependencyConfigs(in Package p, string c)
726 		{
727 			string[][string] depconfigs;
728 			foreach (d; p.getAllDependencies()) {
729 				auto dp = getDependency(d.name.toString(), true);
730 				if (!dp) continue;
731 
732 				string[] cfgs;
733 				if (auto pc = dp.name in m_overriddenConfigs) cfgs = [*pc];
734 				else {
735 					auto subconf = p.getSubConfiguration(c, dp, platform);
736 					if (!subconf.empty) cfgs = [subconf];
737 					else cfgs = dp.getPlatformConfigurations(platform);
738 				}
739 				cfgs = cfgs.filter!(c => haveConfig(d.name.toString(), c)).array;
740 
741 				// if no valid configuration was found for a dependency, don't include the
742 				// current configuration
743 				if (!cfgs.length) {
744 					logDebug("Skip %s %s (missing configuration for %s)", p.name, c, dp.name);
745 					return;
746 				}
747 				depconfigs[d.name.toString()] = cfgs;
748 			}
749 
750 			// add this configuration to the graph
751 			size_t cidx = createConfig(p.name, c);
752 			foreach (d; p.getAllDependencies())
753 				foreach (sc; depconfigs.get(d.name.toString(), null))
754 					createEdge(cidx, createConfig(d.name.toString(), sc));
755 		}
756 
757 		// create a graph of all possible package configurations (package, config) -> (sub-package, sub-config)
758 		void determineAllConfigs(in Package p)
759 		{
760 			auto idx = allconfigs_path.countUntil(p.name);
761 			enforce(idx < 0, format("Detected dependency cycle: %s", (allconfigs_path[idx .. $] ~ p.name).join("->")));
762 			allconfigs_path ~= p.name;
763 			scope (exit) allconfigs_path.length--;
764 
765 			// first, add all dependency configurations
766 			foreach (d; p.getAllDependencies) {
767 				auto dp = getDependency(d.name.toString(), true);
768 				if (!dp) continue;
769 				determineAllConfigs(dp);
770 			}
771 
772 			// for each configuration, determine the configurations usable for the dependencies
773 			if (auto pc = p.name in m_overriddenConfigs)
774 				determineDependencyConfigs(p, *pc);
775 			else
776 				foreach (c; p.getPlatformConfigurations(platform, p is m_rootPackage && allow_non_library))
777 					determineDependencyConfigs(p, c);
778 		}
779 		if (config.length) createConfig(m_rootPackage.name, config);
780 		determineAllConfigs(m_rootPackage);
781 
782 		// successively remove configurations until only one configuration per package is left
783 		bool changed;
784 		do {
785 			// remove all configs that are not reachable by all parent packages
786 			changed = false;
787 			foreach (i, ref c; configs) {
788 				if (c == Vertex.init) continue; // ignore deleted configurations
789 				if (!isReachableByAllParentPacks(i)) {
790 					logDebug("%s %s NOT REACHABLE by all of (%s):", c.pack, c.config, parents[c.pack]);
791 					removeConfig(i);
792 					changed = true;
793 				}
794 			}
795 
796 			// when all edges are cleaned up, pick one package and remove all but one config
797 			if (!changed) {
798 				foreach (p; getTopologicalPackageList()) {
799 					size_t cnt = 0;
800 					foreach (i, ref c; configs)
801 						if (c.pack == p.name && ++cnt > 1) {
802 							logDebug("NON-PRIMARY: %s %s", c.pack, c.config);
803 							removeConfig(i);
804 						}
805 					if (cnt > 1) {
806 						changed = true;
807 						break;
808 					}
809 				}
810 			}
811 		} while (changed);
812 
813 		// print out the resulting tree
814 		foreach (e; edges) logDebug("    %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config);
815 
816 		// return the resulting configuration set as an AA
817 		string[string] ret;
818 		foreach (c; configs) {
819 			if (c == Vertex.init) continue; // ignore deleted configurations
820 			assert(ret.get(c.pack, c.config) == c.config, format("Conflicting configurations for %s found: %s vs. %s", c.pack, c.config, ret[c.pack]));
821 			logDebug("Using configuration '%s' for %s", c.config, c.pack);
822 			ret[c.pack] = c.config;
823 		}
824 
825 		// check for conflicts (packages missing in the final configuration graph)
826 		void checkPacksRec(in Package pack) {
827 			auto pc = pack.name in ret;
828 			enforce(pc !is null, "Could not resolve configuration for package "~pack.name);
829 			foreach (p, dep; pack.getDependencies(*pc)) {
830 				auto deppack = getDependency(p, dep.optional);
831 				if (deppack) checkPacksRec(deppack);
832 			}
833 		}
834 		checkPacksRec(m_rootPackage);
835 
836 		return ret;
837 	}
838 
839 	/**
840 	 * Fills `dst` with values from this project.
841 	 *
842 	 * `dst` gets initialized according to the given platform and config.
843 	 *
844 	 * Params:
845 	 *   dst = The BuildSettings struct to fill with data.
846 	 *   gsettings = The generator settings to retrieve the values for.
847 	 *   config = Values of the given configuration will be retrieved.
848 	 *   root_package = If non null, use it instead of the project's real root package.
849 	 *   shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary.
850 	 */
851 	void addBuildSettings(ref BuildSettings dst, in GeneratorSettings gsettings, string config, in Package root_package = null, bool shallow = false)
852 	const {
853 		import dub.internal.utils : stripDlangSpecialChars;
854 
855 		auto configs = getPackageConfigs(gsettings.platform, config);
856 
857 		foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) {
858 			auto pkg_path = pkg.path.toNativeString();
859 			dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]);
860 
861 			assert(pkg.name in configs, "Missing configuration for "~pkg.name);
862 			logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]);
863 
864 			auto psettings = pkg.getBuildSettings(gsettings.platform, configs[pkg.name]);
865 			if (psettings.targetType != TargetType.none) {
866 				if (shallow && pkg !is m_rootPackage)
867 					psettings.sourceFiles = null;
868 				processVars(dst, this, pkg, psettings, gsettings);
869 				if (!gsettings.single && psettings.importPaths.empty)
870 					logWarn(`Package %s (configuration "%s") defines no import paths, use {"importPaths": [...]} or the default package directory structure to fix this.`, pkg.name, configs[pkg.name]);
871 				if (psettings.mainSourceFile.empty && pkg is m_rootPackage && psettings.targetType == TargetType.executable)
872 					logWarn(`Executable configuration "%s" of package %s defines no main source file, this may cause certain build modes to fail. Add an explicit "mainSourceFile" to the package description to fix this.`, configs[pkg.name], pkg.name);
873 			}
874 			if (pkg is m_rootPackage) {
875 				if (!shallow) {
876 					enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build.");
877 					enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build.");
878 				}
879 				dst.targetType = psettings.targetType;
880 				dst.targetPath = psettings.targetPath;
881 				dst.targetName = psettings.targetName;
882 				if (!psettings.workingDirectory.empty)
883 					dst.workingDirectory = processVars(psettings.workingDirectory, this, pkg, gsettings, true, [dst.environments, dst.buildEnvironments]);
884 				if (psettings.mainSourceFile.length)
885 					dst.mainSourceFile = processVars(psettings.mainSourceFile, this, pkg, gsettings, true, [dst.environments, dst.buildEnvironments]);
886 			}
887 		}
888 
889 		// always add all version identifiers of all packages
890 		foreach (pkg; this.getTopologicalPackageList(false, null, configs)) {
891 			auto psettings = pkg.getBuildSettings(gsettings.platform, configs[pkg.name]);
892 			dst.addVersions(psettings.versions);
893 		}
894 	}
895 
896 	/** Fills `dst` with build settings specific to the given build type.
897 
898 		Params:
899 			dst = The `BuildSettings` instance to add the build settings to
900 			gsettings = Target generator settings
901 			for_root_package = Selects if the build settings are for the root
902 				package or for one of the dependencies. Unittest flags will
903 				only be added to the root package.
904 	*/
905 	void addBuildTypeSettings(ref BuildSettings dst, in GeneratorSettings gsettings, bool for_root_package = true)
906 	{
907 		bool usedefflags = !(dst.requirements & BuildRequirement.noDefaultFlags);
908 		if (usedefflags) {
909 			BuildSettings btsettings;
910 			m_rootPackage.addBuildTypeSettings(btsettings, gsettings.platform, gsettings.buildType);
911 
912 			if (!for_root_package) {
913 				// don't propagate unittest switch to dependencies, as dependent
914 				// unit tests aren't run anyway and the additional code may
915 				// cause linking to fail on Windows (issue #640)
916 				btsettings.removeOptions(BuildOption.unittests);
917 			}
918 
919 			processVars(dst, this, m_rootPackage, btsettings, gsettings);
920 		}
921 	}
922 
923 	/// Outputs a build description of the project, including its dependencies.
924 	ProjectDescription describe(GeneratorSettings settings)
925 	{
926 		import dub.generators.targetdescription;
927 
928 		// store basic build parameters
929 		ProjectDescription ret;
930 		ret.rootPackage = m_rootPackage.name;
931 		ret.configuration = settings.config;
932 		ret.buildType = settings.buildType;
933 		ret.compiler = settings.platform.compiler;
934 		ret.architecture = settings.platform.architecture;
935 		ret.platform = settings.platform.platform;
936 
937 		// collect high level information about projects (useful for IDE display)
938 		auto configs = getPackageConfigs(settings.platform, settings.config);
939 		ret.packages ~= m_rootPackage.describe(settings.platform, settings.config);
940 		foreach (dep; m_dependencies)
941 			ret.packages ~= dep.describe(settings.platform, configs[dep.name]);
942 
943 		foreach (p; getTopologicalPackageList(false, null, configs))
944 			ret.packages[ret.packages.countUntil!(pp => pp.name == p.name)].active = true;
945 
946 		if (settings.buildType.length) {
947 			// collect build target information (useful for build tools)
948 			auto gen = new TargetDescriptionGenerator(this);
949 			try {
950 				gen.generate(settings);
951 				ret.targets = gen.targetDescriptions;
952 				ret.targetLookup = gen.targetDescriptionLookup;
953 			} catch (Exception e) {
954 				logDiagnostic("Skipping targets description: %s", e.msg);
955 				logDebug("Full error: %s", e.toString().sanitize);
956 			}
957 		}
958 
959 		return ret;
960 	}
961 
962 	private string[] listBuildSetting(string attributeName)(ref GeneratorSettings settings,
963 		string config, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
964 	{
965 		return listBuildSetting!attributeName(settings, getPackageConfigs(settings.platform, config),
966 			projectDescription, compiler, disableEscaping);
967 	}
968 
969 	private string[] listBuildSetting(string attributeName)(ref GeneratorSettings settings,
970 		string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
971 	{
972 		if (compiler)
973 			return formatBuildSettingCompiler!attributeName(settings, configs, projectDescription, compiler, disableEscaping);
974 		else
975 			return formatBuildSettingPlain!attributeName(settings, configs, projectDescription);
976 	}
977 
978 	// Output a build setting formatted for a compiler
979 	private string[] formatBuildSettingCompiler(string attributeName)(ref GeneratorSettings settings,
980 		string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
981 	{
982 		import std.process : escapeShellFileName;
983 		import std.path : dirSeparator;
984 
985 		assert(compiler);
986 
987 		auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage);
988 		auto buildSettings = targetDescription.buildSettings;
989 
990 		string[] values;
991 		switch (attributeName)
992 		{
993 		case "dflags":
994 		case "linkerFiles":
995 		case "mainSourceFile":
996 		case "importFiles":
997 			values = formatBuildSettingPlain!attributeName(settings, configs, projectDescription);
998 			break;
999 
1000 		case "lflags":
1001 		case "sourceFiles":
1002 		case "injectSourceFiles":
1003 		case "versions":
1004 		case "debugVersions":
1005 		case "importPaths":
1006 		case "cImportPaths":
1007 		case "stringImportPaths":
1008 		case "options":
1009 			auto bs = buildSettings.dup;
1010 			bs.dflags = null;
1011 
1012 			// Ensure trailing slash on directory paths
1013 			auto ensureTrailingSlash = (string path) => path.endsWith(dirSeparator) ? path : path ~ dirSeparator;
1014 			static if (attributeName == "importPaths")
1015 				bs.importPaths = bs.importPaths.map!(ensureTrailingSlash).array();
1016 			else static if (attributeName == "cImportPaths")
1017 				bs.cImportPaths = bs.cImportPaths.map!(ensureTrailingSlash).array();
1018 			else static if (attributeName == "stringImportPaths")
1019 				bs.stringImportPaths = bs.stringImportPaths.map!(ensureTrailingSlash).array();
1020 
1021 			compiler.prepareBuildSettings(bs, settings.platform, BuildSetting.all & ~to!BuildSetting(attributeName));
1022 			values = bs.dflags;
1023 			break;
1024 
1025 		case "libs":
1026 			auto bs = buildSettings.dup;
1027 			bs.dflags = null;
1028 			bs.lflags = null;
1029 			bs.sourceFiles = null;
1030 			bs.targetType = TargetType.none; // Force Compiler to NOT omit dependency libs when package is a library.
1031 
1032 			compiler.prepareBuildSettings(bs, settings.platform, BuildSetting.all & ~to!BuildSetting(attributeName));
1033 
1034 			if (bs.lflags)
1035 				values = compiler.lflagsToDFlags( bs.lflags );
1036 			else if (bs.sourceFiles)
1037 				values = compiler.lflagsToDFlags( bs.sourceFiles );
1038 			else
1039 				values = bs.dflags;
1040 
1041 			break;
1042 
1043 		default: assert(0);
1044 		}
1045 
1046 		// Escape filenames and paths
1047 		if(!disableEscaping)
1048 		{
1049 			switch (attributeName)
1050 			{
1051 			case "mainSourceFile":
1052 			case "linkerFiles":
1053 			case "injectSourceFiles":
1054 			case "copyFiles":
1055 			case "importFiles":
1056 			case "stringImportFiles":
1057 			case "sourceFiles":
1058 			case "importPaths":
1059 			case "cImportPaths":
1060 			case "stringImportPaths":
1061 				return values.map!(escapeShellFileName).array();
1062 
1063 			default:
1064 				return values;
1065 			}
1066 		}
1067 
1068 		return values;
1069 	}
1070 
1071 	// Output a build setting without formatting for any particular compiler
1072 	private string[] formatBuildSettingPlain(string attributeName)(ref GeneratorSettings settings, string[string] configs, ProjectDescription projectDescription)
1073 	{
1074 		import std.path : buildNormalizedPath, dirSeparator;
1075 		import std.range : only;
1076 
1077 		string[] list;
1078 
1079 		enforce(attributeName == "targetType" || projectDescription.lookupRootPackage().targetType != TargetType.none,
1080 			"Target type is 'none'. Cannot list build settings.");
1081 
1082 		static if (attributeName == "targetType")
1083 			if (projectDescription.rootPackage !in projectDescription.targetLookup)
1084 				return ["none"];
1085 
1086 		auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage);
1087 		auto buildSettings = targetDescription.buildSettings;
1088 
1089 		string[] substituteCommands(Package pack, string[] commands, CommandType type)
1090 		{
1091 			auto env = makeCommandEnvironmentVariables(type, pack, this, settings, buildSettings);
1092 			return processVars(this, pack, settings, commands, false, env);
1093 		}
1094 
1095 		// Return any BuildSetting member attributeName as a range of strings. Don't attempt to fixup values.
1096 		// allowEmptyString: When the value is a string (as opposed to string[]),
1097 		//                   is empty string an actual permitted value instead of
1098 		//                   a missing value?
1099 		auto getRawBuildSetting(Package pack, bool allowEmptyString) {
1100 			auto value = __traits(getMember, buildSettings, attributeName);
1101 
1102 			static if( attributeName.endsWith("Commands") )
1103 				return substituteCommands(pack, value, mixin("CommandType.", attributeName[0 .. $ - "Commands".length]));
1104 			else static if( is(typeof(value) == string[]) )
1105 				return value;
1106 			else static if( is(typeof(value) == string) )
1107 			{
1108 				auto ret = only(value);
1109 
1110 				// only() has a different return type from only(value), so we
1111 				// have to empty the range rather than just returning only().
1112 				if(value.empty && !allowEmptyString) {
1113 					ret.popFront();
1114 					assert(ret.empty);
1115 				}
1116 
1117 				return ret;
1118 			}
1119 			else static if( is(typeof(value) == string[string]) )
1120 				return value.byKeyValue.map!(a => a.key ~ "=" ~ a.value);
1121 			else static if( is(typeof(value) == enum) )
1122 				return only(value);
1123 			else static if( is(typeof(value) == Flags!BuildRequirement) )
1124 				return only(cast(BuildRequirement) cast(int) value.values);
1125 			else static if( is(typeof(value) == Flags!BuildOption) )
1126 				return only(cast(BuildOption) cast(int) value.values);
1127 			else
1128 				static assert(false, "Type of BuildSettings."~attributeName~" is unsupported.");
1129 		}
1130 
1131 		// Adjust BuildSetting member attributeName as needed.
1132 		// Returns a range of strings.
1133 		auto getFixedBuildSetting(Package pack) {
1134 			// Is relative path(s) to a directory?
1135 			enum isRelativeDirectory =
1136 				attributeName == "importPaths" || attributeName == "cImportPaths" || attributeName == "stringImportPaths" ||
1137 				attributeName == "targetPath" || attributeName == "workingDirectory";
1138 
1139 			// Is relative path(s) to a file?
1140 			enum isRelativeFile =
1141 				attributeName == "sourceFiles" || attributeName == "linkerFiles" ||
1142 				attributeName == "importFiles" || attributeName == "stringImportFiles" ||
1143 				attributeName == "copyFiles" || attributeName == "mainSourceFile" ||
1144 				attributeName == "injectSourceFiles";
1145 
1146 			// For these, empty string means "main project directory", not "missing value"
1147 			enum allowEmptyString =
1148 				attributeName == "targetPath" || attributeName == "workingDirectory";
1149 
1150 			enum isEnumBitfield =
1151 				attributeName == "requirements" || attributeName == "options";
1152 
1153 			enum isEnum = attributeName == "targetType";
1154 
1155 			auto values = getRawBuildSetting(pack, allowEmptyString);
1156 			string fixRelativePath(string importPath) { return buildNormalizedPath(pack.path.toString(), importPath); }
1157 			static string ensureTrailingSlash(string path) { return path.endsWith(dirSeparator) ? path : path ~ dirSeparator; }
1158 
1159 			static if(isRelativeDirectory) {
1160 				// Return full paths for the paths, making sure a
1161 				// directory separator is on the end of each path.
1162 				return values.map!(fixRelativePath).map!(ensureTrailingSlash);
1163 			}
1164 			else static if(isRelativeFile) {
1165 				// Return full paths.
1166 				return values.map!(fixRelativePath);
1167 			}
1168 			else static if(isEnumBitfield)
1169 				return bitFieldNames(values.front);
1170 			else static if (isEnum)
1171 				return [values.front.to!string];
1172 			else
1173 				return values;
1174 		}
1175 
1176 		foreach(value; getFixedBuildSetting(m_rootPackage)) {
1177 			list ~= value;
1178 		}
1179 
1180 		return list;
1181 	}
1182 
1183 	// The "compiler" arg is for choosing which compiler the output should be formatted for,
1184 	// or null to imply "list" format.
1185 	private string[] listBuildSetting(ref GeneratorSettings settings, string[string] configs,
1186 		ProjectDescription projectDescription, string requestedData, Compiler compiler, bool disableEscaping)
1187 	{
1188 		// Certain data cannot be formatter for a compiler
1189 		if (compiler)
1190 		{
1191 			switch (requestedData)
1192 			{
1193 			case "target-type":
1194 			case "target-path":
1195 			case "target-name":
1196 			case "working-directory":
1197 			case "string-import-files":
1198 			case "copy-files":
1199 			case "extra-dependency-files":
1200 			case "pre-generate-commands":
1201 			case "post-generate-commands":
1202 			case "pre-build-commands":
1203 			case "post-build-commands":
1204 			case "pre-run-commands":
1205 			case "post-run-commands":
1206 			case "environments":
1207 			case "build-environments":
1208 			case "run-environments":
1209 			case "pre-generate-environments":
1210 			case "post-generate-environments":
1211 			case "pre-build-environments":
1212 			case "post-build-environments":
1213 			case "pre-run-environments":
1214 			case "post-run-environments":
1215 			case "default-config":
1216 			case "configs":
1217 			case "default-build":
1218 			case "builds":
1219 				enforce(false, "--data="~requestedData~" can only be used with `--data-list` or `--data-list --data-0`.");
1220 				break;
1221 
1222 			case "requirements":
1223 				enforce(false, "--data=requirements can only be used with `--data-list` or `--data-list --data-0`. Use --data=options instead.");
1224 				break;
1225 
1226 			default: break;
1227 			}
1228 		}
1229 
1230 		import std.typetuple : TypeTuple;
1231 		auto args = TypeTuple!(settings, configs, projectDescription, compiler, disableEscaping);
1232 		switch (requestedData)
1233 		{
1234 		case "target-type":                return listBuildSetting!"targetType"(args);
1235 		case "target-path":                return listBuildSetting!"targetPath"(args);
1236 		case "target-name":                return listBuildSetting!"targetName"(args);
1237 		case "working-directory":          return listBuildSetting!"workingDirectory"(args);
1238 		case "main-source-file":           return listBuildSetting!"mainSourceFile"(args);
1239 		case "dflags":                     return listBuildSetting!"dflags"(args);
1240 		case "lflags":                     return listBuildSetting!"lflags"(args);
1241 		case "libs":                       return listBuildSetting!"libs"(args);
1242 		case "linker-files":               return listBuildSetting!"linkerFiles"(args);
1243 		case "source-files":               return listBuildSetting!"sourceFiles"(args);
1244 		case "inject-source-files":        return listBuildSetting!"injectSourceFiles"(args);
1245 		case "copy-files":                 return listBuildSetting!"copyFiles"(args);
1246 		case "extra-dependency-files":     return listBuildSetting!"extraDependencyFiles"(args);
1247 		case "versions":                   return listBuildSetting!"versions"(args);
1248 		case "debug-versions":             return listBuildSetting!"debugVersions"(args);
1249 		case "import-paths":               return listBuildSetting!"importPaths"(args);
1250 		case "string-import-paths":        return listBuildSetting!"stringImportPaths"(args);
1251 		case "import-files":               return listBuildSetting!"importFiles"(args);
1252 		case "string-import-files":        return listBuildSetting!"stringImportFiles"(args);
1253 		case "pre-generate-commands":      return listBuildSetting!"preGenerateCommands"(args);
1254 		case "post-generate-commands":     return listBuildSetting!"postGenerateCommands"(args);
1255 		case "pre-build-commands":         return listBuildSetting!"preBuildCommands"(args);
1256 		case "post-build-commands":        return listBuildSetting!"postBuildCommands"(args);
1257 		case "pre-run-commands":           return listBuildSetting!"preRunCommands"(args);
1258 		case "post-run-commands":          return listBuildSetting!"postRunCommands"(args);
1259 		case "environments":               return listBuildSetting!"environments"(args);
1260 		case "build-environments":         return listBuildSetting!"buildEnvironments"(args);
1261 		case "run-environments":           return listBuildSetting!"runEnvironments"(args);
1262 		case "pre-generate-environments":  return listBuildSetting!"preGenerateEnvironments"(args);
1263 		case "post-generate-environments": return listBuildSetting!"postGenerateEnvironments"(args);
1264 		case "pre-build-environments":     return listBuildSetting!"preBuildEnvironments"(args);
1265 		case "post-build-environments":    return listBuildSetting!"postBuildEnvironments"(args);
1266 		case "pre-run-environments":       return listBuildSetting!"preRunEnvironments"(args);
1267 		case "post-run-environments":      return listBuildSetting!"postRunEnvironments"(args);
1268 		case "requirements":               return listBuildSetting!"requirements"(args);
1269 		case "options":                    return listBuildSetting!"options"(args);
1270 		case "default-config":             return [getDefaultConfiguration(settings.platform)];
1271 		case "configs":                    return configurations;
1272 		case "default-build":              return [builds[0]];
1273 		case "builds":                     return builds;
1274 
1275 		default:
1276 			enforce(false, "--data="~requestedData~
1277 				" is not a valid option. See 'dub describe --help' for accepted --data= values.");
1278 		}
1279 
1280 		assert(0);
1281 	}
1282 
1283 	/// Outputs requested data for the project, optionally including its dependencies.
1284 	string[] listBuildSettings(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type)
1285 	{
1286 		import dub.compilers.utils : isLinkerFile;
1287 
1288 		auto projectDescription = describe(settings);
1289 		auto configs = getPackageConfigs(settings.platform, settings.config);
1290 		PackageDescription packageDescription;
1291 		foreach (pack; projectDescription.packages) {
1292 			if (pack.name == projectDescription.rootPackage)
1293 				packageDescription = pack;
1294 		}
1295 
1296 		if (projectDescription.rootPackage in projectDescription.targetLookup) {
1297 			// Copy linker files from sourceFiles to linkerFiles
1298 			auto target = projectDescription.lookupTarget(projectDescription.rootPackage);
1299 			foreach (file; target.buildSettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)))
1300 				target.buildSettings.addLinkerFiles(file);
1301 
1302 			// Remove linker files from sourceFiles
1303 			target.buildSettings.sourceFiles =
1304 				target.buildSettings.sourceFiles
1305 				.filter!(a => !isLinkerFile(settings.platform, a))
1306 				.array();
1307 			projectDescription.lookupTarget(projectDescription.rootPackage) = target;
1308 		}
1309 
1310 		Compiler compiler;
1311 		bool no_escape;
1312 		final switch (list_type) with (ListBuildSettingsFormat) {
1313 			case list: break;
1314 			case listNul: no_escape = true; break;
1315 			case commandLine: compiler = settings.compiler; break;
1316 			case commandLineNul: compiler = settings.compiler; no_escape = true; break;
1317 
1318 		}
1319 
1320 		auto result = requestedData
1321 			.map!(dataName => listBuildSetting(settings, configs, projectDescription, dataName, compiler, no_escape));
1322 
1323 		final switch (list_type) with (ListBuildSettingsFormat) {
1324 			case list: return result.map!(l => l.join("\n")).array();
1325 			case listNul: return result.map!(l => l.join("\0")).array;
1326 			case commandLine: return result.map!(l => l.join(" ")).array;
1327 			case commandLineNul: return result.map!(l => l.join("\0")).array;
1328 		}
1329 	}
1330 
1331 	/** Saves the currently selected dependency versions to disk.
1332 
1333 		The selections will be written to a file named
1334 		`SelectedVersions.defaultFile` ("dub.selections.json") within the
1335 		directory of the root package. Any existing file will get overwritten.
1336 	*/
1337 	void saveSelections()
1338 	{
1339 		assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections).");
1340 		const name = PackageName(m_rootPackage.basePackage.name);
1341 		if (m_selections.hasSelectedVersion(name))
1342 			m_selections.deselectVersion(name);
1343 		this.m_packageManager.writeSelections(
1344 			this.m_rootPackage, this.m_selections.m_selections,
1345 			this.m_selections.dirty);
1346 	}
1347 
1348 	deprecated bool isUpgradeCacheUpToDate()
1349 	{
1350 		return false;
1351 	}
1352 
1353 	deprecated Dependency[string] getUpgradeCache()
1354 	{
1355 		return null;
1356 	}
1357 }
1358 
1359 
1360 /// Determines the output format used for `Project.listBuildSettings`.
1361 enum ListBuildSettingsFormat {
1362 	list,           /// Newline separated list entries
1363 	listNul,        /// NUL character separated list entries (unescaped)
1364 	commandLine,    /// Formatted for compiler command line (one data list per line)
1365 	commandLineNul, /// NUL character separated list entries (unescaped, data lists separated by two NUL characters)
1366 }
1367 
1368 deprecated("Use `dub.packagemanager : PlacementLocation` instead")
1369 public alias PlacementLocation = dub.packagemanager.PlacementLocation;
1370 
1371 void processVars(ref BuildSettings dst, in Project project, in Package pack,
1372 	BuildSettings settings, in GeneratorSettings gsettings, bool include_target_settings = false)
1373 {
1374 	string[string] processVerEnvs(in string[string] targetEnvs, in string[string] defaultEnvs)
1375 	{
1376 		string[string] retEnv;
1377 		foreach (k, v; targetEnvs)
1378 			retEnv[k] = v;
1379 		foreach (k, v; defaultEnvs) {
1380 			if (k !in targetEnvs)
1381 				retEnv[k] = v;
1382 		}
1383 		return processVars(project, pack, gsettings, retEnv);
1384 	}
1385 	dst.addEnvironments(processVerEnvs(settings.environments, gsettings.buildSettings.environments));
1386 	dst.addBuildEnvironments(processVerEnvs(settings.buildEnvironments, gsettings.buildSettings.buildEnvironments));
1387 	dst.addRunEnvironments(processVerEnvs(settings.runEnvironments, gsettings.buildSettings.runEnvironments));
1388 	dst.addPreGenerateEnvironments(processVerEnvs(settings.preGenerateEnvironments, gsettings.buildSettings.preGenerateEnvironments));
1389 	dst.addPostGenerateEnvironments(processVerEnvs(settings.postGenerateEnvironments, gsettings.buildSettings.postGenerateEnvironments));
1390 	dst.addPreBuildEnvironments(processVerEnvs(settings.preBuildEnvironments, gsettings.buildSettings.preBuildEnvironments));
1391 	dst.addPostBuildEnvironments(processVerEnvs(settings.postBuildEnvironments, gsettings.buildSettings.postBuildEnvironments));
1392 	dst.addPreRunEnvironments(processVerEnvs(settings.preRunEnvironments, gsettings.buildSettings.preRunEnvironments));
1393 	dst.addPostRunEnvironments(processVerEnvs(settings.postRunEnvironments, gsettings.buildSettings.postRunEnvironments));
1394 
1395 	auto buildEnvs = [dst.environments, dst.buildEnvironments];
1396 
1397 	dst.addDFlags(processVars(project, pack, gsettings, settings.dflags, false, buildEnvs));
1398 	dst.addLFlags(processVars(project, pack, gsettings, settings.lflags, false, buildEnvs));
1399 	dst.addLibs(processVars(project, pack, gsettings, settings.libs, false, buildEnvs));
1400 	dst.addSourceFiles(processVars!true(project, pack, gsettings, settings.sourceFiles, true, buildEnvs));
1401 	dst.addImportFiles(processVars(project, pack, gsettings, settings.importFiles, true, buildEnvs));
1402 	dst.addStringImportFiles(processVars(project, pack, gsettings, settings.stringImportFiles, true, buildEnvs));
1403 	dst.addInjectSourceFiles(processVars!true(project, pack, gsettings, settings.injectSourceFiles, true, buildEnvs));
1404 	dst.addCopyFiles(processVars(project, pack, gsettings, settings.copyFiles, true, buildEnvs));
1405 	dst.addExtraDependencyFiles(processVars(project, pack, gsettings, settings.extraDependencyFiles, true, buildEnvs));
1406 	dst.addVersions(processVars(project, pack, gsettings, settings.versions, false, buildEnvs));
1407 	dst.addDebugVersions(processVars(project, pack, gsettings, settings.debugVersions, false, buildEnvs));
1408 	dst.addVersionFilters(processVars(project, pack, gsettings, settings.versionFilters, false, buildEnvs));
1409 	dst.addDebugVersionFilters(processVars(project, pack, gsettings, settings.debugVersionFilters, false, buildEnvs));
1410 	dst.addImportPaths(processVars(project, pack, gsettings, settings.importPaths, true, buildEnvs));
1411 	dst.addCImportPaths(processVars(project, pack, gsettings, settings.cImportPaths, true, buildEnvs));
1412 	dst.addStringImportPaths(processVars(project, pack, gsettings, settings.stringImportPaths, true, buildEnvs));
1413 	dst.addRequirements(settings.requirements);
1414 	dst.addOptions(settings.options);
1415 
1416 	// commands are substituted in dub.generators.generator : runBuildCommands
1417 	dst.addPreGenerateCommands(settings.preGenerateCommands);
1418 	dst.addPostGenerateCommands(settings.postGenerateCommands);
1419 	dst.addPreBuildCommands(settings.preBuildCommands);
1420 	dst.addPostBuildCommands(settings.postBuildCommands);
1421 	dst.addPreRunCommands(settings.preRunCommands);
1422 	dst.addPostRunCommands(settings.postRunCommands);
1423 
1424 	if (include_target_settings) {
1425 		dst.targetType = settings.targetType;
1426 		dst.targetPath = processVars(settings.targetPath, project, pack, gsettings, true, buildEnvs);
1427 		dst.targetName = settings.targetName;
1428 		if (!settings.workingDirectory.empty)
1429 			dst.workingDirectory = processVars(settings.workingDirectory, project, pack, gsettings, true, buildEnvs);
1430 		if (settings.mainSourceFile.length)
1431 			dst.mainSourceFile = processVars(settings.mainSourceFile, project, pack, gsettings, true, buildEnvs);
1432 	}
1433 }
1434 
1435 string[] processVars(bool glob = false)(in Project project, in Package pack, in GeneratorSettings gsettings, in string[] vars, bool are_paths = false, in string[string][] extraVers = null)
1436 {
1437 	auto ret = appender!(string[])();
1438 	processVars!glob(ret, project, pack, gsettings, vars, are_paths, extraVers);
1439 	return ret.data;
1440 }
1441 void processVars(bool glob = false)(ref Appender!(string[]) dst, in Project project, in Package pack, in GeneratorSettings gsettings, in string[] vars, bool are_paths = false, in string[string][] extraVers = null)
1442 {
1443 	static if (glob)
1444 		alias process = processVarsWithGlob!(Project, Package);
1445 	else
1446 		alias process = processVars!(Project, Package);
1447 	foreach (var; vars)
1448 		dst.put(process(var, project, pack, gsettings, are_paths, extraVers));
1449 }
1450 
1451 string processVars(Project, Package)(string var, in Project project, in Package pack, in GeneratorSettings gsettings, bool is_path, in string[string][] extraVers = null)
1452 {
1453 	var = var.expandVars!(varName => getVariable(varName, project, pack, gsettings, extraVers));
1454 	if (!is_path)
1455 		return var;
1456 	auto p = NativePath(var);
1457 	if (!p.absolute)
1458 		return (pack.path ~ p).toNativeString();
1459 	else
1460 		return p.toNativeString();
1461 }
1462 string[string] processVars(bool glob = false)(in Project project, in Package pack, in GeneratorSettings gsettings, in string[string] vars, in string[string][] extraVers = null)
1463 {
1464 	string[string] ret;
1465 	processVars!glob(ret, project, pack, gsettings, vars, extraVers);
1466 	return ret;
1467 }
1468 void processVars(bool glob = false)(ref string[string] dst, in Project project, in Package pack, in GeneratorSettings gsettings, in string[string] vars, in string[string][] extraVers)
1469 {
1470 	static if (glob)
1471 		alias process = processVarsWithGlob!(Project, Package);
1472 	else
1473 		alias process = processVars!(Project, Package);
1474 	foreach (k, var; vars)
1475 		dst[k] = process(var, project, pack, gsettings, false, extraVers);
1476 }
1477 
1478 private string[] processVarsWithGlob(Project, Package)(string var, in Project project, in Package pack, in GeneratorSettings gsettings, bool is_path, in string[string][] extraVers)
1479 {
1480 	assert(is_path, "can't glob something that isn't a path");
1481 	string res = processVars(var, project, pack, gsettings, is_path, extraVers);
1482 	// Find the unglobbed prefix and iterate from there.
1483 	size_t i = 0;
1484 	size_t sepIdx = 0;
1485 	loop: while (i < res.length) {
1486 		switch_: switch (res[i])
1487 		{
1488 		case '*', '?', '[', '{': break loop;
1489 		case '/': sepIdx = i; goto default;
1490 		version (Windows) { case '\\': sepIdx = i; goto default; }
1491 		default: ++i; break switch_;
1492 		}
1493 	}
1494 	if (i == res.length) //no globbing found in the path
1495 		return [res];
1496 	import std.file : dirEntries, SpanMode;
1497 	import std.path : buildNormalizedPath, globMatch, isAbsolute, relativePath;
1498 	auto cwd = gsettings.toolWorkingDirectory.toNativeString;
1499 	auto path = res[0 .. sepIdx];
1500 	bool prependCwd = false;
1501 	if (!isAbsolute(path))
1502 	{
1503 		prependCwd = true;
1504 		path = buildNormalizedPath(cwd, path);
1505 	}
1506 
1507 	return dirEntries(path, SpanMode.depth)
1508 		.map!(de => prependCwd
1509 			? de.name.relativePath(cwd)
1510 			: de.name)
1511 		.filter!(name => globMatch(name, res))
1512 		.array;
1513 }
1514 /// Expand variables using `$VAR_NAME` or `${VAR_NAME}` syntax.
1515 /// `$$` escapes itself and is expanded to a single `$`.
1516 private string expandVars(alias expandVar)(string s)
1517 {
1518 	import std.functional : not;
1519 
1520 	auto result = appender!string;
1521 
1522 	static bool isVarChar(char c)
1523 	{
1524 		import std.ascii;
1525 		return isAlphaNum(c) || c == '_';
1526 	}
1527 
1528 	while (true)
1529 	{
1530 		auto pos = s.indexOf('$');
1531 		if (pos < 0)
1532 		{
1533 			result.put(s);
1534 			return result.data;
1535 		}
1536 		result.put(s[0 .. pos]);
1537 		s = s[pos + 1 .. $];
1538 		enforce(s.length > 0, "Variable name expected at end of string");
1539 		switch (s[0])
1540 		{
1541 			case '$':
1542 				result.put("$");
1543 				s = s[1 .. $];
1544 				break;
1545 			case '{':
1546 				pos = s.indexOf('}');
1547 				enforce(pos >= 0, "Could not find '}' to match '${'");
1548 				result.put(expandVar(s[1 .. pos]));
1549 				s = s[pos + 1 .. $];
1550 				break;
1551 			default:
1552 				pos = s.representation.countUntil!(not!isVarChar);
1553 				if (pos < 0)
1554 					pos = s.length;
1555 				result.put(expandVar(s[0 .. pos]));
1556 				s = s[pos .. $];
1557 				break;
1558 		}
1559 	}
1560 }
1561 
1562 unittest
1563 {
1564 	string[string] vars =
1565 	[
1566 		"A" : "a",
1567 		"B" : "b",
1568 	];
1569 
1570 	string expandVar(string name) { auto p = name in vars; enforce(p, name); return *p; }
1571 
1572 	assert(expandVars!expandVar("") == "");
1573 	assert(expandVars!expandVar("x") == "x");
1574 	assert(expandVars!expandVar("$$") == "$");
1575 	assert(expandVars!expandVar("x$$") == "x$");
1576 	assert(expandVars!expandVar("$$x") == "$x");
1577 	assert(expandVars!expandVar("$$$$") == "$$");
1578 	assert(expandVars!expandVar("x$A") == "xa");
1579 	assert(expandVars!expandVar("x$$A") == "x$A");
1580 	assert(expandVars!expandVar("$A$B") == "ab");
1581 	assert(expandVars!expandVar("${A}$B") == "ab");
1582 	assert(expandVars!expandVar("$A${B}") == "ab");
1583 	assert(expandVars!expandVar("a${B}") == "ab");
1584 	assert(expandVars!expandVar("${A}b") == "ab");
1585 
1586 	import std.exception : assertThrown;
1587 	assertThrown(expandVars!expandVar("$"));
1588 	assertThrown(expandVars!expandVar("${}"));
1589 	assertThrown(expandVars!expandVar("$|"));
1590 	assertThrown(expandVars!expandVar("x$"));
1591 	assertThrown(expandVars!expandVar("$X"));
1592 	assertThrown(expandVars!expandVar("${"));
1593 	assertThrown(expandVars!expandVar("${X"));
1594 
1595 	// https://github.com/dlang/dmd/pull/9275
1596 	assert(expandVars!expandVar("$${DUB_EXE:-dub}") == "${DUB_EXE:-dub}");
1597 }
1598 
1599 /// Expands the variables in the input string with the same rules as command
1600 /// variables inside custom dub commands.
1601 ///
1602 /// Params:
1603 ///     s = the input string where environment variables in form `$VAR` should be replaced
1604 ///     throwIfMissing = if true, throw an exception if the given variable is not found,
1605 ///                      otherwise replace unknown variables with the empty string.
1606 string expandEnvironmentVariables(string s, bool throwIfMissing = true)
1607 {
1608 	import std.process : environment;
1609 
1610 	return expandVars!((v) {
1611 		auto ret = environment.get(v);
1612 		if (ret is null && throwIfMissing)
1613 			throw new Exception("Specified environment variable `$" ~ v ~ "` is not set");
1614 		return ret;
1615 	})(s);
1616 }
1617 
1618 // Keep the following list up-to-date if adding more build settings variables.
1619 /// List of variables that can be used in build settings
1620 package(dub) immutable buildSettingsVars = [
1621 	"ARCH", "PLATFORM", "PLATFORM_POSIX", "BUILD_TYPE"
1622 ];
1623 
1624 private string getVariable(Project, Package)(string name, in Project project, in Package pack, in GeneratorSettings gsettings, in string[string][] extraVars = null)
1625 {
1626 	import dub.internal.utils : getDUBExePath;
1627 	import std.process : environment, escapeShellFileName;
1628 	import std.uni : asUpperCase;
1629 
1630 	NativePath path;
1631 	if (name == "PACKAGE_DIR")
1632 		path = pack.path;
1633 	else if (name == "ROOT_PACKAGE_DIR")
1634 		path = project.rootPackage.path;
1635 
1636 	if (name.endsWith("_PACKAGE_DIR")) {
1637 		auto pname = name[0 .. $-12];
1638 		foreach (prj; project.getTopologicalPackageList())
1639 			if (prj.name.asUpperCase.map!(a => a == '-' ? '_' : a).equal(pname))
1640 			{
1641 				path = prj.path;
1642 				break;
1643 			}
1644 	}
1645 
1646 	if (!path.empty)
1647 	{
1648 		// no trailing slash for clean path concatenation (see #1392)
1649 		path.endsWithSlash = false;
1650 		return path.toNativeString();
1651 	}
1652 
1653 	if (name == "DUB") {
1654 		return getDUBExePath(gsettings.platform.compilerBinary).toNativeString();
1655 	}
1656 
1657 	if (name == "ARCH") {
1658 		foreach (a; gsettings.platform.architecture)
1659 			return a;
1660 		return "";
1661 	}
1662 
1663 	if (name == "PLATFORM") {
1664 		import std.algorithm : filter;
1665 		foreach (p; gsettings.platform.platform.filter!(p => p != "posix"))
1666 			return p;
1667 		foreach (p; gsettings.platform.platform)
1668 			return p;
1669 		return "";
1670 	}
1671 
1672 	if (name == "PLATFORM_POSIX") {
1673 		import std.algorithm : canFind;
1674 		if (gsettings.platform.platform.canFind("posix"))
1675 			return "posix";
1676 		foreach (p; gsettings.platform.platform)
1677 			return p;
1678 		return "";
1679 	}
1680 
1681 	if (name == "BUILD_TYPE") return gsettings.buildType;
1682 
1683 	if (name == "DFLAGS" || name == "LFLAGS")
1684 	{
1685 		auto buildSettings = pack.getBuildSettings(gsettings.platform, gsettings.config);
1686 		if (name == "DFLAGS")
1687 			return join(buildSettings.dflags," ");
1688 		else if (name == "LFLAGS")
1689 			return join(buildSettings.lflags," ");
1690 	}
1691 
1692 	import std.range;
1693 	foreach (aa; retro(extraVars))
1694 		if (auto exvar = name in aa)
1695 			return *exvar;
1696 
1697 	auto envvar = environment.get(name);
1698 	if (envvar !is null) return envvar;
1699 
1700 	throw new Exception("Invalid variable: "~name);
1701 }
1702 
1703 
1704 unittest
1705 {
1706 	static struct MockPackage
1707 	{
1708 		this(string name)
1709 		{
1710 			this.name = name;
1711 			version (Posix)
1712 				path = NativePath("/pkgs/"~name);
1713 			else version (Windows)
1714 				path = NativePath(`C:\pkgs\`~name);
1715 			// see 4d4017c14c, #268, and #1392 for why this all package paths end on slash internally
1716 			path.endsWithSlash = true;
1717 		}
1718 		string name;
1719 		NativePath path;
1720 		BuildSettings getBuildSettings(in BuildPlatform platform, string config) const
1721 		{
1722 			return BuildSettings();
1723 		}
1724 	}
1725 
1726 	static struct MockProject
1727 	{
1728 		MockPackage rootPackage;
1729 		inout(MockPackage)[] getTopologicalPackageList() inout
1730 		{
1731 			return _dependencies;
1732 		}
1733 	private:
1734 		MockPackage[] _dependencies;
1735 	}
1736 
1737 	MockProject proj = {
1738 		rootPackage: MockPackage("root"),
1739 		_dependencies: [MockPackage("dep1"), MockPackage("dep2")]
1740 	};
1741 	auto pack = MockPackage("test");
1742 	GeneratorSettings gsettings;
1743 	enum isPath = true;
1744 
1745 	import std.path : dirSeparator;
1746 
1747 	static NativePath woSlash(NativePath p) { p.endsWithSlash = false; return p; }
1748 	// basic vars
1749 	assert(processVars("Hello $PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(pack.path).toNativeString);
1750 	assert(processVars("Hello $ROOT_PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(proj.rootPackage.path).toNativeString.chomp(dirSeparator));
1751 	assert(processVars("Hello $DEP1_PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(proj._dependencies[0].path).toNativeString);
1752 	// ${VAR} replacements
1753 	assert(processVars("Hello ${PACKAGE_DIR}"~dirSeparator~"foobar", proj, pack, gsettings, !isPath) == "Hello "~(pack.path ~ "foobar").toNativeString);
1754 	assert(processVars("Hello $PACKAGE_DIR"~dirSeparator~"foobar", proj, pack, gsettings, !isPath) == "Hello "~(pack.path ~ "foobar").toNativeString);
1755 	// test with isPath
1756 	assert(processVars("local", proj, pack, gsettings, isPath) == (pack.path ~ "local").toNativeString);
1757 	assert(processVars("foo/$$ESCAPED", proj, pack, gsettings, isPath) == (pack.path ~ "foo/$ESCAPED").toNativeString);
1758 	assert(processVars("$$ESCAPED", proj, pack, gsettings, !isPath) == "$ESCAPED");
1759 	// test other env variables
1760 	import std.process : environment;
1761 	environment["MY_ENV_VAR"] = "blablabla";
1762 	assert(processVars("$MY_ENV_VAR", proj, pack, gsettings, !isPath) == "blablabla");
1763 	assert(processVars("${MY_ENV_VAR}suffix", proj, pack, gsettings, !isPath) == "blablablasuffix");
1764 	assert(processVars("$MY_ENV_VAR-suffix", proj, pack, gsettings, !isPath) == "blablabla-suffix");
1765 	assert(processVars("$MY_ENV_VAR:suffix", proj, pack, gsettings, !isPath) == "blablabla:suffix");
1766 	assert(processVars("$MY_ENV_VAR$MY_ENV_VAR", proj, pack, gsettings, !isPath) == "blablablablablabla");
1767 	environment.remove("MY_ENV_VAR");
1768 }
1769 
1770 /**
1771  * Holds and stores a set of version selections for package dependencies.
1772  *
1773  * This is the runtime representation of the information contained in
1774  * "dub.selections.json" within a package's directory.
1775  *
1776  * Note that as subpackages share the same version as their main package,
1777  * this class will treat any subpackage reference as a reference to its
1778  * main package.
1779  */
1780 public class SelectedVersions {
1781 	protected {
1782 		enum FileVersion = 1;
1783 		Selections!1 m_selections;
1784 		bool m_dirty = false; // has changes since last save
1785 		bool m_bare = true;
1786 	}
1787 
1788 	/// Default file name to use for storing selections.
1789 	enum defaultFile = "dub.selections.json";
1790 
1791 	/// Constructs a new empty version selection.
1792 	public this(uint version_ = FileVersion) @safe pure
1793 	{
1794 		enforce(version_ == 1, "Unsupported file version");
1795 		this.m_selections = Selections!1(version_);
1796 	}
1797 
1798 	/// Constructs a new non-empty version selection.
1799 	public this(Selections!1 data) @safe pure nothrow @nogc
1800 	{
1801 		this.m_selections = data;
1802 		this.m_bare = false;
1803 	}
1804 
1805 	/** Constructs a new version selection from JSON data.
1806 
1807 		The structure of the JSON document must match the contents of the
1808 		"dub.selections.json" file.
1809 	*/
1810 	deprecated("Pass a `dub.recipe.selection : Selected` directly")
1811 	this(Json data)
1812 	{
1813 		deserialize(data);
1814 		m_dirty = false;
1815 	}
1816 
1817 	/** Constructs a new version selections from an existing JSON file.
1818 	*/
1819 	deprecated("JSON deserialization is deprecated")
1820 	this(NativePath path)
1821 	{
1822 		auto json = jsonFromFile(path);
1823 		deserialize(json);
1824 		m_dirty = false;
1825 		m_bare = false;
1826 	}
1827 
1828 	/// Returns a list of names for all packages that have a version selection.
1829 	@property string[] selectedPackages() const { return m_selections.versions.keys; }
1830 
1831 	/// Determines if any changes have been made after loading the selections from a file.
1832 	@property bool dirty() const { return m_dirty; }
1833 
1834 	/// Determine if this set of selections is still empty (but not `clear`ed).
1835 	@property bool bare() const { return m_bare && !m_dirty; }
1836 
1837 	/// Removes all selections.
1838 	void clear()
1839 	{
1840 		m_selections.versions = null;
1841 		m_dirty = true;
1842 	}
1843 
1844 	/// Duplicates the set of selected versions from another instance.
1845 	void set(SelectedVersions versions)
1846 	{
1847 		m_selections.fileVersion = versions.m_selections.fileVersion;
1848 		m_selections.versions = versions.m_selections.versions.dup;
1849 		m_dirty = true;
1850 	}
1851 
1852 	/// Selects a certain version for a specific package.
1853 	deprecated("Use the overload that accepts a `PackageName`")
1854 	void selectVersion(string package_id, Version version_)
1855 	{
1856 		const name = PackageName(package_id);
1857 		return this.selectVersion(name, version_);
1858 	}
1859 
1860 	/// Ditto
1861 	void selectVersion(in PackageName name, Version version_)
1862 	{
1863 		const dep = Dependency(version_);
1864 		this.selectVersionInternal(name, dep);
1865 	}
1866 
1867 	/// Selects a certain path for a specific package.
1868 	deprecated("Use the overload that accepts a `PackageName`")
1869 	void selectVersion(string package_id, NativePath path)
1870 	{
1871 		const name = PackageName(package_id);
1872 		return this.selectVersion(name, path);
1873 	}
1874 
1875 	/// Ditto
1876 	void selectVersion(in PackageName name, NativePath path)
1877 	{
1878 		const dep = Dependency(path);
1879 		this.selectVersionInternal(name, dep);
1880 	}
1881 
1882 	/// Selects a certain Git reference for a specific package.
1883 	deprecated("Use the overload that accepts a `PackageName`")
1884 	void selectVersion(string package_id, Repository repository)
1885 	{
1886 		const name = PackageName(package_id);
1887 		return this.selectVersion(name, repository);
1888 	}
1889 
1890 	/// Ditto
1891 	void selectVersion(in PackageName name, Repository repository)
1892 	{
1893 		const dep = Dependency(repository);
1894 		this.selectVersionInternal(name, dep);
1895 	}
1896 
1897 	/// Internal implementation of selectVersion
1898 	private void selectVersionInternal(in PackageName name, in Dependency dep)
1899 	{
1900 		if (auto pdep = name.main.toString() in m_selections.versions) {
1901 			if (*pdep == dep)
1902 				return;
1903 		}
1904 		m_selections.versions[name.main.toString()] = dep;
1905 		m_dirty = true;
1906 	}
1907 
1908 	deprecated("Move `spec` inside of the `repository` parameter and call `selectVersion`")
1909 	void selectVersionWithRepository(string package_id, Repository repository, string spec)
1910 	{
1911 		this.selectVersion(package_id, Repository(repository.remote(), spec));
1912 	}
1913 
1914 	/// Removes the selection for a particular package.
1915 	deprecated("Use the overload that accepts a `PackageName`")
1916 	void deselectVersion(string package_id)
1917 	{
1918 		const n = PackageName(package_id);
1919 		this.deselectVersion(n);
1920 	}
1921 
1922 	/// Ditto
1923 	void deselectVersion(in PackageName name)
1924 	{
1925 		m_selections.versions.remove(name.main.toString());
1926 		m_dirty = true;
1927 	}
1928 
1929 	/// Determines if a particular package has a selection set.
1930 	deprecated("Use the overload that accepts a `PackageName`")
1931 	bool hasSelectedVersion(string packageId) const {
1932 		const name = PackageName(packageId);
1933 		return this.hasSelectedVersion(name);
1934 	}
1935 
1936 	/// Ditto
1937 	bool hasSelectedVersion(in PackageName name) const
1938 	{
1939 		return (name.main.toString() in m_selections.versions) !is null;
1940 	}
1941 
1942 	/** Returns the selection for a particular package.
1943 
1944 		Note that the returned `Dependency` can either have the
1945 		`Dependency.path` property set to a non-empty value, in which case this
1946 		is a path based selection, or its `Dependency.version_` property is
1947 		valid and it is a version selection.
1948 	*/
1949 	deprecated("Use the overload that accepts a `PackageName`")
1950 	Dependency getSelectedVersion(string packageId) const
1951 	{
1952 		const name = PackageName(packageId);
1953 		return this.getSelectedVersion(name);
1954 	}
1955 
1956 	/// Ditto
1957 	Dependency getSelectedVersion(in PackageName name) const
1958 	{
1959 		enforce(hasSelectedVersion(name));
1960 		return m_selections.versions[name.main.toString()];
1961 	}
1962 
1963 	/** Stores the selections to disk.
1964 
1965 		The target file will be written in JSON format. Usually, `defaultFile`
1966 		should be used as the file name and the directory should be the root
1967 		directory of the project's root package.
1968 	*/
1969 	deprecated("Use `PackageManager.writeSelections` to write a `SelectionsFile`")
1970 	void save(NativePath path)
1971 	{
1972 		path.writeFile(PackageManager.selectionsToString(this.m_selections));
1973 		m_dirty = false;
1974 		m_bare = false;
1975 	}
1976 
1977 	deprecated("Use `dub.dependency : Dependency.toJson(true)`")
1978 	static Json dependencyToJson(Dependency d)
1979 	{
1980 		return d.toJson(true);
1981 	}
1982 
1983 	deprecated("JSON deserialization is deprecated")
1984 	static Dependency dependencyFromJson(Json j)
1985 	{
1986 		if (j.type == Json.Type..string)
1987 			return Dependency(Version(j.get!string));
1988 		else if (j.type == Json.Type.object && "path" in j)
1989 			return Dependency(NativePath(j["path"].get!string));
1990 		else if (j.type == Json.Type.object && "repository" in j)
1991 			return Dependency(Repository(j["repository"].get!string,
1992 				enforce("version" in j, "Expected \"version\" field in repository version object").get!string));
1993 		else throw new Exception(format("Unexpected type for dependency: %s", j));
1994 	}
1995 
1996 	deprecated("JSON serialization is deprecated")
1997 	Json serialize() const {
1998 		return PackageManager.selectionsToJSON(this.m_selections);
1999 	}
2000 
2001 	deprecated("JSON deserialization is deprecated")
2002 	private void deserialize(Json json)
2003 	{
2004 		const fileVersion = json["fileVersion"].get!int;
2005 		enforce(fileVersion == FileVersion, "Mismatched dub.selections.json version: " ~ to!string(fileVersion) ~ " vs. " ~ to!string(FileVersion));
2006 		clear();
2007 		m_selections.fileVersion = fileVersion;
2008 		scope(failure) clear();
2009 		foreach (string p, dep; json["versions"])
2010 			m_selections.versions[p] = dependencyFromJson(dep);
2011 	}
2012 }
2013 
2014 /// The template code from which the test runner is generated
2015 private immutable TestRunnerTemplate = q{
2016 deprecated // allow silently using deprecated symbols
2017 module dub_test_root;
2018 
2019 import std.typetuple;
2020 
2021 %-(static import %s;
2022 %);
2023 
2024 alias allModules = TypeTuple!(
2025     %-(%s, %)
2026 );
2027 
2028 %s
2029 };
2030 
2031 /// The default test runner that gets used if none is provided
2032 private immutable DefaultTestRunnerCode = q{
2033 	version(D_BetterC) {
2034 		extern(C) int main() {
2035 			foreach (module_; allModules) {
2036 				foreach (unitTest; __traits(getUnitTests, module_)) {
2037 					unitTest();
2038 				}
2039 			}
2040 			import core.stdc.stdio : puts;
2041 			puts("All unit tests have been run successfully.");
2042 			return 0;
2043 		}
2044 	} else {
2045 		void main() {
2046 			version (D_Coverage) {
2047 			} else {
2048 				import std.stdio : writeln;
2049 				writeln("All unit tests have been run successfully.");
2050 			}
2051 		}
2052 		shared static this() {
2053 			version (Have_tested) {
2054 				import tested;
2055 				import core.runtime;
2056 				import std.exception;
2057 				Runtime.moduleUnitTester = () => true;
2058 				enforce(runUnitTests!allModules(new ConsoleTestResultWriter), "Unit tests failed.");
2059 			}
2060 		}
2061 	}
2062 };