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.easy;
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, PackageName.init, 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.normalized()) {
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 						return m_packageManager.getOrLoadPackage(path, NativePath.init, dep.name);
557 					},
558 					(Repository repo) {
559 						return m_packageManager.loadSCMPackage(dep.name, repo);
560 					},
561 					(VersionRange range) {
562 						// See `dub.recipe.selection : SelectedDependency.fromConfig`
563 						assert(range.isExactVersion());
564 						return m_packageManager.getPackage(dep.name, vspec.version_);
565 					},
566 				);
567 			} else if (m_dependencies.canFind!(d => PackageName(d.name).main == basename)) {
568 				auto idx = m_dependencies.countUntil!(d => PackageName(d.name).main == basename);
569 				auto bp = m_dependencies[idx].basePackage;
570 				vspec = Dependency(bp.path);
571 				p = resolveSubPackage(bp, subname, false);
572 			} else {
573 				logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.",
574 					indent, basename, dep.name, pack.name);
575 			}
576 
577 			// We didn't find the package
578 			if (p is null)
579 			{
580 				if (!vspec.repository.empty) {
581 					p = m_packageManager.loadSCMPackage(dep.name, vspec.repository);
582 					enforce(p !is null,
583 						"Unable to fetch '%s@%s' using git - does the repository and version exist?".format(
584 							dep.name, vspec.repository));
585 				} else if (!vspec.path.empty && is_desired) {
586 					NativePath path = vspec.path;
587 					if (!path.absolute) path = pack.path ~ path;
588 					logDiagnostic("%sAdding local %s in %s", indent, dep.name, path);
589 					p = m_packageManager.getOrLoadPackage(path, NativePath.init, dep.name);
590 					path.endsWithSlash = true;
591 					if (path != p.basePackage.path) {
592 						logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name);
593 					}
594 				} else {
595 					logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name);
596 					if (is_desired) m_missingDependencies ~= dep.name.toString();
597 					continue;
598 				}
599 			}
600 
601 			if (!m_dependencies.canFind(p)) {
602 				logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString());
603 				m_dependencies ~= p;
604 				if (basename.toString() == m_rootPackage.basePackage.name)
605 					p.warnOnSpecialCompilerFlags();
606 				collectDependenciesRec(p, depth+1);
607 			}
608 
609 			m_dependees[p] ~= pack;
610 			//enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString());
611 		}
612 	}
613 
614 	/// Convenience function used by `reinit`
615 	private Package resolveSubPackage(Package p, string subname, bool silentFail) {
616 		if (!subname.length || p is null)
617 			return p;
618 		return m_packageManager.getSubPackage(p, subname, silentFail);
619 	}
620 
621 	/// Returns the name of the root package.
622 	@property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; }
623 
624 	/// Returns the names of all configurations of the root package.
625 	@property string[] configurations() const { return m_rootPackage.configurations; }
626 
627 	/// Returns the names of all built-in and custom build types of the root package.
628 	/// The default built-in build type is the first item in the list.
629 	@property string[] builds() const { return builtinBuildTypes ~ m_rootPackage.customBuildTypes; }
630 
631 	/// Returns a map with the configuration for all packages in the dependency tree.
632 	string[string] getPackageConfigs(in BuildPlatform platform, string config, bool allow_non_library = true)
633 	const {
634 		import std.typecons : Rebindable, rebindable;
635 		import std.range : only;
636 
637 		// prepare by collecting information about all packages in the project
638 		// qualified names and dependencies are cached, to avoid recomputing
639 		// them multiple times during the algorithm
640 		auto packages = collectPackageInformation();
641 
642 		// graph of the project's package configuration dependencies
643 		// (package, config) -> (sub-package, sub-config)
644 		static struct Vertex { size_t pack = size_t.max; string config; }
645 		static struct Edge { size_t from, to; }
646 		Vertex[] configs;
647 		void[0][Vertex] configs_set;
648 		Edge[] edges;
649 
650 
651 		size_t createConfig(size_t pack_idx, string config) {
652 			foreach (i, v; configs)
653 				if (v.pack == pack_idx && v.config == config)
654 					return i;
655 
656 			auto pname = packages[pack_idx].name;
657 			assert(pname !in m_overriddenConfigs || config == m_overriddenConfigs[pname]);
658 			logDebug("Add config %s %s", pname, config);
659 			auto cfg = Vertex(pack_idx, config);
660 			configs ~= cfg;
661 			configs_set[cfg] = (void[0]).init;
662 			return configs.length-1;
663 		}
664 
665 		bool haveConfig(size_t pack_idx, string config) {
666 			return (Vertex(pack_idx, config) in configs_set) !is null;
667 		}
668 
669 		void removeConfig(size_t config_index) {
670 			logDebug("Eliminating config %s for %s", configs[config_index].config, configs[config_index].pack);
671 			auto had_dep_to_pack = new bool[configs.length];
672 			auto still_has_dep_to_pack = new bool[configs.length];
673 
674 			// eliminate all edges that connect to config 'config_index' and
675 			// track all connected configs
676 			edges = edges.filterInPlace!((e) {
677 				if (e.to == config_index) {
678 					had_dep_to_pack[e.from] = true;
679 					return false;
680 				} else if (configs[e.to].pack == configs[config_index].pack) {
681 					still_has_dep_to_pack[e.from] = true;
682 				}
683 
684 				return e.from != config_index;
685 			});
686 
687 			// mark config as removed
688 			configs_set.remove(configs[config_index]);
689 			configs[config_index] = Vertex.init;
690 
691 			// also remove any configs that cannot be satisfied anymore
692 			foreach (j; 0 .. configs.length)
693 				if (j != config_index && had_dep_to_pack[j] && !still_has_dep_to_pack[j])
694 					removeConfig(j);
695 		}
696 
697 		bool[] reachable = new bool[packages.length]; // reused to avoid continuous re-allocation
698 		bool isReachableByAllParentPacks(size_t cidx) {
699 			foreach (p; packages[configs[cidx].pack].parents) reachable[p] = false;
700 			foreach (e; edges) {
701 				if (e.to != cidx) continue;
702 				reachable[configs[e.from].pack] = true;
703 			}
704 			foreach (p; packages[configs[cidx].pack].parents)
705 				if (!reachable[p])
706 					return false;
707 			return true;
708 		}
709 
710 		string[][] depconfigs = new string[][](packages.length);
711 		void determineDependencyConfigs(size_t pack_idx, string c)
712 		{
713 			void[0][Edge] edges_set;
714 			void createEdge(size_t from, size_t to) {
715 				if (Edge(from, to) in edges_set)
716 					return;
717 				logDebug("Including %s %s -> %s %s", configs[from].pack, configs[from].config, configs[to].pack, configs[to].config);
718 				edges ~= Edge(from, to);
719 				edges_set[Edge(from, to)] = (void[0]).init;
720 			}
721 
722 			auto pack = &packages[pack_idx];
723 
724 			// below we call createConfig for the main package if
725 			// config.length is not zero.  Carry on for that case,
726 			// otherwise we've handle the pair (p, c) already
727 			if(haveConfig(pack_idx, c) && !(config.length && pack.name == m_rootPackage.name && config == c))
728 				return;
729 
730 			foreach (d; pack.dependencies) {
731 				auto dp = packages.getPackageIndex(d.name.toString());
732 				if (dp == size_t.max) continue;
733 
734 				depconfigs[dp].length = 0;
735 				depconfigs[dp].assumeSafeAppend;
736 
737 				void setConfigs(R)(R configs) {
738 					configs
739 						.filter!(c => haveConfig(dp, c))
740 						.each!((c) { depconfigs[dp] ~= c; });
741 				}
742 				if (auto pc = packages[dp].name in m_overriddenConfigs) {
743 					setConfigs(only(*pc));
744 				} else {
745 					auto subconf = pack.package_.getSubConfiguration(c, packages[dp].package_, platform);
746 					if (!subconf.empty) setConfigs(only(subconf));
747 					else {
748 						setConfigs(packages[dp].package_.getPlatformConfigurations(platform));
749 						if (depconfigs[dp].length == 0) {
750 							// Try with the executables too
751 							setConfigs(packages[dp].package_.getPlatformConfigurations(platform, true));
752 						}
753 					}
754 				}
755 
756 				// if no valid configuration was found for a dependency, don't include the
757 				// current configuration
758 				if (!depconfigs[dp].length) {
759 					logDebug("Skip %s %s (missing configuration for %s)", pack.name, c, packages[dp].name);
760 					return;
761 				}
762 			}
763 
764 			// add this configuration to the graph
765 			size_t cidx = createConfig(pack_idx, c);
766 			foreach (d; pack.dependencies) {
767 				if (auto pdp = d.name.toString() in packages)
768 					foreach (sc; depconfigs[*pdp])
769 						createEdge(cidx, createConfig(*pdp, sc));
770 			}
771 		}
772 
773 		string[] allconfigs_path;
774 		void determineAllConfigs(size_t pack_idx)
775 		{
776 			auto pack = &packages[pack_idx];
777 
778 			auto idx = allconfigs_path.countUntil(pack.name);
779 			enforce(idx < 0, format("Detected dependency cycle: %s", (allconfigs_path[idx .. $] ~ pack.name).join("->")));
780 			allconfigs_path ~= pack.name;
781 			scope (exit) {
782 				allconfigs_path.length--;
783 				allconfigs_path.assumeSafeAppend;
784 			}
785 
786 			// first, add all dependency configurations
787 			foreach (d; pack.dependencies)
788 				if (auto pi = d.name.toString() in packages)
789 					determineAllConfigs(*pi);
790 
791 			// for each configuration, determine the configurations usable for the dependencies
792 			if (auto pc = pack.name in m_overriddenConfigs)
793 				determineDependencyConfigs(pack_idx, *pc);
794 			else
795 				foreach (c; pack.package_.getPlatformConfigurations(platform, allow_non_library))
796 					determineDependencyConfigs(pack_idx, c);
797 		}
798 
799 
800 		// first, create a graph of all possible package configurations
801 		assert(packages[0].package_ is m_rootPackage);
802 		if (config.length) createConfig(0, config);
803 		determineAllConfigs(0);
804 
805 		// then, successively remove configurations until only one configuration
806 		// per package is left
807 		bool changed;
808 		do {
809 			// remove all configs that are not reachable by all parent packages
810 			changed = false;
811 			foreach (i, ref c; configs) {
812 				if (c == Vertex.init) continue; // ignore deleted configurations
813 				if (!isReachableByAllParentPacks(i)) {
814 					logDebug("%s %s NOT REACHABLE by all of (%s):", c.pack, c.config, packages[c.pack].parents);
815 					removeConfig(i);
816 					changed = true;
817 				}
818 			}
819 
820 			// when all edges are cleaned up, pick one package and remove all but one config
821 			if (!changed) {
822 				foreach (pidx; 0 .. packages.length) {
823 					size_t cnt = 0;
824 					foreach (i, ref c; configs)
825 						if (c.pack == pidx && ++cnt > 1) {
826 							logDebug("NON-PRIMARY: %s %s", c.pack, c.config);
827 							removeConfig(i);
828 						}
829 					if (cnt > 1) {
830 						changed = true;
831 						break;
832 					}
833 				}
834 			}
835 		} while (changed);
836 
837 		// print out the resulting tree
838 		foreach (e; edges) logDebug("    %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config);
839 
840 		// return the resulting configuration set as an AA
841 		string[string] ret;
842 		foreach (c; configs) {
843 			if (c == Vertex.init) continue; // ignore deleted configurations
844 			auto pname = packages[c.pack].name;
845 			assert(ret.get(pname, c.config) == c.config, format("Conflicting configurations for %s found: %s vs. %s", pname, c.config, ret[pname]));
846 			logDebug("Using configuration '%s' for %s", c.config, pname);
847 			ret[pname] = c.config;
848 		}
849 
850 		// check for conflicts (packages missing in the final configuration graph)
851 		auto visited = new bool[](packages.length);
852 		void checkPacksRec(size_t pack_idx) {
853 			if (visited[pack_idx]) return;
854 			visited[pack_idx] = true;
855 			auto pname = packages[pack_idx].name;
856 			auto pc = pname in ret;
857 			enforce(pc !is null, "Could not resolve configuration for package "~pname);
858 			foreach (p, dep; packages[pack_idx].package_.getDependencies(*pc)) {
859 				auto deppack = getDependency(p, dep.optional);
860 				if (deppack) checkPacksRec(packages[].countUntil!(p => p.package_ is deppack));
861 			}
862 		}
863 		checkPacksRec(0);
864 
865 		return ret;
866 	}
867 
868 	/** Returns an ordered list of all packages with the additional possibility
869 		to look up by name.
870 	*/
871 	private auto collectPackageInformation()
872 	const {
873 		static struct PackageInfo {
874 			const(Package) package_;
875 			size_t[] parents;
876 			string name;
877 			PackageDependency[] dependencies;
878 		}
879 
880 		static struct PackageInfoAccessor {
881 			private {
882 				PackageInfo[] m_packages;
883 				size_t[string] m_packageMap;
884 			}
885 
886 			private void initialize(P)(P all_packages, size_t reserve_count)
887 			{
888 				m_packages.reserve(reserve_count);
889 				foreach (p; all_packages) {
890 					auto pname = p.name;
891 					m_packageMap[pname] = m_packages.length;
892 					m_packages ~= PackageInfo(p, null, pname, p.getAllDependencies());
893 				}
894 				foreach (pack_idx, ref pack_info; m_packages)
895 					foreach (d; pack_info.dependencies)
896 						if (auto pi = d.name.toString() in m_packageMap)
897 							m_packages[*pi].parents ~= pack_idx;
898 			}
899 
900 			size_t length() const { return m_packages.length; }
901 			const(PackageInfo)[] opIndex() const { return m_packages; }
902 			ref const(PackageInfo) opIndex(size_t package_index) const { return m_packages[package_index]; }
903 			size_t getPackageIndex(string package_name) const { return m_packageMap.get(package_name, size_t.max); }
904 			const(size_t)* opBinaryRight(string op = "in")(string package_name) const { return package_name in m_packageMap; }
905 		}
906 
907 		PackageInfoAccessor ret;
908 		ret.initialize(getTopologicalPackageList(), m_dependencies.length);
909 		return ret;
910 	}
911 
912 	/**
913 	 * Fills `dst` with values from this project.
914 	 *
915 	 * `dst` gets initialized according to the given platform and config.
916 	 *
917 	 * Params:
918 	 *   dst = The BuildSettings struct to fill with data.
919 	 *   gsettings = The generator settings to retrieve the values for.
920 	 *   config = Values of the given configuration will be retrieved.
921 	 *   root_package = If non null, use it instead of the project's real root package.
922 	 *   shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary.
923 	 */
924 	void addBuildSettings(ref BuildSettings dst, in GeneratorSettings gsettings, string config, in Package root_package = null, bool shallow = false)
925 	const {
926 		import dub.internal.utils : stripDlangSpecialChars;
927 
928 		auto configs = getPackageConfigs(gsettings.platform, config);
929 
930 		foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) {
931 			auto pkg_path = pkg.path.toNativeString();
932 			dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]);
933 
934 			assert(pkg.name in configs, "Missing configuration for "~pkg.name);
935 			logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]);
936 
937 			auto psettings = pkg.getBuildSettings(gsettings.platform, configs[pkg.name]);
938 			if (psettings.targetType != TargetType.none) {
939 				if (shallow && pkg !is m_rootPackage)
940 					psettings.sourceFiles = null;
941 				processVars(dst, this, pkg, psettings, gsettings);
942 				if (!gsettings.single && psettings.importPaths.empty)
943 					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]);
944 				if (psettings.mainSourceFile.empty && pkg is m_rootPackage && psettings.targetType == TargetType.executable)
945 					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);
946 			}
947 			if (pkg is m_rootPackage) {
948 				if (!shallow) {
949 					enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build.");
950 					enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build.");
951 				}
952 				dst.targetType = psettings.targetType;
953 				dst.targetPath = psettings.targetPath;
954 				dst.targetName = psettings.targetName;
955 				if (!psettings.workingDirectory.empty)
956 					dst.workingDirectory = processVars(psettings.workingDirectory, this, pkg, gsettings, true, [dst.environments, dst.buildEnvironments]);
957 				if (psettings.mainSourceFile.length)
958 					dst.mainSourceFile = processVars(psettings.mainSourceFile, this, pkg, gsettings, true, [dst.environments, dst.buildEnvironments]);
959 			}
960 		}
961 
962 		// always add all version identifiers of all packages
963 		foreach (pkg; this.getTopologicalPackageList(false, null, configs)) {
964 			auto psettings = pkg.getBuildSettings(gsettings.platform, configs[pkg.name]);
965 			dst.addVersions(psettings.versions);
966 		}
967 	}
968 
969 	/** Fills `dst` with build settings specific to the given build type.
970 
971 		Params:
972 			dst = The `BuildSettings` instance to add the build settings to
973 			gsettings = Target generator settings
974 			for_root_package = Selects if the build settings are for the root
975 				package or for one of the dependencies. Unittest flags will
976 				only be added to the root package.
977 	*/
978 	void addBuildTypeSettings(ref BuildSettings dst, in GeneratorSettings gsettings, bool for_root_package = true)
979 	{
980 		bool usedefflags = !(dst.requirements & BuildRequirement.noDefaultFlags);
981 		if (usedefflags) {
982 			BuildSettings btsettings;
983 			m_rootPackage.addBuildTypeSettings(btsettings, gsettings.platform, gsettings.buildType);
984 
985 			if (!for_root_package) {
986 				// don't propagate unittest switch to dependencies, as dependent
987 				// unit tests aren't run anyway and the additional code may
988 				// cause linking to fail on Windows (issue #640)
989 				btsettings.removeOptions(BuildOption.unittests);
990 			}
991 
992 			processVars(dst, this, m_rootPackage, btsettings, gsettings);
993 		}
994 	}
995 
996 	/// Outputs a build description of the project, including its dependencies.
997 	ProjectDescription describe(GeneratorSettings settings)
998 	{
999 		import dub.generators.targetdescription;
1000 
1001 		// store basic build parameters
1002 		ProjectDescription ret;
1003 		ret.rootPackage = m_rootPackage.name;
1004 		ret.configuration = settings.config;
1005 		ret.buildType = settings.buildType;
1006 		ret.compiler = settings.platform.compiler;
1007 		ret.architecture = settings.platform.architecture;
1008 		ret.platform = settings.platform.platform;
1009 
1010 		// collect high level information about projects (useful for IDE display)
1011 		auto configs = getPackageConfigs(settings.platform, settings.config);
1012 		ret.packages ~= m_rootPackage.describe(settings.platform, settings.config);
1013 		foreach (dep; m_dependencies)
1014 			ret.packages ~= dep.describe(settings.platform, configs[dep.name]);
1015 
1016 		foreach (p; getTopologicalPackageList(false, null, configs))
1017 			ret.packages[ret.packages.countUntil!(pp => pp.name == p.name)].active = true;
1018 
1019 		if (settings.buildType.length) {
1020 			// collect build target information (useful for build tools)
1021 			auto gen = new TargetDescriptionGenerator(this);
1022 			try {
1023 				gen.generate(settings);
1024 				ret.targets = gen.targetDescriptions;
1025 				ret.targetLookup = gen.targetDescriptionLookup;
1026 			} catch (Exception e) {
1027 				logDiagnostic("Skipping targets description: %s", e.msg);
1028 				logDebug("Full error: %s", e.toString().sanitize);
1029 			}
1030 		}
1031 
1032 		return ret;
1033 	}
1034 
1035 	private string[] listBuildSetting(string attributeName)(ref GeneratorSettings settings,
1036 		string config, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
1037 	{
1038 		return listBuildSetting!attributeName(settings, getPackageConfigs(settings.platform, config),
1039 			projectDescription, compiler, disableEscaping);
1040 	}
1041 
1042 	private string[] listBuildSetting(string attributeName)(ref GeneratorSettings settings,
1043 		string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
1044 	{
1045 		if (compiler)
1046 			return formatBuildSettingCompiler!attributeName(settings, configs, projectDescription, compiler, disableEscaping);
1047 		else
1048 			return formatBuildSettingPlain!attributeName(settings, configs, projectDescription);
1049 	}
1050 
1051 	// Output a build setting formatted for a compiler
1052 	private string[] formatBuildSettingCompiler(string attributeName)(ref GeneratorSettings settings,
1053 		string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
1054 	{
1055 		import std.process : escapeShellFileName;
1056 		import std.path : dirSeparator;
1057 
1058 		assert(compiler);
1059 
1060 		auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage);
1061 		auto buildSettings = targetDescription.buildSettings;
1062 
1063 		string[] values;
1064 		switch (attributeName)
1065 		{
1066 		case "dflags":
1067 		case "linkerFiles":
1068 		case "mainSourceFile":
1069 		case "importFiles":
1070 		case "frameworks":
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 "frameworks":                 return listBuildSetting!"frameworks"(args);
1317 		case "linker-files":               return listBuildSetting!"linkerFiles"(args);
1318 		case "source-files":               return listBuildSetting!"sourceFiles"(args);
1319 		case "inject-source-files":        return listBuildSetting!"injectSourceFiles"(args);
1320 		case "copy-files":                 return listBuildSetting!"copyFiles"(args);
1321 		case "extra-dependency-files":     return listBuildSetting!"extraDependencyFiles"(args);
1322 		case "versions":                   return listBuildSetting!"versions"(args);
1323 		case "debug-versions":             return listBuildSetting!"debugVersions"(args);
1324 		case "import-paths":               return listBuildSetting!"importPaths"(args);
1325 		case "string-import-paths":        return listBuildSetting!"stringImportPaths"(args);
1326 		case "import-files":               return listBuildSetting!"importFiles"(args);
1327 		case "string-import-files":        return listBuildSetting!"stringImportFiles"(args);
1328 		case "pre-generate-commands":      return listBuildSetting!"preGenerateCommands"(args);
1329 		case "post-generate-commands":     return listBuildSetting!"postGenerateCommands"(args);
1330 		case "pre-build-commands":         return listBuildSetting!"preBuildCommands"(args);
1331 		case "post-build-commands":        return listBuildSetting!"postBuildCommands"(args);
1332 		case "pre-run-commands":           return listBuildSetting!"preRunCommands"(args);
1333 		case "post-run-commands":          return listBuildSetting!"postRunCommands"(args);
1334 		case "environments":               return listBuildSetting!"environments"(args);
1335 		case "build-environments":         return listBuildSetting!"buildEnvironments"(args);
1336 		case "run-environments":           return listBuildSetting!"runEnvironments"(args);
1337 		case "pre-generate-environments":  return listBuildSetting!"preGenerateEnvironments"(args);
1338 		case "post-generate-environments": return listBuildSetting!"postGenerateEnvironments"(args);
1339 		case "pre-build-environments":     return listBuildSetting!"preBuildEnvironments"(args);
1340 		case "post-build-environments":    return listBuildSetting!"postBuildEnvironments"(args);
1341 		case "pre-run-environments":       return listBuildSetting!"preRunEnvironments"(args);
1342 		case "post-run-environments":      return listBuildSetting!"postRunEnvironments"(args);
1343 		case "requirements":               return listBuildSetting!"requirements"(args);
1344 		case "options":                    return listBuildSetting!"options"(args);
1345 		case "default-config":             return [getDefaultConfiguration(settings.platform)];
1346 		case "configs":                    return configurations;
1347 		case "default-build":              return [builds[0]];
1348 		case "builds":                     return builds;
1349 
1350 		default:
1351 			enforce(false, "--data="~requestedData~
1352 				" is not a valid option. See 'dub describe --help' for accepted --data= values.");
1353 		}
1354 
1355 		assert(0);
1356 	}
1357 
1358 	/// Outputs requested data for the project, optionally including its dependencies.
1359 	string[] listBuildSettings(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type)
1360 	{
1361 		import dub.compilers.utils : isLinkerFile;
1362 
1363 		auto projectDescription = describe(settings);
1364 		auto configs = getPackageConfigs(settings.platform, settings.config);
1365 		PackageDescription packageDescription;
1366 		foreach (pack; projectDescription.packages) {
1367 			if (pack.name == projectDescription.rootPackage)
1368 				packageDescription = pack;
1369 		}
1370 
1371 		if (projectDescription.rootPackage in projectDescription.targetLookup) {
1372 			// Copy linker files from sourceFiles to linkerFiles
1373 			auto target = projectDescription.lookupTarget(projectDescription.rootPackage);
1374 			foreach (file; target.buildSettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)))
1375 				target.buildSettings.addLinkerFiles(file);
1376 
1377 			// Remove linker files from sourceFiles
1378 			target.buildSettings.sourceFiles =
1379 				target.buildSettings.sourceFiles
1380 				.filter!(a => !isLinkerFile(settings.platform, a))
1381 				.array();
1382 			projectDescription.lookupTarget(projectDescription.rootPackage) = target;
1383 		}
1384 
1385 		Compiler compiler;
1386 		bool no_escape;
1387 		final switch (list_type) with (ListBuildSettingsFormat) {
1388 			case list: break;
1389 			case listNul: no_escape = true; break;
1390 			case commandLine: compiler = settings.compiler; break;
1391 			case commandLineNul: compiler = settings.compiler; no_escape = true; break;
1392 
1393 		}
1394 
1395 		auto result = requestedData
1396 			.map!(dataName => listBuildSetting(settings, configs, projectDescription, dataName, compiler, no_escape));
1397 
1398 		final switch (list_type) with (ListBuildSettingsFormat) {
1399 			case list: return result.map!(l => l.join("\n")).array();
1400 			case listNul: return result.map!(l => l.join("\0")).array;
1401 			case commandLine: return result.map!(l => l.join(" ")).array;
1402 			case commandLineNul: return result.map!(l => l.join("\0")).array;
1403 		}
1404 	}
1405 
1406 	/** Saves the currently selected dependency versions to disk.
1407 
1408 		The selections will be written to a file named
1409 		`SelectedVersions.defaultFile` ("dub.selections.json") within the
1410 		directory of the root package. Any existing file will get overwritten.
1411 	*/
1412 	void saveSelections()
1413 	{
1414 		assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections).");
1415 		const name = PackageName(m_rootPackage.basePackage.name);
1416 		if (m_selections.hasSelectedVersion(name))
1417 			m_selections.deselectVersion(name);
1418 		this.m_packageManager.writeSelections(
1419 			this.m_rootPackage, this.m_selections.m_selections,
1420 			this.m_selections.dirty);
1421 	}
1422 
1423 	deprecated bool isUpgradeCacheUpToDate()
1424 	{
1425 		return false;
1426 	}
1427 
1428 	deprecated Dependency[string] getUpgradeCache()
1429 	{
1430 		return null;
1431 	}
1432 }
1433 
1434 
1435 /// Determines the output format used for `Project.listBuildSettings`.
1436 enum ListBuildSettingsFormat {
1437 	list,           /// Newline separated list entries
1438 	listNul,        /// NUL character separated list entries (unescaped)
1439 	commandLine,    /// Formatted for compiler command line (one data list per line)
1440 	commandLineNul, /// NUL character separated list entries (unescaped, data lists separated by two NUL characters)
1441 }
1442 
1443 deprecated("Use `dub.packagemanager : PlacementLocation` instead")
1444 public alias PlacementLocation = dub.packagemanager.PlacementLocation;
1445 
1446 void processVars(ref BuildSettings dst, in Project project, in Package pack,
1447 	BuildSettings settings, in GeneratorSettings gsettings, bool include_target_settings = false)
1448 {
1449 	string[string] processVerEnvs(in string[string] targetEnvs, in string[string] defaultEnvs)
1450 	{
1451 		string[string] retEnv;
1452 		foreach (k, v; targetEnvs)
1453 			retEnv[k] = v;
1454 		foreach (k, v; defaultEnvs) {
1455 			if (k !in targetEnvs)
1456 				retEnv[k] = v;
1457 		}
1458 		return processVars(project, pack, gsettings, retEnv);
1459 	}
1460 	dst.addEnvironments(processVerEnvs(settings.environments, gsettings.buildSettings.environments));
1461 	dst.addBuildEnvironments(processVerEnvs(settings.buildEnvironments, gsettings.buildSettings.buildEnvironments));
1462 	dst.addRunEnvironments(processVerEnvs(settings.runEnvironments, gsettings.buildSettings.runEnvironments));
1463 	dst.addPreGenerateEnvironments(processVerEnvs(settings.preGenerateEnvironments, gsettings.buildSettings.preGenerateEnvironments));
1464 	dst.addPostGenerateEnvironments(processVerEnvs(settings.postGenerateEnvironments, gsettings.buildSettings.postGenerateEnvironments));
1465 	dst.addPreBuildEnvironments(processVerEnvs(settings.preBuildEnvironments, gsettings.buildSettings.preBuildEnvironments));
1466 	dst.addPostBuildEnvironments(processVerEnvs(settings.postBuildEnvironments, gsettings.buildSettings.postBuildEnvironments));
1467 	dst.addPreRunEnvironments(processVerEnvs(settings.preRunEnvironments, gsettings.buildSettings.preRunEnvironments));
1468 	dst.addPostRunEnvironments(processVerEnvs(settings.postRunEnvironments, gsettings.buildSettings.postRunEnvironments));
1469 
1470 	auto buildEnvs = [dst.environments, dst.buildEnvironments];
1471 
1472 	dst.addDFlags(processVars(project, pack, gsettings, settings.dflags, false, buildEnvs));
1473 	dst.addLFlags(processVars(project, pack, gsettings, settings.lflags, false, buildEnvs));
1474 	dst.addLibs(processVars(project, pack, gsettings, settings.libs, false, buildEnvs));
1475 	dst.addFrameworks(processVars(project, pack, gsettings, settings.frameworks, false, buildEnvs));
1476 	dst.addSourceFiles(processVars!true(project, pack, gsettings, settings.sourceFiles, true, buildEnvs));
1477 	dst.addImportFiles(processVars(project, pack, gsettings, settings.importFiles, true, buildEnvs));
1478 	dst.addStringImportFiles(processVars(project, pack, gsettings, settings.stringImportFiles, true, buildEnvs));
1479 	dst.addInjectSourceFiles(processVars!true(project, pack, gsettings, settings.injectSourceFiles, true, buildEnvs));
1480 	dst.addCopyFiles(processVars(project, pack, gsettings, settings.copyFiles, true, buildEnvs));
1481 	dst.addExtraDependencyFiles(processVars(project, pack, gsettings, settings.extraDependencyFiles, true, buildEnvs));
1482 	dst.addVersions(processVars(project, pack, gsettings, settings.versions, false, buildEnvs));
1483 	dst.addDebugVersions(processVars(project, pack, gsettings, settings.debugVersions, false, buildEnvs));
1484 	dst.addVersionFilters(processVars(project, pack, gsettings, settings.versionFilters, false, buildEnvs));
1485 	dst.addDebugVersionFilters(processVars(project, pack, gsettings, settings.debugVersionFilters, false, buildEnvs));
1486 	dst.addImportPaths(processVars(project, pack, gsettings, settings.importPaths, true, buildEnvs));
1487 	dst.addCImportPaths(processVars(project, pack, gsettings, settings.cImportPaths, true, buildEnvs));
1488 	dst.addStringImportPaths(processVars(project, pack, gsettings, settings.stringImportPaths, true, buildEnvs));
1489 	dst.addRequirements(settings.requirements);
1490 	dst.addOptions(settings.options);
1491 
1492 	// commands are substituted in dub.generators.generator : runBuildCommands
1493 	dst.addPreGenerateCommands(settings.preGenerateCommands);
1494 	dst.addPostGenerateCommands(settings.postGenerateCommands);
1495 	dst.addPreBuildCommands(settings.preBuildCommands);
1496 	dst.addPostBuildCommands(settings.postBuildCommands);
1497 	dst.addPreRunCommands(settings.preRunCommands);
1498 	dst.addPostRunCommands(settings.postRunCommands);
1499 
1500 	if (include_target_settings) {
1501 		dst.targetType = settings.targetType;
1502 		dst.targetPath = processVars(settings.targetPath, project, pack, gsettings, true, buildEnvs);
1503 		dst.targetName = settings.targetName;
1504 		if (!settings.workingDirectory.empty)
1505 			dst.workingDirectory = processVars(settings.workingDirectory, project, pack, gsettings, true, buildEnvs);
1506 		if (settings.mainSourceFile.length)
1507 			dst.mainSourceFile = processVars(settings.mainSourceFile, project, pack, gsettings, true, buildEnvs);
1508 	}
1509 }
1510 
1511 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)
1512 {
1513 	auto ret = appender!(string[])();
1514 	processVars!glob(ret, project, pack, gsettings, vars, are_paths, extraVers);
1515 	return ret.data;
1516 }
1517 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)
1518 {
1519 	static if (glob)
1520 		alias process = processVarsWithGlob!(Project, Package);
1521 	else
1522 		alias process = processVars!(Project, Package);
1523 	foreach (var; vars)
1524 		dst.put(process(var, project, pack, gsettings, are_paths, extraVers));
1525 }
1526 
1527 string processVars(Project, Package)(string var, in Project project, in Package pack, in GeneratorSettings gsettings, bool is_path, in string[string][] extraVers = null)
1528 {
1529 	var = var.expandVars!(varName => getVariable(varName, project, pack, gsettings, extraVers));
1530 	if (!is_path)
1531 		return var;
1532 	auto p = NativePath(var);
1533 	if (!p.absolute)
1534 		return (pack.path ~ p).toNativeString();
1535 	else
1536 		return p.toNativeString();
1537 }
1538 string[string] processVars(bool glob = false)(in Project project, in Package pack, in GeneratorSettings gsettings, in string[string] vars, in string[string][] extraVers = null)
1539 {
1540 	string[string] ret;
1541 	processVars!glob(ret, project, pack, gsettings, vars, extraVers);
1542 	return ret;
1543 }
1544 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)
1545 {
1546 	static if (glob)
1547 		alias process = processVarsWithGlob!(Project, Package);
1548 	else
1549 		alias process = processVars!(Project, Package);
1550 	foreach (k, var; vars)
1551 		dst[k] = process(var, project, pack, gsettings, false, extraVers);
1552 }
1553 
1554 private string[] processVarsWithGlob(Project, Package)(string var, in Project project, in Package pack, in GeneratorSettings gsettings, bool is_path, in string[string][] extraVers)
1555 {
1556 	assert(is_path, "can't glob something that isn't a path");
1557 	string res = processVars(var, project, pack, gsettings, is_path, extraVers);
1558 	// Find the unglobbed prefix and iterate from there.
1559 	size_t i = 0;
1560 	size_t sepIdx = 0;
1561 	loop: while (i < res.length) {
1562 		switch_: switch (res[i])
1563 		{
1564 		case '*', '?', '[', '{': break loop;
1565 		case '/': sepIdx = i; goto default;
1566 		version (Windows) { case '\\': sepIdx = i; goto default; }
1567 		default: ++i; break switch_;
1568 		}
1569 	}
1570 	if (i == res.length) //no globbing found in the path
1571 		return [res];
1572 	import std.file : dirEntries, SpanMode;
1573 	import std.path : buildNormalizedPath, globMatch, isAbsolute, relativePath;
1574 	auto cwd = gsettings.toolWorkingDirectory.toNativeString;
1575 	auto path = res[0 .. sepIdx];
1576 	bool prependCwd = false;
1577 	if (!isAbsolute(path))
1578 	{
1579 		prependCwd = true;
1580 		path = buildNormalizedPath(cwd, path);
1581 	}
1582 
1583 	return dirEntries(path, SpanMode.depth)
1584 		.map!(de => prependCwd
1585 			? de.name.relativePath(cwd)
1586 			: de.name)
1587 		.filter!(name => globMatch(name, res))
1588 		.array;
1589 }
1590 /// Expand variables using `$VAR_NAME` or `${VAR_NAME}` syntax.
1591 /// `$$` escapes itself and is expanded to a single `$`.
1592 private string expandVars(alias expandVar)(string s)
1593 {
1594 	import std.functional : not;
1595 
1596 	auto result = appender!string;
1597 
1598 	static bool isVarChar(char c)
1599 	{
1600 		import std.ascii;
1601 		return isAlphaNum(c) || c == '_';
1602 	}
1603 
1604 	while (true)
1605 	{
1606 		auto pos = s.indexOf('$');
1607 		if (pos < 0)
1608 		{
1609 			result.put(s);
1610 			return result.data;
1611 		}
1612 		result.put(s[0 .. pos]);
1613 		s = s[pos + 1 .. $];
1614 		enforce(s.length > 0, "Variable name expected at end of string");
1615 		switch (s[0])
1616 		{
1617 			case '$':
1618 				result.put("$");
1619 				s = s[1 .. $];
1620 				break;
1621 			case '{':
1622 				pos = s.indexOf('}');
1623 				enforce(pos >= 0, "Could not find '}' to match '${'");
1624 				result.put(expandVar(s[1 .. pos]));
1625 				s = s[pos + 1 .. $];
1626 				break;
1627 			default:
1628 				pos = s.representation.countUntil!(not!isVarChar);
1629 				if (pos < 0)
1630 					pos = s.length;
1631 				result.put(expandVar(s[0 .. pos]));
1632 				s = s[pos .. $];
1633 				break;
1634 		}
1635 	}
1636 }
1637 
1638 unittest
1639 {
1640 	string[string] vars =
1641 	[
1642 		"A" : "a",
1643 		"B" : "b",
1644 	];
1645 
1646 	string expandVar(string name) { auto p = name in vars; enforce(p, name); return *p; }
1647 
1648 	assert(expandVars!expandVar("") == "");
1649 	assert(expandVars!expandVar("x") == "x");
1650 	assert(expandVars!expandVar("$$") == "$");
1651 	assert(expandVars!expandVar("x$$") == "x$");
1652 	assert(expandVars!expandVar("$$x") == "$x");
1653 	assert(expandVars!expandVar("$$$$") == "$$");
1654 	assert(expandVars!expandVar("x$A") == "xa");
1655 	assert(expandVars!expandVar("x$$A") == "x$A");
1656 	assert(expandVars!expandVar("$A$B") == "ab");
1657 	assert(expandVars!expandVar("${A}$B") == "ab");
1658 	assert(expandVars!expandVar("$A${B}") == "ab");
1659 	assert(expandVars!expandVar("a${B}") == "ab");
1660 	assert(expandVars!expandVar("${A}b") == "ab");
1661 
1662 	import std.exception : assertThrown;
1663 	assertThrown(expandVars!expandVar("$"));
1664 	assertThrown(expandVars!expandVar("${}"));
1665 	assertThrown(expandVars!expandVar("$|"));
1666 	assertThrown(expandVars!expandVar("x$"));
1667 	assertThrown(expandVars!expandVar("$X"));
1668 	assertThrown(expandVars!expandVar("${"));
1669 	assertThrown(expandVars!expandVar("${X"));
1670 
1671 	// https://github.com/dlang/dmd/pull/9275
1672 	assert(expandVars!expandVar("$${DUB_EXE:-dub}") == "${DUB_EXE:-dub}");
1673 }
1674 
1675 /// Expands the variables in the input string with the same rules as command
1676 /// variables inside custom dub commands.
1677 ///
1678 /// Params:
1679 ///     s = the input string where environment variables in form `$VAR` should be replaced
1680 ///     throwIfMissing = if true, throw an exception if the given variable is not found,
1681 ///                      otherwise replace unknown variables with the empty string.
1682 string expandEnvironmentVariables(string s, bool throwIfMissing = true)
1683 {
1684 	import std.process : environment;
1685 
1686 	return expandVars!((v) {
1687 		auto ret = environment.get(v);
1688 		if (ret is null && throwIfMissing)
1689 			throw new Exception("Specified environment variable `$" ~ v ~ "` is not set");
1690 		return ret;
1691 	})(s);
1692 }
1693 
1694 // Keep the following list up-to-date if adding more build settings variables.
1695 /// List of variables that can be used in build settings
1696 package(dub) immutable buildSettingsVars = [
1697 	"ARCH", "PLATFORM", "PLATFORM_POSIX", "BUILD_TYPE"
1698 ];
1699 
1700 private string getVariable(Project, Package)(string name, in Project project, in Package pack, in GeneratorSettings gsettings, in string[string][] extraVars = null)
1701 {
1702 	import dub.internal.utils : getDUBExePath;
1703 	import std.process : environment, escapeShellFileName;
1704 	import std.uni : asUpperCase;
1705 
1706 	NativePath path;
1707 	if (name == "PACKAGE_DIR")
1708 		path = pack.path;
1709 	else if (name == "ROOT_PACKAGE_DIR")
1710 		path = project.rootPackage.path;
1711 
1712 	if (name.endsWith("_PACKAGE_DIR")) {
1713 		auto pname = name[0 .. $-12];
1714 		foreach (prj; project.getTopologicalPackageList())
1715 			if (prj.name.asUpperCase.map!(a => a == '-' ? '_' : a).equal(pname))
1716 			{
1717 				path = prj.path;
1718 				break;
1719 			}
1720 	}
1721 
1722 	if (!path.empty)
1723 	{
1724 		// no trailing slash for clean path concatenation (see #1392)
1725 		path.endsWithSlash = false;
1726 		return path.toNativeString();
1727 	}
1728 
1729 	if (name == "DUB") {
1730 		return getDUBExePath(gsettings.platform.compilerBinary).toNativeString();
1731 	}
1732 
1733 	if (name == "ARCH") {
1734 		foreach (a; gsettings.platform.architecture)
1735 			return a;
1736 		return "";
1737 	}
1738 
1739 	if (name == "PLATFORM") {
1740 		import std.algorithm : filter;
1741 		foreach (p; gsettings.platform.platform.filter!(p => p != "posix"))
1742 			return p;
1743 		foreach (p; gsettings.platform.platform)
1744 			return p;
1745 		return "";
1746 	}
1747 
1748 	if (name == "PLATFORM_POSIX") {
1749 		import std.algorithm : canFind;
1750 		if (gsettings.platform.platform.canFind("posix"))
1751 			return "posix";
1752 		foreach (p; gsettings.platform.platform)
1753 			return p;
1754 		return "";
1755 	}
1756 
1757 	if (name == "BUILD_TYPE") return gsettings.buildType;
1758 
1759 	if (name == "DFLAGS" || name == "LFLAGS")
1760 	{
1761 		auto buildSettings = pack.getBuildSettings(gsettings.platform, gsettings.config);
1762 		if (name == "DFLAGS")
1763 			return join(buildSettings.dflags," ");
1764 		else if (name == "LFLAGS")
1765 			return join(buildSettings.lflags," ");
1766 	}
1767 
1768 	import std.range;
1769 	foreach (aa; retro(extraVars))
1770 		if (auto exvar = name in aa)
1771 			return *exvar;
1772 
1773 	auto envvar = environment.get(name);
1774 	if (envvar !is null) return envvar;
1775 
1776 	throw new Exception("Invalid variable: "~name);
1777 }
1778 
1779 
1780 unittest
1781 {
1782 	static struct MockPackage
1783 	{
1784 		this(string name)
1785 		{
1786 			this.name = name;
1787 			version (Posix)
1788 				path = NativePath("/pkgs/"~name);
1789 			else version (Windows)
1790 				path = NativePath(`C:\pkgs\`~name);
1791 			// see 4d4017c14c, #268, and #1392 for why this all package paths end on slash internally
1792 			path.endsWithSlash = true;
1793 		}
1794 		string name;
1795 		NativePath path;
1796 		BuildSettings getBuildSettings(in BuildPlatform platform, string config) const
1797 		{
1798 			return BuildSettings();
1799 		}
1800 	}
1801 
1802 	static struct MockProject
1803 	{
1804 		MockPackage rootPackage;
1805 		inout(MockPackage)[] getTopologicalPackageList() inout
1806 		{
1807 			return _dependencies;
1808 		}
1809 	private:
1810 		MockPackage[] _dependencies;
1811 	}
1812 
1813 	MockProject proj = {
1814 		rootPackage: MockPackage("root"),
1815 		_dependencies: [MockPackage("dep1"), MockPackage("dep2")]
1816 	};
1817 	auto pack = MockPackage("test");
1818 	GeneratorSettings gsettings;
1819 	enum isPath = true;
1820 
1821 	import std.path : dirSeparator;
1822 
1823 	static NativePath woSlash(NativePath p) { p.endsWithSlash = false; return p; }
1824 	// basic vars
1825 	assert(processVars("Hello $PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(pack.path).toNativeString);
1826 	assert(processVars("Hello $ROOT_PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(proj.rootPackage.path).toNativeString.chomp(dirSeparator));
1827 	assert(processVars("Hello $DEP1_PACKAGE_DIR", proj, pack, gsettings, !isPath) == "Hello "~woSlash(proj._dependencies[0].path).toNativeString);
1828 	// ${VAR} replacements
1829 	assert(processVars("Hello ${PACKAGE_DIR}"~dirSeparator~"foobar", proj, pack, gsettings, !isPath) == "Hello "~(pack.path ~ "foobar").toNativeString);
1830 	assert(processVars("Hello $PACKAGE_DIR"~dirSeparator~"foobar", proj, pack, gsettings, !isPath) == "Hello "~(pack.path ~ "foobar").toNativeString);
1831 	// test with isPath
1832 	assert(processVars("local", proj, pack, gsettings, isPath) == (pack.path ~ "local").toNativeString);
1833 	assert(processVars("foo/$$ESCAPED", proj, pack, gsettings, isPath) == (pack.path ~ "foo/$ESCAPED").toNativeString);
1834 	assert(processVars("$$ESCAPED", proj, pack, gsettings, !isPath) == "$ESCAPED");
1835 	// test other env variables
1836 	import std.process : environment;
1837 	environment["MY_ENV_VAR"] = "blablabla";
1838 	assert(processVars("$MY_ENV_VAR", proj, pack, gsettings, !isPath) == "blablabla");
1839 	assert(processVars("${MY_ENV_VAR}suffix", proj, pack, gsettings, !isPath) == "blablablasuffix");
1840 	assert(processVars("$MY_ENV_VAR-suffix", proj, pack, gsettings, !isPath) == "blablabla-suffix");
1841 	assert(processVars("$MY_ENV_VAR:suffix", proj, pack, gsettings, !isPath) == "blablabla:suffix");
1842 	assert(processVars("$MY_ENV_VAR$MY_ENV_VAR", proj, pack, gsettings, !isPath) == "blablablablablabla");
1843 	environment.remove("MY_ENV_VAR");
1844 }
1845 
1846 /**
1847  * Holds and stores a set of version selections for package dependencies.
1848  *
1849  * This is the runtime representation of the information contained in
1850  * "dub.selections.json" within a package's directory.
1851  *
1852  * Note that as subpackages share the same version as their main package,
1853  * this class will treat any subpackage reference as a reference to its
1854  * main package.
1855  */
1856 public class SelectedVersions {
1857 	protected {
1858 		enum FileVersion = 1;
1859 		Selections!1 m_selections;
1860 		bool m_dirty = false; // has changes since last save
1861 		bool m_bare = true;
1862 	}
1863 
1864 	/// Default file name to use for storing selections.
1865 	enum defaultFile = "dub.selections.json";
1866 
1867 	/// Constructs a new empty version selection.
1868 	public this(uint version_ = FileVersion) @safe pure
1869 	{
1870 		enforce(version_ == 1, "Unsupported file version");
1871 		this.m_selections = Selections!1(version_);
1872 	}
1873 
1874 	/// Constructs a new non-empty version selection.
1875 	public this(Selections!1 data) @safe pure nothrow @nogc
1876 	{
1877 		this.m_selections = data;
1878 		this.m_bare = false;
1879 	}
1880 
1881 	/** Constructs a new non-empty version selection, prefixing relative path
1882 		selections with the specified prefix.
1883 
1884 		To be used in cases where the "dub.selections.json" file isn't located
1885 		in the root package directory.
1886 	*/
1887 	public this(Selections!1 data, NativePath relPathPrefix)
1888 	{
1889 		this(data);
1890 		if (relPathPrefix.empty) return;
1891 		foreach (ref dep; m_selections.versions.byValue) {
1892 			const depPath = dep.path;
1893 			if (!depPath.empty && !depPath.absolute)
1894 				dep = Dependency(relPathPrefix ~ depPath);
1895 		}
1896 	}
1897 
1898 	/** Constructs a new version selection from JSON data.
1899 
1900 		The structure of the JSON document must match the contents of the
1901 		"dub.selections.json" file.
1902 	*/
1903 	deprecated("Pass a `dub.recipe.selection : Selected` directly")
1904 	this(Json data)
1905 	{
1906 		deserialize(data);
1907 		m_dirty = false;
1908 	}
1909 
1910 	/** Constructs a new version selections from an existing JSON file.
1911 	*/
1912 	deprecated("JSON deserialization is deprecated")
1913 	this(NativePath path)
1914 	{
1915 		auto json = jsonFromFile(path);
1916 		deserialize(json);
1917 		m_dirty = false;
1918 		m_bare = false;
1919 	}
1920 
1921 	/// Returns a list of names for all packages that have a version selection.
1922 	@property string[] selectedPackages() const { return m_selections.versions.keys; }
1923 
1924 	/// Determines if any changes have been made after loading the selections from a file.
1925 	@property bool dirty() const { return m_dirty; }
1926 
1927 	/// Determine if this set of selections is still empty (but not `clear`ed).
1928 	@property bool bare() const { return m_bare && !m_dirty; }
1929 
1930 	/// Removes all selections.
1931 	void clear()
1932 	{
1933 		m_selections.versions = null;
1934 		m_dirty = true;
1935 	}
1936 
1937 	/// Duplicates the set of selected versions from another instance.
1938 	void set(SelectedVersions versions)
1939 	{
1940 		m_selections.fileVersion = versions.m_selections.fileVersion;
1941 		m_selections.versions = versions.m_selections.versions.dup;
1942 		m_selections.inheritable = versions.m_selections.inheritable;
1943 		m_dirty = true;
1944 	}
1945 
1946 	/// Selects a certain version for a specific package.
1947 	deprecated("Use the overload that accepts a `PackageName`")
1948 	void selectVersion(string package_id, Version version_)
1949 	{
1950 		const name = PackageName(package_id);
1951 		return this.selectVersionInternal(name, Dependency(version_));
1952 	}
1953 
1954 	/// Ditto
1955 	void selectVersion(in PackageName name, Version version_, in IntegrityTag tag = IntegrityTag.init)
1956 	{
1957 		auto dep = SelectedDependency(Dependency(version_), tag);
1958 		if (auto pdep = name.main.toString() in this.m_selections.versions) {
1959 			if (*pdep == dep)
1960 				return;
1961 		}
1962 		this.m_selections.versions[name.main.toString()] = dep;
1963 		this.m_dirty = true;
1964 	}
1965 
1966 	/// Selects a certain path for a specific package.
1967 	deprecated("Use the overload that accepts a `PackageName`")
1968 	void selectVersion(string package_id, NativePath path)
1969 	{
1970 		const name = PackageName(package_id);
1971 		return this.selectVersion(name, path);
1972 	}
1973 
1974 	/// Ditto
1975 	void selectVersion(in PackageName name, NativePath path)
1976 	{
1977 		const dep = Dependency(path);
1978 		this.selectVersionInternal(name, dep);
1979 	}
1980 
1981 	/// Selects a certain Git reference for a specific package.
1982 	deprecated("Use the overload that accepts a `PackageName`")
1983 	void selectVersion(string package_id, Repository repository)
1984 	{
1985 		const name = PackageName(package_id);
1986 		return this.selectVersion(name, repository);
1987 	}
1988 
1989 	/// Ditto
1990 	void selectVersion(in PackageName name, Repository repository)
1991 	{
1992 		const dep = Dependency(repository);
1993 		this.selectVersionInternal(name, dep);
1994 	}
1995 
1996 	/// Internal implementation of selectVersion
1997 	private void selectVersionInternal(in PackageName name, in Dependency dep)
1998 	{
1999 		if (auto pdep = name.main.toString() in m_selections.versions) {
2000 			if (*pdep == dep)
2001 				return;
2002 		}
2003 		m_selections.versions[name.main.toString()] = dep;
2004 		m_dirty = true;
2005 	}
2006 
2007 	deprecated("Move `spec` inside of the `repository` parameter and call `selectVersion`")
2008 	void selectVersionWithRepository(string package_id, Repository repository, string spec)
2009 	{
2010 		this.selectVersion(package_id, Repository(repository.remote(), spec));
2011 	}
2012 
2013 	/// Removes the selection for a particular package.
2014 	deprecated("Use the overload that accepts a `PackageName`")
2015 	void deselectVersion(string package_id)
2016 	{
2017 		const n = PackageName(package_id);
2018 		this.deselectVersion(n);
2019 	}
2020 
2021 	/// Ditto
2022 	void deselectVersion(in PackageName name)
2023 	{
2024 		m_selections.versions.remove(name.main.toString());
2025 		m_dirty = true;
2026 	}
2027 
2028 	/// Determines if a particular package has a selection set.
2029 	deprecated("Use the overload that accepts a `PackageName`")
2030 	bool hasSelectedVersion(string packageId) const {
2031 		const name = PackageName(packageId);
2032 		return this.hasSelectedVersion(name);
2033 	}
2034 
2035 	/// Ditto
2036 	bool hasSelectedVersion(in PackageName name) const
2037 	{
2038 		return (name.main.toString() in m_selections.versions) !is null;
2039 	}
2040 
2041 	/** Returns the selection for a particular package.
2042 
2043 		Note that the returned `Dependency` can either have the
2044 		`Dependency.path` property set to a non-empty value, in which case this
2045 		is a path based selection, or its `Dependency.version_` property is
2046 		valid and it is a version selection.
2047 	*/
2048 	deprecated("Use the overload that accepts a `PackageName`")
2049 	Dependency getSelectedVersion(string packageId) const
2050 	{
2051 		const name = PackageName(packageId);
2052 		return this.getSelectedVersion(name);
2053 	}
2054 
2055 	/// Ditto
2056 	Dependency getSelectedVersion(in PackageName name) const
2057 	{
2058 		enforce(hasSelectedVersion(name));
2059 		return m_selections.versions[name.main.toString()];
2060 	}
2061 
2062 	/// Returns: The `IntegrityTag` associated to the version, or `.init` if none
2063 	IntegrityTag getIntegrityTag(in PackageName name) const
2064 	{
2065 		if (auto ptr = name.main.toString() in this.m_selections.versions)
2066 			return (*ptr).integrity;
2067 		return typeof(return).init;
2068 	}
2069 
2070 	/** Stores the selections to disk.
2071 
2072 		The target file will be written in JSON format. Usually, `defaultFile`
2073 		should be used as the file name and the directory should be the root
2074 		directory of the project's root package.
2075 	*/
2076 	deprecated("Use `PackageManager.writeSelections` to write a `SelectionsFile`")
2077 	void save(NativePath path)
2078 	{
2079 		path.writeFile(PackageManager.selectionsToString(this.m_selections));
2080 		m_dirty = false;
2081 		m_bare = false;
2082 	}
2083 
2084 	deprecated("Use `dub.dependency : Dependency.toJson(true)`")
2085 	static Json dependencyToJson(Dependency d)
2086 	{
2087 		return d.toJson(true);
2088 	}
2089 
2090 	deprecated("JSON deserialization is deprecated")
2091 	static Dependency dependencyFromJson(Json j)
2092 	{
2093 		if (j.type == Json.Type..string)
2094 			return Dependency(Version(j.get!string));
2095 		else if (j.type == Json.Type.object && "path" in j)
2096 			return Dependency(NativePath(j["path"].get!string));
2097 		else if (j.type == Json.Type.object && "repository" in j)
2098 			return Dependency(Repository(j["repository"].get!string,
2099 				enforce("version" in j, "Expected \"version\" field in repository version object").get!string));
2100 		else throw new Exception(format("Unexpected type for dependency: %s", j));
2101 	}
2102 
2103 	deprecated("JSON serialization is deprecated")
2104 	Json serialize() const {
2105 		return PackageManager.selectionsToJSON(this.m_selections);
2106 	}
2107 
2108 	deprecated("JSON deserialization is deprecated")
2109 	private void deserialize(Json json)
2110 	{
2111 		const fileVersion = json["fileVersion"].get!int;
2112 		enforce(fileVersion == FileVersion, "Mismatched dub.selections.json version: " ~ to!string(fileVersion) ~ " vs. " ~ to!string(FileVersion));
2113 		clear();
2114 		m_selections.fileVersion = fileVersion;
2115 		scope(failure) clear();
2116 		if (auto p = "inheritable" in json)
2117 			m_selections.inheritable = p.get!bool;
2118 		foreach (string p, dep; json["versions"])
2119 			m_selections.versions[p] = dependencyFromJson(dep);
2120 	}
2121 }
2122 
2123 /// The template code from which the test runner is generated
2124 private immutable TestRunnerTemplate = q{
2125 deprecated // allow silently using deprecated symbols
2126 module dub_test_root;
2127 
2128 import std.typetuple;
2129 
2130 %-(static import %s;
2131 %);
2132 
2133 alias allModules = TypeTuple!(
2134     %-(%s, %)
2135 );
2136 
2137 %s
2138 };
2139 
2140 /// The default test runner that gets used if none is provided
2141 private immutable DefaultTestRunnerCode = q{
2142 	version(D_BetterC) {
2143 		extern(C) int main() {
2144 			foreach (module_; allModules) {
2145 				foreach (unitTest; __traits(getUnitTests, module_)) {
2146 					unitTest();
2147 				}
2148 			}
2149 			import core.stdc.stdio : puts;
2150 			puts("All unit tests have been run successfully.");
2151 			return 0;
2152 		}
2153 	} else {
2154 		void main() {
2155 			version (D_Coverage) {
2156 			} else {
2157 				import std.stdio : writeln;
2158 				writeln("All unit tests have been run successfully.");
2159 			}
2160 		}
2161 		shared static this() {
2162 			version (Have_tested) {
2163 				import tested;
2164 				import core.runtime;
2165 				import std.exception;
2166 				Runtime.moduleUnitTester = () => true;
2167 				enforce(runUnitTests!allModules(new ConsoleTestResultWriter), "Unit tests failed.");
2168 			}
2169 		}
2170 	}
2171 };