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