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.internal.utils;
14 import dub.internal.vibecompat.core.file;
15 import dub.internal.vibecompat.core.log;
16 import dub.internal.vibecompat.data.json;
17 import dub.internal.vibecompat.inet.url;
18 import dub.package_;
19 import dub.packagemanager;
20 import dub.packagesupplier;
21 import dub.generators.generator;
22 
23 import std.algorithm;
24 import std.array;
25 import std.conv : to;
26 import std.datetime;
27 import std.exception : enforce;
28 import std..string;
29 import std.encoding : sanitize;
30 
31 /**
32 	Represents a full project, a root package with its dependencies and package
33 	selection.
34 
35 	All dependencies must be available locally so that the package dependency
36 	graph can be built. Use `Project.reinit` if necessary for reloading
37 	dependencies after more packages are available.
38 */
39 class Project {
40 	private {
41 		PackageManager m_packageManager;
42 		Json m_packageSettings;
43 		Package m_rootPackage;
44 		Package[] m_dependencies;
45 		Package[][Package] m_dependees;
46 		SelectedVersions m_selections;
47 		bool m_hasAllDependencies;
48 		string[string] m_overriddenConfigs;
49 	}
50 
51 	/** Loads a project.
52 
53 		Params:
54 			package_manager = Package manager instance to use for loading
55 				dependencies
56 			project_path = Path of the root package to load
57 			pack = An existing `Package` instance to use as the root package
58 	*/
59 	this(PackageManager package_manager, NativePath project_path)
60 	{
61 		Package pack;
62 		auto packageFile = Package.findPackageFile(project_path);
63 		if (packageFile.empty) {
64 			logWarn("There was no package description found for the application in '%s'.", project_path.toNativeString());
65 			pack = new Package(PackageRecipe.init, project_path);
66 		} else {
67 			pack = package_manager.getOrLoadPackage(project_path, packageFile);
68 		}
69 
70 		this(package_manager, pack);
71 	}
72 
73 	/// ditto
74 	this(PackageManager package_manager, Package pack)
75 	{
76 		m_packageManager = package_manager;
77 		m_rootPackage = pack;
78 		m_packageSettings = Json.emptyObject;
79 
80 		try m_packageSettings = jsonFromFile(m_rootPackage.path ~ ".dub/dub.json", true);
81 		catch(Exception t) logDiagnostic("Failed to read .dub/dub.json: %s", t.msg);
82 
83 		auto selverfile = m_rootPackage.path ~ SelectedVersions.defaultFile;
84 		if (existsFile(selverfile)) {
85 			try m_selections = new SelectedVersions(selverfile);
86 			catch(Exception e) {
87 				logWarn("Failed to load %s: %s", SelectedVersions.defaultFile, e.msg);
88 				logDiagnostic("Full error: %s", e.toString().sanitize);
89 				m_selections = new SelectedVersions;
90 			}
91 		} else m_selections = new SelectedVersions;
92 
93 		reinit();
94 	}
95 
96 	/** List of all resolved dependencies.
97 
98 		This includes all direct and indirect dependencies of all configurations
99 		combined. Optional dependencies that were not chosen are not included.
100 	*/
101 	@property const(Package[]) dependencies() const { return m_dependencies; }
102 
103 	/// The root package of the project.
104 	@property inout(Package) rootPackage() inout { return m_rootPackage; }
105 
106 	/// The versions to use for all dependencies. Call reinit() after changing these.
107 	@property inout(SelectedVersions) selections() inout { return m_selections; }
108 
109 	/// Package manager instance used by the project.
110 	@property inout(PackageManager) packageManager() inout { return m_packageManager; }
111 
112 	/** Determines if all dependencies necessary to build have been collected.
113 
114 		If this function returns `false`, it may be necessary to add more entries
115 		to `selections`, or to use `Dub.upgrade` to automatically select all
116 		missing dependencies.
117 	*/
118 	bool hasAllDependencies() const { return m_hasAllDependencies; }
119 
120 	/** Allows iteration of the dependency tree in topological order
121 	*/
122 	int delegate(int delegate(ref Package)) getTopologicalPackageList(bool children_first = false, Package root_package = null, string[string] configs = null)
123 	{
124 		// ugly way to avoid code duplication since inout isn't compatible with foreach type inference
125 		return cast(int delegate(int delegate(ref Package)))(cast(const)this).getTopologicalPackageList(children_first, root_package, configs);
126 	}
127 	/// ditto
128 	int delegate(int delegate(ref const Package)) getTopologicalPackageList(bool children_first = false, in Package root_package = null, string[string] configs = null)
129 	const {
130 		const(Package) rootpack = root_package ? root_package : m_rootPackage;
131 
132 		int iterator(int delegate(ref const Package) del)
133 		{
134 			int ret = 0;
135 			bool[const(Package)] visited;
136 			void perform_rec(in Package p){
137 				if( p in visited ) return;
138 				visited[p] = true;
139 
140 				if( !children_first ){
141 					ret = del(p);
142 					if( ret ) return;
143 				}
144 
145 				auto cfg = configs.get(p.name, null);
146 
147 				PackageDependency[] deps;
148 				if (!cfg.length) deps = p.getAllDependencies();
149 				else {
150 					auto depmap = p.getDependencies(cfg);
151 					deps = depmap.byKey.map!(k => PackageDependency(k, depmap[k])).array;
152 				}
153 				deps.sort!((a, b) => a.name < b.name);
154 
155 				foreach (d; deps) {
156 					auto dependency = getDependency(d.name, true);
157 					assert(dependency || d.spec.optional,
158 						format("Non-optional dependency %s of %s not found in dependency tree!?.", d.name, p.name));
159 					if(dependency) perform_rec(dependency);
160 					if( ret ) return;
161 				}
162 
163 				if( children_first ){
164 					ret = del(p);
165 					if( ret ) return;
166 				}
167 			}
168 			perform_rec(rootpack);
169 			return ret;
170 		}
171 
172 		return &iterator;
173 	}
174 
175 	/** Retrieves a particular dependency by name.
176 
177 		Params:
178 			name = (Qualified) package name of the dependency
179 			is_optional = If set to true, will return `null` for unsatisfiable
180 				dependencies instead of throwing an exception.
181 	*/
182 	inout(Package) getDependency(string name, bool is_optional)
183 	inout {
184 		foreach(dp; m_dependencies)
185 			if( dp.name == name )
186 				return dp;
187 		if (!is_optional) throw new Exception("Unknown dependency: "~name);
188 		else return null;
189 	}
190 
191 	/** Returns the name of the default build configuration for the specified
192 		target platform.
193 
194 		Params:
195 			platform = The target build platform
196 			allow_non_library_configs = If set to true, will use the first
197 				possible configuration instead of the first "executable"
198 				configuration.
199 	*/
200 	string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true)
201 	const {
202 		auto cfgs = getPackageConfigs(platform, null, allow_non_library_configs);
203 		return cfgs[m_rootPackage.name];
204 	}
205 
206 	/** Overrides the configuration chosen for a particular package in the
207 		dependency graph.
208 
209 		Setting a certain configuration here is equivalent to removing all
210 		but one configuration from the package.
211 
212 		Params:
213 			package_ = The package for which to force selecting a certain
214 				dependency
215 			config = Name of the configuration to force
216 	*/
217 	void overrideConfiguration(string package_, string config)
218 	{
219 		auto p = getDependency(package_, true);
220 		enforce(p !is null,
221 			format("Package '%s', marked for configuration override, is not present in dependency graph.", package_));
222 		enforce(p.configurations.canFind(config),
223 			format("Package '%s' does not have a configuration named '%s'.", package_, config));
224 		m_overriddenConfigs[package_] = config;
225 	}
226 
227 	/** Performs basic validation of various aspects of the package.
228 
229 		This will emit warnings to `stderr` if any discouraged names or
230 		dependency patterns are found.
231 	*/
232 	void validate()
233 	{
234 		// some basic package lint
235 		m_rootPackage.warnOnSpecialCompilerFlags();
236 		string nameSuggestion() {
237 			string ret;
238 			ret ~= `Please modify the "name" field in %s accordingly.`.format(m_rootPackage.recipePath.toNativeString());
239 			if (!m_rootPackage.recipe.buildSettings.targetName.length) {
240 				if (m_rootPackage.recipePath.head.toString().endsWith(".sdl")) {
241 					ret ~= ` You can then add 'targetName "%s"' to keep the current executable name.`.format(m_rootPackage.name);
242 				} else {
243 					ret ~= ` You can then add '"targetName": "%s"' to keep the current executable name.`.format(m_rootPackage.name);
244 				}
245 			}
246 			return ret;
247 		}
248 		if (m_rootPackage.name != m_rootPackage.name.toLower()) {
249 			logWarn(`WARNING: DUB package names should always be lower case. %s`, nameSuggestion());
250 		} else if (!m_rootPackage.recipe.name.all!(ch => ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '-' || ch == '_')) {
251 			logWarn(`WARNING: DUB package names may only contain alphanumeric characters, `
252 				~ `as well as '-' and '_'. %s`, nameSuggestion());
253 		}
254 		enforce(!m_rootPackage.name.canFind(' '), "Aborting due to the package name containing spaces.");
255 
256 		foreach (d; m_rootPackage.getAllDependencies())
257 			if (d.spec.isExactVersion && d.spec.version_.isBranch) {
258 				logWarn("WARNING: A deprecated branch based version specification is used "
259 					~ "for the dependency %s. Please use numbered versions instead. Also "
260 					~ "note that you can still use the %s file to override a certain "
261 					~ "dependency to use a branch instead.",
262 					d.name, SelectedVersions.defaultFile);
263 			}
264 
265 		// search for orphan sub configurations
266 		void warnSubConfig(string pack, string config) {
267 			logWarn("The sub configuration directive \"%s\" -> \"%s\" "
268 				~ "references a package that is not specified as a dependency "
269 				~ "and will have no effect.", pack, config);
270 		}
271 		void checkSubConfig(string pack, string config) {
272 			auto p = getDependency(pack, true);
273 			if (p && !p.configurations.canFind(config)) {
274 				logWarn("The sub configuration directive \"%s\" -> \"%s\" "
275 					~ "references a configuration that does not exist.",
276 					pack, config);
277 			}
278 		}
279 		auto globalbs = m_rootPackage.getBuildSettings();
280 		foreach (p, c; globalbs.subConfigurations) {
281 			if (p !in globalbs.dependencies) warnSubConfig(p, c);
282 			else checkSubConfig(p, c);
283 		}
284 		foreach (c; m_rootPackage.configurations) {
285 			auto bs = m_rootPackage.getBuildSettings(c);
286 			foreach (p, c; bs.subConfigurations) {
287 				if (p !in bs.dependencies && p !in globalbs.dependencies)
288 					warnSubConfig(p, c);
289 				else checkSubConfig(p, c);
290 			}
291 		}
292 
293 		// check for version specification mismatches
294 		bool[Package] visited;
295 		void validateDependenciesRec(Package pack) {
296 			// perform basic package linting
297 			pack.simpleLint();
298 
299 			foreach (d; pack.getAllDependencies()) {
300 				auto basename = getBasePackageName(d.name);
301 				if (m_selections.hasSelectedVersion(basename)) {
302 					auto selver = m_selections.getSelectedVersion(basename);
303 					if (d.spec.merge(selver) == Dependency.invalid) {
304 						logWarn("Selected package %s %s does not match the dependency specification %s in package %s. Need to \"dub upgrade\"?",
305 							basename, selver, d.spec, pack.name);
306 					}
307 				}
308 
309 				auto deppack = getDependency(d.name, true);
310 				if (deppack in visited) continue;
311 				visited[deppack] = true;
312 				if (deppack) validateDependenciesRec(deppack);
313 			}
314 		}
315 		validateDependenciesRec(m_rootPackage);
316 	}
317 
318 	/// Reloads dependencies.
319 	void reinit()
320 	{
321 		m_dependencies = null;
322 		m_hasAllDependencies = true;
323 		m_packageManager.refresh(false);
324 
325 		void collectDependenciesRec(Package pack, int depth = 0)
326 		{
327 			auto indent = replicate("  ", depth);
328 			logDebug("%sCollecting dependencies for %s", indent, pack.name);
329 			indent ~= "  ";
330 
331 			foreach (dep; pack.getAllDependencies()) {
332 				Dependency vspec = dep.spec;
333 				Package p;
334 
335 				auto basename = getBasePackageName(dep.name);
336 				auto subname = getSubPackageName(dep.name);
337 
338 				// non-optional and optional-default dependencies (if no selections file exists)
339 				// need to be satisfied
340 				bool is_desired = !vspec.optional || m_selections.hasSelectedVersion(basename) || (vspec.default_ && m_selections.bare);
341 
342 				if (dep.name == m_rootPackage.basePackage.name) {
343 					vspec = Dependency(m_rootPackage.version_);
344 					p = m_rootPackage.basePackage;
345 				} else if (basename == m_rootPackage.basePackage.name) {
346 					vspec = Dependency(m_rootPackage.version_);
347 					try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, subname, false);
348 					catch (Exception e) {
349 						logDiagnostic("%sError getting sub package %s: %s", indent, dep.name, e.msg);
350 						if (is_desired) m_hasAllDependencies = false;
351 						continue;
352 					}
353 				} else if (m_selections.hasSelectedVersion(basename)) {
354 					vspec = m_selections.getSelectedVersion(basename);
355 					if (vspec.path.empty) p = m_packageManager.getBestPackage(dep.name, vspec);
356 					else {
357 						auto path = vspec.path;
358 						if (!path.absolute) path = m_rootPackage.path ~ path;
359 						p = m_packageManager.getOrLoadPackage(path, NativePath.init, true);
360 						if (subname.length) p = m_packageManager.getSubPackage(p, subname, true);
361 					}
362 				} else if (m_dependencies.canFind!(d => getBasePackageName(d.name) == basename)) {
363 					auto idx = m_dependencies.countUntil!(d => getBasePackageName(d.name) == basename);
364 					auto bp = m_dependencies[idx].basePackage;
365 					vspec = Dependency(bp.path);
366 					if (subname.length) p = m_packageManager.getSubPackage(bp, subname, false);
367 					else p = bp;
368 				} else {
369 					logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.",
370 						indent, basename, dep.name, pack.name);
371 				}
372 
373 				if (!p && !vspec.path.empty) {
374 					NativePath path = vspec.path;
375 					if (!path.absolute) path = pack.path ~ path;
376 					logDiagnostic("%sAdding local %s in %s", indent, dep.name, path);
377 					p = m_packageManager.getOrLoadPackage(path, NativePath.init, true);
378 					if (p.parentPackage !is null) {
379 						logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name);
380 						p = p.parentPackage;
381 					}
382 					if (subname.length) p = m_packageManager.getSubPackage(p, subname, false);
383 					enforce(p.name == dep.name,
384 						format("Path based dependency %s is referenced with a wrong name: %s vs. %s",
385 							path.toNativeString(), dep.name, p.name));
386 				}
387 
388 				if (!p) {
389 					logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name);
390 					if (is_desired) m_hasAllDependencies = false;
391 					continue;
392 				}
393 
394 				if (!m_dependencies.canFind(p)) {
395 					logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString());
396 					m_dependencies ~= p;
397 					if (basename == m_rootPackage.basePackage.name)
398 						p.warnOnSpecialCompilerFlags();
399 					collectDependenciesRec(p, depth+1);
400 				}
401 
402 				m_dependees[p] ~= pack;
403 				//enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString());
404 			}
405 		}
406 		collectDependenciesRec(m_rootPackage);
407 	}
408 
409 	/// Returns the name of the root package.
410 	@property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; }
411 
412 	/// Returns the names of all configurations of the root package.
413 	@property string[] configurations() const { return m_rootPackage.configurations; }
414 
415 	/// Returns a map with the configuration for all packages in the dependency tree.
416 	string[string] getPackageConfigs(in BuildPlatform platform, string config, bool allow_non_library = true)
417 	const {
418 		struct Vertex { string pack, config; }
419 		struct Edge { size_t from, to; }
420 
421 		Vertex[] configs;
422 		Edge[] edges;
423 		string[][string] parents;
424 		parents[m_rootPackage.name] = null;
425 		foreach (p; getTopologicalPackageList())
426 			foreach (d; p.getAllDependencies())
427 				parents[d.name] ~= p.name;
428 
429 		size_t createConfig(string pack, string config) {
430 			foreach (i, v; configs)
431 				if (v.pack == pack && v.config == config)
432 					return i;
433 			assert(pack !in m_overriddenConfigs || config == m_overriddenConfigs[pack]);
434 			logDebug("Add config %s %s", pack, config);
435 			configs ~= Vertex(pack, config);
436 			return configs.length-1;
437 		}
438 
439 		bool haveConfig(string pack, string config) {
440 			return configs.any!(c => c.pack == pack && c.config == config);
441 		}
442 
443 		size_t createEdge(size_t from, size_t to) {
444 			auto idx = edges.countUntil(Edge(from, to));
445 			if (idx >= 0) return idx;
446 			logDebug("Including %s %s -> %s %s", configs[from].pack, configs[from].config, configs[to].pack, configs[to].config);
447 			edges ~= Edge(from, to);
448 			return edges.length-1;
449 		}
450 
451 		void removeConfig(size_t i) {
452 			logDebug("Eliminating config %s for %s", configs[i].config, configs[i].pack);
453 			auto had_dep_to_pack = new bool[configs.length];
454 			auto still_has_dep_to_pack = new bool[configs.length];
455 
456 			edges = edges.filter!((e) {
457 					if (e.to == i) {
458 						had_dep_to_pack[e.from] = true;
459 						return false;
460 					} else if (configs[e.to].pack == configs[i].pack) {
461 						still_has_dep_to_pack[e.from] = true;
462 					}
463 					if (e.from == i) return false;
464 					return true;
465 				}).array;
466 
467 			configs[i] = Vertex.init; // mark config as removed
468 
469 			// also remove any configs that cannot be satisfied anymore
470 			foreach (j; 0 .. configs.length)
471 				if (j != i && had_dep_to_pack[j] && !still_has_dep_to_pack[j])
472 					removeConfig(j);
473 		}
474 
475 		bool isReachable(string pack, string conf) {
476 			if (pack == configs[0].pack && configs[0].config == conf) return true;
477 			foreach (e; edges)
478 				if (configs[e.to].pack == pack && configs[e.to].config == conf)
479 					return true;
480 			return false;
481 			//return (pack == configs[0].pack && conf == configs[0].config) || edges.canFind!(e => configs[e.to].pack == pack && configs[e.to].config == config);
482 		}
483 
484 		bool isReachableByAllParentPacks(size_t cidx) {
485 			bool[string] r;
486 			foreach (p; parents[configs[cidx].pack]) r[p] = false;
487 			foreach (e; edges) {
488 				if (e.to != cidx) continue;
489 				if (auto pp = configs[e.from].pack in r) *pp = true;
490 			}
491 			foreach (bool v; r) if (!v) return false;
492 			return true;
493 		}
494 
495 		string[] allconfigs_path;
496 
497 		void determineDependencyConfigs(in Package p, string c)
498 		{
499 			string[][string] depconfigs;
500 			foreach (d; p.getAllDependencies()) {
501 				auto dp = getDependency(d.name, true);
502 				if (!dp) continue;
503 
504 				string[] cfgs;
505 				if (auto pc = dp.name in m_overriddenConfigs) cfgs = [*pc];
506 				else {
507 					auto subconf = p.getSubConfiguration(c, dp, platform);
508 					if (!subconf.empty) cfgs = [subconf];
509 					else cfgs = dp.getPlatformConfigurations(platform);
510 				}
511 				cfgs = cfgs.filter!(c => haveConfig(d.name, c)).array;
512 
513 				// if no valid configuration was found for a dependency, don't include the
514 				// current configuration
515 				if (!cfgs.length) {
516 					logDebug("Skip %s %s (missing configuration for %s)", p.name, c, dp.name);
517 					return;
518 				}
519 				depconfigs[d.name] = cfgs;
520 			}
521 
522 			// add this configuration to the graph
523 			size_t cidx = createConfig(p.name, c);
524 			foreach (d; p.getAllDependencies())
525 				foreach (sc; depconfigs.get(d.name, null))
526 					createEdge(cidx, createConfig(d.name, sc));
527 		}
528 
529 		// create a graph of all possible package configurations (package, config) -> (subpackage, subconfig)
530 		void determineAllConfigs(in Package p)
531 		{
532 			auto idx = allconfigs_path.countUntil(p.name);
533 			enforce(idx < 0, format("Detected dependency cycle: %s", (allconfigs_path[idx .. $] ~ p.name).join("->")));
534 			allconfigs_path ~= p.name;
535 			scope (exit) allconfigs_path.length--;
536 
537 			// first, add all dependency configurations
538 			foreach (d; p.getAllDependencies) {
539 				auto dp = getDependency(d.name, true);
540 				if (!dp) continue;
541 				determineAllConfigs(dp);
542 			}
543 
544 			// for each configuration, determine the configurations usable for the dependencies
545 			if (auto pc = p.name in m_overriddenConfigs)
546 				determineDependencyConfigs(p, *pc);
547 			else
548 				foreach (c; p.getPlatformConfigurations(platform, p is m_rootPackage && allow_non_library))
549 					determineDependencyConfigs(p, c);
550 		}
551 		if (config.length) createConfig(m_rootPackage.name, config);
552 		determineAllConfigs(m_rootPackage);
553 
554 		// successively remove configurations until only one configuration per package is left
555 		bool changed;
556 		do {
557 			// remove all configs that are not reachable by all parent packages
558 			changed = false;
559 			foreach (i, ref c; configs) {
560 				if (c == Vertex.init) continue; // ignore deleted configurations
561 				if (!isReachableByAllParentPacks(i)) {
562 					logDebug("%s %s NOT REACHABLE by all of (%s):", c.pack, c.config, parents[c.pack]);
563 					removeConfig(i);
564 					changed = true;
565 				}
566 			}
567 
568 			// when all edges are cleaned up, pick one package and remove all but one config
569 			if (!changed) {
570 				foreach (p; getTopologicalPackageList()) {
571 					size_t cnt = 0;
572 					foreach (i, ref c; configs)
573 						if (c.pack == p.name && ++cnt > 1) {
574 							logDebug("NON-PRIMARY: %s %s", c.pack, c.config);
575 							removeConfig(i);
576 						}
577 					if (cnt > 1) {
578 						changed = true;
579 						break;
580 					}
581 				}
582 			}
583 		} while (changed);
584 
585 		// print out the resulting tree
586 		foreach (e; edges) logDebug("    %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config);
587 
588 		// return the resulting configuration set as an AA
589 		string[string] ret;
590 		foreach (c; configs) {
591 			if (c == Vertex.init) continue; // ignore deleted configurations
592 			assert(ret.get(c.pack, c.config) == c.config, format("Conflicting configurations for %s found: %s vs. %s", c.pack, c.config, ret[c.pack]));
593 			logDebug("Using configuration '%s' for %s", c.config, c.pack);
594 			ret[c.pack] = c.config;
595 		}
596 
597 		// check for conflicts (packages missing in the final configuration graph)
598 		void checkPacksRec(in Package pack) {
599 			auto pc = pack.name in ret;
600 			enforce(pc !is null, "Could not resolve configuration for package "~pack.name);
601 			foreach (p, dep; pack.getDependencies(*pc)) {
602 				auto deppack = getDependency(p, dep.optional);
603 				if (deppack) checkPacksRec(deppack);
604 			}
605 		}
606 		checkPacksRec(m_rootPackage);
607 
608 		return ret;
609 	}
610 
611 	/**
612 	 * Fills `dst` with values from this project.
613 	 *
614 	 * `dst` gets initialized according to the given platform and config.
615 	 *
616 	 * Params:
617 	 *   dst = The BuildSettings struct to fill with data.
618 	 *   platform = The platform to retrieve the values for.
619 	 *   config = Values of the given configuration will be retrieved.
620 	 *   root_package = If non null, use it instead of the project's real root package.
621 	 *   shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary.
622 	 */
623 	void addBuildSettings(ref BuildSettings dst, in BuildPlatform platform, string config, in Package root_package = null, bool shallow = false)
624 	const {
625 		import dub.internal.utils : stripDlangSpecialChars;
626 
627 		auto configs = getPackageConfigs(platform, config);
628 
629 		foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) {
630 			auto pkg_path = pkg.path.toNativeString();
631 			dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]);
632 
633 			assert(pkg.name in configs, "Missing configuration for "~pkg.name);
634 			logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]);
635 
636 			auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]);
637 			if (psettings.targetType != TargetType.none) {
638 				if (shallow && pkg !is m_rootPackage)
639 					psettings.sourceFiles = null;
640 				processVars(dst, this, pkg, psettings);
641 				if (psettings.importPaths.empty)
642 					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]);
643 				if (psettings.mainSourceFile.empty && pkg is m_rootPackage && psettings.targetType == TargetType.executable)
644 					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);
645 			}
646 			if (pkg is m_rootPackage) {
647 				if (!shallow) {
648 					enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build.");
649 					enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build.");
650 				}
651 				dst.targetType = psettings.targetType;
652 				dst.targetPath = psettings.targetPath;
653 				dst.targetName = psettings.targetName;
654 				if (!psettings.workingDirectory.empty)
655 					dst.workingDirectory = processVars(psettings.workingDirectory, this, pkg, true);
656 				if (psettings.mainSourceFile.length)
657 					dst.mainSourceFile = processVars(psettings.mainSourceFile, this, pkg, true);
658 			}
659 		}
660 
661 		// always add all version identifiers of all packages
662 		foreach (pkg; this.getTopologicalPackageList(false, null, configs)) {
663 			auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]);
664 			dst.addVersions(psettings.versions);
665 		}
666 	}
667 
668 	/** Fills `dst` with build settings specific to the given build type.
669 
670 		Params:
671 			dst = The `BuildSettings` instance to add the build settings to
672 			platform = Target build platform
673 			build_type = Name of the build type
674 			for_root_package = Selects if the build settings are for the root
675 				package or for one of the dependencies. Unittest flags will
676 				only be added to the root package.
677 	*/
678 	void addBuildTypeSettings(ref BuildSettings dst, in BuildPlatform platform, string build_type, bool for_root_package = true)
679 	{
680 		bool usedefflags = !(dst.requirements & BuildRequirement.noDefaultFlags);
681 		if (usedefflags) {
682 			BuildSettings btsettings;
683 			m_rootPackage.addBuildTypeSettings(btsettings, platform, build_type);
684 
685 			if (!for_root_package) {
686 				// don't propagate unittest switch to dependencies, as dependent
687 				// unit tests aren't run anyway and the additional code may
688 				// cause linking to fail on Windows (issue #640)
689 				btsettings.removeOptions(BuildOption.unittests);
690 			}
691 
692 			processVars(dst, this, m_rootPackage, btsettings);
693 		}
694 	}
695 
696 	/// Outputs a build description of the project, including its dependencies.
697 	ProjectDescription describe(GeneratorSettings settings)
698 	{
699 		import dub.generators.targetdescription;
700 
701 		// store basic build parameters
702 		ProjectDescription ret;
703 		ret.rootPackage = m_rootPackage.name;
704 		ret.configuration = settings.config;
705 		ret.buildType = settings.buildType;
706 		ret.compiler = settings.platform.compiler;
707 		ret.architecture = settings.platform.architecture;
708 		ret.platform = settings.platform.platform;
709 
710 		// collect high level information about projects (useful for IDE display)
711 		auto configs = getPackageConfigs(settings.platform, settings.config);
712 		ret.packages ~= m_rootPackage.describe(settings.platform, settings.config);
713 		foreach (dep; m_dependencies)
714 			ret.packages ~= dep.describe(settings.platform, configs[dep.name]);
715 
716 		foreach (p; getTopologicalPackageList(false, null, configs))
717 			ret.packages[ret.packages.countUntil!(pp => pp.name == p.name)].active = true;
718 
719 		if (settings.buildType.length) {
720 			// collect build target information (useful for build tools)
721 			auto gen = new TargetDescriptionGenerator(this);
722 			try {
723 				gen.generate(settings);
724 				ret.targets = gen.targetDescriptions;
725 				ret.targetLookup = gen.targetDescriptionLookup;
726 			} catch (Exception e) {
727 				logDiagnostic("Skipping targets description: %s", e.msg);
728 				logDebug("Full error: %s", e.toString().sanitize);
729 			}
730 		}
731 
732 		return ret;
733 	}
734 
735 	private string[] listBuildSetting(string attributeName)(BuildPlatform platform,
736 		string config, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
737 	{
738 		return listBuildSetting!attributeName(platform, getPackageConfigs(platform, config),
739 			projectDescription, compiler, disableEscaping);
740 	}
741 
742 	private string[] listBuildSetting(string attributeName)(BuildPlatform platform,
743 		string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
744 	{
745 		if (compiler)
746 			return formatBuildSettingCompiler!attributeName(platform, configs, projectDescription, compiler, disableEscaping);
747 		else
748 			return formatBuildSettingPlain!attributeName(platform, configs, projectDescription);
749 	}
750 
751 	// Output a build setting formatted for a compiler
752 	private string[] formatBuildSettingCompiler(string attributeName)(BuildPlatform platform,
753 		string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping)
754 	{
755 		import std.process : escapeShellFileName;
756 		import std.path : dirSeparator;
757 
758 		assert(compiler);
759 
760 		auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage);
761 		auto buildSettings = targetDescription.buildSettings;
762 
763 		string[] values;
764 		switch (attributeName)
765 		{
766 		case "dflags":
767 		case "linkerFiles":
768 		case "mainSourceFile":
769 		case "importFiles":
770 			values = formatBuildSettingPlain!attributeName(platform, configs, projectDescription);
771 			break;
772 
773 		case "lflags":
774 		case "sourceFiles":
775 		case "versions":
776 		case "debugVersions":
777 		case "importPaths":
778 		case "stringImportPaths":
779 		case "options":
780 			auto bs = buildSettings.dup;
781 			bs.dflags = null;
782 
783 			// Ensure trailing slash on directory paths
784 			auto ensureTrailingSlash = (string path) => path.endsWith(dirSeparator) ? path : path ~ dirSeparator;
785 			static if (attributeName == "importPaths")
786 				bs.importPaths = bs.importPaths.map!(ensureTrailingSlash).array();
787 			else static if (attributeName == "stringImportPaths")
788 				bs.stringImportPaths = bs.stringImportPaths.map!(ensureTrailingSlash).array();
789 
790 			compiler.prepareBuildSettings(bs, BuildSetting.all & ~to!BuildSetting(attributeName));
791 			values = bs.dflags;
792 			break;
793 
794 		case "libs":
795 			auto bs = buildSettings.dup;
796 			bs.dflags = null;
797 			bs.lflags = null;
798 			bs.sourceFiles = null;
799 			bs.targetType = TargetType.none; // Force Compiler to NOT omit dependency libs when package is a library.
800 
801 			compiler.prepareBuildSettings(bs, BuildSetting.all & ~to!BuildSetting(attributeName));
802 
803 			if (bs.lflags)
804 				values = compiler.lflagsToDFlags( bs.lflags );
805 			else if (bs.sourceFiles)
806 				values = compiler.lflagsToDFlags( bs.sourceFiles );
807 			else
808 				values = bs.dflags;
809 
810 			break;
811 
812 		default: assert(0);
813 		}
814 
815 		// Escape filenames and paths
816 		if(!disableEscaping)
817 		{
818 			switch (attributeName)
819 			{
820 			case "mainSourceFile":
821 			case "linkerFiles":
822 			case "copyFiles":
823 			case "importFiles":
824 			case "stringImportFiles":
825 			case "sourceFiles":
826 			case "importPaths":
827 			case "stringImportPaths":
828 				return values.map!(escapeShellFileName).array();
829 
830 			default:
831 				return values;
832 			}
833 		}
834 
835 		return values;
836 	}
837 
838 	// Output a build setting without formatting for any particular compiler
839 	private string[] formatBuildSettingPlain(string attributeName)(BuildPlatform platform, string[string] configs, ProjectDescription projectDescription)
840 	{
841 		import std.path : buildNormalizedPath, dirSeparator;
842 		import std.range : only;
843 
844 		string[] list;
845 
846 		enforce(attributeName == "targetType" || projectDescription.lookupRootPackage().targetType != TargetType.none,
847 			"Target type is 'none'. Cannot list build settings.");
848 
849 		static if (attributeName == "targetType")
850 			if (projectDescription.rootPackage !in projectDescription.targetLookup)
851 				return ["none"];
852 
853 		auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage);
854 		auto buildSettings = targetDescription.buildSettings;
855 
856 		// Return any BuildSetting member attributeName as a range of strings. Don't attempt to fixup values.
857 		// allowEmptyString: When the value is a string (as opposed to string[]),
858 		//                   is empty string an actual permitted value instead of
859 		//                   a missing value?
860 		auto getRawBuildSetting(Package pack, bool allowEmptyString) {
861 			auto value = __traits(getMember, buildSettings, attributeName);
862 
863 			static if( is(typeof(value) == string[]) )
864 				return value;
865 			else static if( is(typeof(value) == string) )
866 			{
867 				auto ret = only(value);
868 
869 				// only() has a different return type from only(value), so we
870 				// have to empty the range rather than just returning only().
871 				if(value.empty && !allowEmptyString) {
872 					ret.popFront();
873 					assert(ret.empty);
874 				}
875 
876 				return ret;
877 			}
878 			else static if( is(typeof(value) == enum) )
879 				return only(value);
880 			else static if( is(typeof(value) == BuildRequirements) )
881 				return only(cast(BuildRequirement) cast(int) value.values);
882 			else static if( is(typeof(value) == BuildOptions) )
883 				return only(cast(BuildOption) cast(int) value.values);
884 			else
885 				static assert(false, "Type of BuildSettings."~attributeName~" is unsupported.");
886 		}
887 
888 		// Adjust BuildSetting member attributeName as needed.
889 		// Returns a range of strings.
890 		auto getFixedBuildSetting(Package pack) {
891 			// Is relative path(s) to a directory?
892 			enum isRelativeDirectory =
893 				attributeName == "importPaths" || attributeName == "stringImportPaths" ||
894 				attributeName == "targetPath" || attributeName == "workingDirectory";
895 
896 			// Is relative path(s) to a file?
897 			enum isRelativeFile =
898 				attributeName == "sourceFiles" || attributeName == "linkerFiles" ||
899 				attributeName == "importFiles" || attributeName == "stringImportFiles" ||
900 				attributeName == "copyFiles" || attributeName == "mainSourceFile";
901 
902 			// For these, empty string means "main project directory", not "missing value"
903 			enum allowEmptyString =
904 				attributeName == "targetPath" || attributeName == "workingDirectory";
905 
906 			enum isEnumBitfield =
907 				attributeName == "requirements" || attributeName == "options";
908 
909 			enum isEnum = attributeName == "targetType";
910 
911 			auto values = getRawBuildSetting(pack, allowEmptyString);
912 			string fixRelativePath(string importPath) { return buildNormalizedPath(pack.path.toString(), importPath); }
913 			static string ensureTrailingSlash(string path) { return path.endsWith(dirSeparator) ? path : path ~ dirSeparator; }
914 
915 			static if(isRelativeDirectory) {
916 				// Return full paths for the paths, making sure a
917 				// directory separator is on the end of each path.
918 				return values.map!(fixRelativePath).map!(ensureTrailingSlash);
919 			}
920 			else static if(isRelativeFile) {
921 				// Return full paths.
922 				return values.map!(fixRelativePath);
923 			}
924 			else static if(isEnumBitfield)
925 				return bitFieldNames(values.front);
926 			else static if (isEnum)
927 				return [values.front.to!string];
928 			else
929 				return values;
930 		}
931 
932 		foreach(value; getFixedBuildSetting(m_rootPackage)) {
933 			list ~= value;
934 		}
935 
936 		return list;
937 	}
938 
939 	// The "compiler" arg is for choosing which compiler the output should be formatted for,
940 	// or null to imply "list" format.
941 	private string[] listBuildSetting(BuildPlatform platform, string[string] configs,
942 		ProjectDescription projectDescription, string requestedData, Compiler compiler, bool disableEscaping)
943 	{
944 		// Certain data cannot be formatter for a compiler
945 		if (compiler)
946 		{
947 			switch (requestedData)
948 			{
949 			case "target-type":
950 			case "target-path":
951 			case "target-name":
952 			case "working-directory":
953 			case "string-import-files":
954 			case "copy-files":
955 			case "pre-generate-commands":
956 			case "post-generate-commands":
957 			case "pre-build-commands":
958 			case "post-build-commands":
959 				enforce(false, "--data="~requestedData~" can only be used with --data-list or --data-0.");
960 				break;
961 
962 			case "requirements":
963 				enforce(false, "--data=requirements can only be used with --data-list or --data-0. Use --data=options instead.");
964 				break;
965 
966 			default: break;
967 			}
968 		}
969 
970 		import std.typetuple : TypeTuple;
971 		auto args = TypeTuple!(platform, configs, projectDescription, compiler, disableEscaping);
972 		switch (requestedData)
973 		{
974 		case "target-type":            return listBuildSetting!"targetType"(args);
975 		case "target-path":            return listBuildSetting!"targetPath"(args);
976 		case "target-name":            return listBuildSetting!"targetName"(args);
977 		case "working-directory":      return listBuildSetting!"workingDirectory"(args);
978 		case "main-source-file":       return listBuildSetting!"mainSourceFile"(args);
979 		case "dflags":                 return listBuildSetting!"dflags"(args);
980 		case "lflags":                 return listBuildSetting!"lflags"(args);
981 		case "libs":                   return listBuildSetting!"libs"(args);
982 		case "linker-files":           return listBuildSetting!"linkerFiles"(args);
983 		case "source-files":           return listBuildSetting!"sourceFiles"(args);
984 		case "copy-files":             return listBuildSetting!"copyFiles"(args);
985 		case "versions":               return listBuildSetting!"versions"(args);
986 		case "debug-versions":         return listBuildSetting!"debugVersions"(args);
987 		case "import-paths":           return listBuildSetting!"importPaths"(args);
988 		case "string-import-paths":    return listBuildSetting!"stringImportPaths"(args);
989 		case "import-files":           return listBuildSetting!"importFiles"(args);
990 		case "string-import-files":    return listBuildSetting!"stringImportFiles"(args);
991 		case "pre-generate-commands":  return listBuildSetting!"preGenerateCommands"(args);
992 		case "post-generate-commands": return listBuildSetting!"postGenerateCommands"(args);
993 		case "pre-build-commands":     return listBuildSetting!"preBuildCommands"(args);
994 		case "post-build-commands":    return listBuildSetting!"postBuildCommands"(args);
995 		case "requirements":           return listBuildSetting!"requirements"(args);
996 		case "options":                return listBuildSetting!"options"(args);
997 
998 		default:
999 			enforce(false, "--data="~requestedData~
1000 				" is not a valid option. See 'dub describe --help' for accepted --data= values.");
1001 		}
1002 
1003 		assert(0);
1004 	}
1005 
1006 	/// Outputs requested data for the project, optionally including its dependencies.
1007 	string[] listBuildSettings(GeneratorSettings settings, string[] requestedData, ListBuildSettingsFormat list_type)
1008 	{
1009 		import dub.compilers.utils : isLinkerFile;
1010 
1011 		auto projectDescription = describe(settings);
1012 		auto configs = getPackageConfigs(settings.platform, settings.config);
1013 		PackageDescription packageDescription;
1014 		foreach (pack; projectDescription.packages) {
1015 			if (pack.name == projectDescription.rootPackage)
1016 				packageDescription = pack;
1017 		}
1018 
1019 		if (projectDescription.rootPackage in projectDescription.targetLookup) {
1020 			// Copy linker files from sourceFiles to linkerFiles
1021 			auto target = projectDescription.lookupTarget(projectDescription.rootPackage);
1022 			foreach (file; target.buildSettings.sourceFiles.filter!(isLinkerFile))
1023 				target.buildSettings.addLinkerFiles(file);
1024 
1025 			// Remove linker files from sourceFiles
1026 			target.buildSettings.sourceFiles =
1027 				target.buildSettings.sourceFiles
1028 				.filter!(a => !isLinkerFile(a))
1029 				.array();
1030 			projectDescription.lookupTarget(projectDescription.rootPackage) = target;
1031 		}
1032 
1033 		Compiler compiler;
1034 		bool no_escape;
1035 		final switch (list_type) with (ListBuildSettingsFormat) {
1036 			case list: break;
1037 			case listNul: no_escape = true; break;
1038 			case commandLine: compiler = settings.compiler; break;
1039 			case commandLineNul: compiler = settings.compiler; no_escape = true; break;
1040 
1041 		}
1042 
1043 		auto result = requestedData
1044 			.map!(dataName => listBuildSetting(settings.platform, configs, projectDescription, dataName, compiler, no_escape));
1045 
1046 		final switch (list_type) with (ListBuildSettingsFormat) {
1047 			case list: return result.map!(l => l.join("\n")).array();
1048 			case listNul: return result.map!(l => l.join("\0")).array;
1049 			case commandLine: return result.map!(l => l.join(" ")).array;
1050 			case commandLineNul: return result.map!(l => l.join("\0")).array;
1051 		}
1052 	}
1053 
1054 	/** Saves the currently selected dependency versions to disk.
1055 
1056 		The selections will be written to a file named
1057 		`SelectedVersions.defaultFile` ("dub.selections.json") within the
1058 		directory of the root package. Any existing file will get overwritten.
1059 	*/
1060 	void saveSelections()
1061 	{
1062 		assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections).");
1063 		if (m_selections.hasSelectedVersion(m_rootPackage.basePackage.name))
1064 			m_selections.deselectVersion(m_rootPackage.basePackage.name);
1065 
1066 		auto path = m_rootPackage.path ~ SelectedVersions.defaultFile;
1067 		if (m_selections.dirty || !existsFile(path))
1068 			m_selections.save(path);
1069 	}
1070 
1071 	/** Checks if the cached upgrade information is still considered up to date.
1072 
1073 		The cache will be considered out of date after 24 hours after the last
1074 		online check.
1075 	*/
1076 	bool isUpgradeCacheUpToDate()
1077 	{
1078 		try {
1079 			auto datestr = m_packageSettings["dub"].opt!(Json[string]).get("lastUpgrade", Json("")).get!string;
1080 			if (!datestr.length) return false;
1081 			auto date = SysTime.fromISOExtString(datestr);
1082 			if ((Clock.currTime() - date) > 1.days) return false;
1083 			return true;
1084 		} catch (Exception t) {
1085 			logDebug("Failed to get the last upgrade time: %s", t.msg);
1086 			return false;
1087 		}
1088 	}
1089 
1090 	/** Returns the currently cached upgrade information.
1091 
1092 		The returned dictionary maps from dependency package name to the latest
1093 		available version that matches the dependency specifications.
1094 	*/
1095 	Dependency[string] getUpgradeCache()
1096 	{
1097 		try {
1098 			Dependency[string] ret;
1099 			foreach (string p, d; m_packageSettings["dub"].opt!(Json[string]).get("cachedUpgrades", Json.emptyObject))
1100 				ret[p] = SelectedVersions.dependencyFromJson(d);
1101 			return ret;
1102 		} catch (Exception t) {
1103 			logDebug("Failed to get cached upgrades: %s", t.msg);
1104 			return null;
1105 		}
1106 	}
1107 
1108 	/** Sets a new set of versions for the upgrade cache.
1109 	*/
1110 	void setUpgradeCache(Dependency[string] versions)
1111 	{
1112 		logDebug("markUpToDate");
1113 		Json create(ref Json json, string object) {
1114 			if (json[object].type == Json.Type.undefined) json[object] = Json.emptyObject;
1115 			return json[object];
1116 		}
1117 		create(m_packageSettings, "dub");
1118 		m_packageSettings["dub"]["lastUpgrade"] = Clock.currTime().toISOExtString();
1119 
1120 		create(m_packageSettings["dub"], "cachedUpgrades");
1121 		foreach (p, d; versions)
1122 			m_packageSettings["dub"]["cachedUpgrades"][p] = SelectedVersions.dependencyToJson(d);
1123 
1124 		writeDubJson();
1125 	}
1126 
1127 	private void writeDubJson() {
1128 		import std.file : exists, mkdir;
1129 		// don't bother to write an empty file
1130 		if( m_packageSettings.length == 0 ) return;
1131 
1132 		try {
1133 			logDebug("writeDubJson");
1134 			auto dubpath = m_rootPackage.path~".dub";
1135 			if( !exists(dubpath.toNativeString()) ) mkdir(dubpath.toNativeString());
1136 			auto dstFile = openFile((dubpath~"dub.json").toString(), FileMode.createTrunc);
1137 			scope(exit) dstFile.close();
1138 			dstFile.writePrettyJsonString(m_packageSettings);
1139 		} catch( Exception e ){
1140 			logWarn("Could not write .dub/dub.json.");
1141 		}
1142 	}
1143 }
1144 
1145 
1146 /// Determines the output format used for `Project.listBuildSettings`.
1147 enum ListBuildSettingsFormat {
1148 	list,           /// Newline separated list entries
1149 	listNul,        /// NUL character separated list entries (unescaped)
1150 	commandLine,    /// Formatted for compiler command line (one data list per line)
1151 	commandLineNul, /// NUL character separated list entries (unescaped, data lists separated by two NUL characters)
1152 }
1153 
1154 
1155 /// Indicates where a package has been or should be placed to.
1156 enum PlacementLocation {
1157 	/// Packages retrieved with 'local' will be placed in the current folder
1158 	/// using the package name as destination.
1159 	local,
1160 	/// Packages with 'userWide' will be placed in a folder accessible by
1161 	/// all of the applications from the current user.
1162 	user,
1163 	/// Packages retrieved with 'systemWide' will be placed in a shared folder,
1164 	/// which can be accessed by all users of the system.
1165 	system
1166 }
1167 
1168 void processVars(ref BuildSettings dst, in Project project, in Package pack,
1169 	BuildSettings settings, bool include_target_settings = false)
1170 {
1171 	dst.addDFlags(processVars(project, pack, settings.dflags));
1172 	dst.addLFlags(processVars(project, pack, settings.lflags));
1173 	dst.addLibs(processVars(project, pack, settings.libs));
1174 	dst.addSourceFiles(processVars(project, pack, settings.sourceFiles, true));
1175 	dst.addImportFiles(processVars(project, pack, settings.importFiles, true));
1176 	dst.addStringImportFiles(processVars(project, pack, settings.stringImportFiles, true));
1177 	dst.addCopyFiles(processVars(project, pack, settings.copyFiles, true));
1178 	dst.addVersions(processVars(project, pack, settings.versions));
1179 	dst.addDebugVersions(processVars(project, pack, settings.debugVersions));
1180 	dst.addImportPaths(processVars(project, pack, settings.importPaths, true));
1181 	dst.addStringImportPaths(processVars(project, pack, settings.stringImportPaths, true));
1182 	dst.addPreGenerateCommands(processVars(project, pack, settings.preGenerateCommands));
1183 	dst.addPostGenerateCommands(processVars(project, pack, settings.postGenerateCommands));
1184 	dst.addPreBuildCommands(processVars(project, pack, settings.preBuildCommands));
1185 	dst.addPostBuildCommands(processVars(project, pack, settings.postBuildCommands));
1186 	dst.addRequirements(settings.requirements);
1187 	dst.addOptions(settings.options);
1188 
1189 	if (include_target_settings) {
1190 		dst.targetType = settings.targetType;
1191 		dst.targetPath = processVars(settings.targetPath, project, pack, true);
1192 		dst.targetName = settings.targetName;
1193 		if (!settings.workingDirectory.empty)
1194 			dst.workingDirectory = processVars(settings.workingDirectory, project, pack, true);
1195 		if (settings.mainSourceFile.length)
1196 			dst.mainSourceFile = processVars(settings.mainSourceFile, project, pack, true);
1197 	}
1198 }
1199 
1200 private string[] processVars(in Project project, in Package pack, string[] vars, bool are_paths = false)
1201 {
1202 	auto ret = appender!(string[])();
1203 	processVars(ret, project, pack, vars, are_paths);
1204 	return ret.data;
1205 
1206 }
1207 private void processVars(ref Appender!(string[]) dst, in Project project, in Package pack, string[] vars, bool are_paths = false)
1208 {
1209 	foreach (var; vars) dst.put(processVars(var, project, pack, are_paths));
1210 }
1211 
1212 private string processVars(string var, in Project project, in Package pack, bool is_path)
1213 {
1214 	auto idx = std..string.indexOf(var, '$');
1215 	if (idx >= 0) {
1216 		auto vres = appender!string();
1217 		while (idx >= 0) {
1218 			if (idx+1 >= var.length) break;
1219 			if (var[idx+1] == '$') {
1220 				vres.put(var[0 .. idx+1]);
1221 				var = var[idx+2 .. $];
1222 			} else {
1223 				vres.put(var[0 .. idx]);
1224 				var = var[idx+1 .. $];
1225 
1226 				size_t idx2 = 0;
1227 				while( idx2 < var.length && isIdentChar(var[idx2]) ) idx2++;
1228 				auto varname = var[0 .. idx2];
1229 				var = var[idx2 .. $];
1230 
1231 				vres.put(getVariable(varname, project, pack));
1232 			}
1233 			idx = std..string.indexOf(var, '$');
1234 		}
1235 		vres.put(var);
1236 		var = vres.data;
1237 	}
1238 	if (is_path) {
1239 		auto p = NativePath(var);
1240 		if (!p.absolute) {
1241 			return (pack.path ~ p).toNativeString();
1242 		} else return p.toNativeString();
1243 	} else return var;
1244 }
1245 
1246 private string getVariable(string name, in Project project, in Package pack)
1247 {
1248 	import std.process : environment;
1249 	if (name == "PACKAGE_DIR") return pack.path.toNativeString();
1250 	if (name == "ROOT_PACKAGE_DIR") return project.rootPackage.path.toNativeString();
1251 
1252 	if (name.endsWith("_PACKAGE_DIR")) {
1253 		auto pname = name[0 .. $-12];
1254 		foreach (prj; project.getTopologicalPackageList())
1255 			if (prj.name.toUpper().replace("-", "_") == pname)
1256 				return prj.path.toNativeString();
1257 	}
1258 
1259 	auto envvar = environment.get(name);
1260 	if (envvar !is null) return envvar;
1261 
1262 	throw new Exception("Invalid variable: "~name);
1263 }
1264 
1265 
1266 /** Holds and stores a set of version selections for package dependencies.
1267 
1268 	This is the runtime representation of the information contained in
1269 	"dub.selections.json" within a package's directory.
1270 */
1271 final class SelectedVersions {
1272 	private struct Selected {
1273 		Dependency dep;
1274 		//Dependency[string] packages;
1275 	}
1276 	private {
1277 		enum FileVersion = 1;
1278 		Selected[string] m_selections;
1279 		bool m_dirty = false; // has changes since last save
1280 		bool m_bare = true;
1281 	}
1282 
1283 	/// Default file name to use for storing selections.
1284 	enum defaultFile = "dub.selections.json";
1285 
1286 	/// Constructs a new empty version selection.
1287 	this() {}
1288 
1289 	/** Constructs a new version selection from JSON data.
1290 
1291 		The structure of the JSON document must match the contents of the
1292 		"dub.selections.json" file.
1293 	*/
1294 	this(Json data)
1295 	{
1296 		deserialize(data);
1297 		m_dirty = false;
1298 	}
1299 
1300 	/** Constructs a new version selections from an existing JSON file.
1301 	*/
1302 	this(NativePath path)
1303 	{
1304 		auto json = jsonFromFile(path);
1305 		deserialize(json);
1306 		m_dirty = false;
1307 		m_bare = false;
1308 	}
1309 
1310 	/// Returns a list of names for all packages that have a version selection.
1311 	@property string[] selectedPackages() const { return m_selections.keys; }
1312 
1313 	/// Determines if any changes have been made after loading the selections from a file.
1314 	@property bool dirty() const { return m_dirty; }
1315 
1316 	/// Determine if this set of selections is still empty (but not `clear`ed).
1317 	@property bool bare() const { return m_bare && !m_dirty; }
1318 
1319 	/// Removes all selections.
1320 	void clear()
1321 	{
1322 		m_selections = null;
1323 		m_dirty = true;
1324 	}
1325 
1326 	/// Duplicates the set of selected versions from another instance.
1327 	void set(SelectedVersions versions)
1328 	{
1329 		m_selections = versions.m_selections.dup;
1330 		m_dirty = true;
1331 	}
1332 
1333 	/// Selects a certain version for a specific package.
1334 	void selectVersion(string package_id, Version version_)
1335 	{
1336 		if (auto ps = package_id in m_selections) {
1337 			if (ps.dep == Dependency(version_))
1338 				return;
1339 		}
1340 		m_selections[package_id] = Selected(Dependency(version_)/*, issuer*/);
1341 		m_dirty = true;
1342 	}
1343 
1344 	/// Selects a certain path for a specific package.
1345 	void selectVersion(string package_id, NativePath path)
1346 	{
1347 		if (auto ps = package_id in m_selections) {
1348 			if (ps.dep == Dependency(path))
1349 				return;
1350 		}
1351 		m_selections[package_id] = Selected(Dependency(path));
1352 		m_dirty = true;
1353 	}
1354 
1355 	/// Removes the selection for a particular package.
1356 	void deselectVersion(string package_id)
1357 	{
1358 		m_selections.remove(package_id);
1359 		m_dirty = true;
1360 	}
1361 
1362 	/// Determines if a particular package has a selection set.
1363 	bool hasSelectedVersion(string packageId)
1364 	const {
1365 		return (packageId in m_selections) !is null;
1366 	}
1367 
1368 	/** Returns the selection for a particular package.
1369 
1370 		Note that the returned `Dependency` can either have the
1371 		`Dependency.path` property set to a non-empty value, in which case this
1372 		is a path based selection, or its `Dependency.version_` property is
1373 		valid and it is a version selection.
1374 	*/
1375 	Dependency getSelectedVersion(string packageId)
1376 	const {
1377 		enforce(hasSelectedVersion(packageId));
1378 		return m_selections[packageId].dep;
1379 	}
1380 
1381 	/** Stores the selections to disk.
1382 
1383 		The target file will be written in JSON format. Usually, `defaultFile`
1384 		should be used as the file name and the directory should be the root
1385 		directory of the project's root package.
1386 	*/
1387 	void save(NativePath path)
1388 	{
1389 		Json json = serialize();
1390 		auto file = openFile(path, FileMode.createTrunc);
1391 		scope(exit) file.close();
1392 
1393 		assert(json.type == Json.Type.object);
1394 		assert(json.length == 2);
1395 		assert(json["versions"].type != Json.Type.undefined);
1396 
1397 		file.write("{\n\t\"fileVersion\": ");
1398 		file.writeJsonString(json["fileVersion"]);
1399 		file.write(",\n\t\"versions\": {");
1400 		auto vers = json["versions"].get!(Json[string]);
1401 		bool first = true;
1402 		foreach (k; vers.byKey.array.sort()) {
1403 			if (!first) file.write(",");
1404 			else first = false;
1405 			file.write("\n\t\t");
1406 			file.writeJsonString(Json(k));
1407 			file.write(": ");
1408 			file.writeJsonString(vers[k]);
1409 		}
1410 		file.write("\n\t}\n}\n");
1411 		m_dirty = false;
1412 		m_bare = false;
1413 	}
1414 
1415 	static Json dependencyToJson(Dependency d)
1416 	{
1417 		if (d.path.empty) return Json(d.version_.toString());
1418 		else return serializeToJson(["path": d.path.toString()]);
1419 	}
1420 
1421 	static Dependency dependencyFromJson(Json j)
1422 	{
1423 		if (j.type == Json.Type..string)
1424 			return Dependency(Version(j.get!string));
1425 		else if (j.type == Json.Type.object)
1426 			return Dependency(NativePath(j["path"].get!string));
1427 		else throw new Exception(format("Unexpected type for dependency: %s", j.type));
1428 	}
1429 
1430 	Json serialize()
1431 	const {
1432 		Json json = serializeToJson(m_selections);
1433 		Json serialized = Json.emptyObject;
1434 		serialized["fileVersion"] = FileVersion;
1435 		serialized["versions"] = Json.emptyObject;
1436 		foreach (p, v; m_selections)
1437 			serialized["versions"][p] = dependencyToJson(v.dep);
1438 		return serialized;
1439 	}
1440 
1441 	private void deserialize(Json json)
1442 	{
1443 		enforce(cast(int)json["fileVersion"] == FileVersion, "Mismatched dub.select.json version: " ~ to!string(cast(int)json["fileVersion"]) ~ "vs. " ~to!string(FileVersion));
1444 		clear();
1445 		scope(failure) clear();
1446 		foreach (string p, v; json["versions"])
1447 			m_selections[p] = Selected(dependencyFromJson(v));
1448 	}
1449 }
1450