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