1 /**
2 	Representing a full project, with a root Package and several dependencies.
3 
4 	Copyright: © 2012-2013 Matthias Dondorff
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.internal.utils;
13 import dub.internal.vibecompat.core.file;
14 import dub.internal.vibecompat.core.log;
15 import dub.internal.vibecompat.data.json;
16 import dub.internal.vibecompat.inet.url;
17 import dub.package_;
18 import dub.packagemanager;
19 import dub.packagesupplier;
20 import dub.generators.generator;
21 
22 
23 // todo: cleanup imports.
24 import std.algorithm;
25 import std.array;
26 import std.conv;
27 import std.datetime;
28 import std.exception;
29 import std.file;
30 import std.process;
31 import std.string;
32 import std.typecons;
33 import std.zip;
34 import std.encoding : sanitize;
35 
36 /// Representing a full project, with a root Package and several dependencies.
37 class Project {
38 	private {
39 		PackageManager m_packageManager;
40 		Json m_packageSettings;
41 		Package m_rootPackage;
42 		Package[] m_dependencies;
43 		Package[][Package] m_dependees;
44 		SelectedVersions m_selections;
45 	}
46 
47 	this(PackageManager package_manager, Path project_path)
48 	{
49 		Package pack;
50 		auto packageFile = Package.findPackageFile(project_path);
51 		if (packageFile.empty) {
52 			logWarn("There was no package description found for the application in '%s'.", project_path.toNativeString());
53 			pack = new Package(null, project_path);
54 		} else {
55 			pack = package_manager.getOrLoadPackage(project_path, packageFile);
56 		}
57 
58 		this(package_manager, pack);
59 	}
60 
61 	this(PackageManager package_manager, Package pack)
62 	{
63 		m_packageManager = package_manager;
64 		m_rootPackage = pack;
65 		m_packageSettings = Json.emptyObject;
66 
67 		try m_packageSettings = jsonFromFile(m_rootPackage.path ~ ".dub/dub.json", true);
68 		catch(Exception t) logDiagnostic("Failed to read .dub/dub.json: %s", t.msg);
69 
70 		auto selverfile = m_rootPackage.path ~ SelectedVersions.defaultFile;
71 		if (existsFile(selverfile)) {
72 			try m_selections = new SelectedVersions(selverfile);
73 			catch(Exception e) {
74 				logDiagnostic("A " ~ SelectedVersions.defaultFile ~ " file was not found or failed to load:\n%s", e.msg);
75 				m_selections = new SelectedVersions;
76 			}
77 		} else m_selections = new SelectedVersions;
78 
79 		reinit();
80 	}
81 
82 	/// Gathers information
83 	@property string info()
84 	const {
85 		if(!m_rootPackage)
86 			return "-Unrecognized application in '"~m_rootPackage.path.toNativeString()~"' (probably no dub.json in this directory)";
87 		string s = "-Application identifier: " ~ m_rootPackage.name;
88 		s ~= "\n" ~ m_rootPackage.generateInfoString();
89 		s ~= "\n-Retrieved dependencies:";
90 		foreach(p; m_dependencies)
91 			s ~= "\n" ~ p.generateInfoString();
92 		return s;
93 	}
94 
95 	/// Gets all retrieved packages as a "packageId" = "version" associative array
96 	@property string[string] cachedPackagesIDs() const {
97 		string[string] pkgs;
98 		foreach(p; m_dependencies)
99 			pkgs[p.name] = p.vers;
100 		return pkgs;
101 	}
102 
103 	/// List of retrieved dependency Packages
104 	@property const(Package[]) dependencies() const { return m_dependencies; }
105 
106 	/// Main package.
107 	@property inout(Package) rootPackage() inout { return m_rootPackage; }
108 
109 	/// The versions to use for all dependencies. Call reinit() after changing these.
110 	@property inout(SelectedVersions) selections() inout { return m_selections; }
111 
112 	/// Package manager instance used by the project.
113 	@property inout(PackageManager) packageManager() inout { return m_packageManager; }
114 
115 	/** Allows iteration of the dependency tree in topological order
116 	*/
117 	int delegate(int delegate(ref const Package)) getTopologicalPackageList(bool children_first = false, in Package root_package = null, string[string] configs = null)
118 	const {
119 		const(Package) rootpack = root_package ? root_package : m_rootPackage;
120 
121 		int iterator(int delegate(ref const Package) del)
122 		{
123 			int ret = 0;
124 			bool[const(Package)] visited;
125 			void perform_rec(in Package p){
126 				if( p in visited ) return;
127 				visited[p] = true;
128 
129 				if( !children_first ){
130 					ret = del(p);
131 					if( ret ) return;
132 				}
133 
134 				auto cfg = configs.get(p.name, null);
135 
136 				foreach (dn, dv; p.dependencies) {
137 					// filter out dependencies not in the current configuration set
138 					if (!p.hasDependency(dn, cfg)) continue;
139 					auto dependency = getDependency(dn, dv.optional);
140 					if(dependency) perform_rec(dependency);
141 					if( ret ) return;
142 				}
143 
144 				if( children_first ){
145 					ret = del(p);
146 					if( ret ) return;
147 				}
148 			}
149 			perform_rec(rootpack);
150 			return ret;
151 		}
152 
153 		return &iterator;
154 	}
155 
156 	inout(Package) getDependency(string name, bool isOptional)
157 	inout {
158 		foreach(dp; m_dependencies)
159 			if( dp.name == name )
160 				return dp;
161 		if(!isOptional) throw new Exception("Unknown dependency: "~name);
162 		else return null;
163 	}
164 
165 	string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true)
166 	const {
167 		auto cfgs = getPackageConfigs(platform, null, allow_non_library_configs);
168 		return cfgs[m_rootPackage.name];
169 	}
170 
171 	void validate()
172 	{
173 		// some basic package lint
174 		m_rootPackage.warnOnSpecialCompilerFlags();
175 		if (m_rootPackage.name != m_rootPackage.name.toLower()) {
176 			logWarn(`WARNING: DUB package names should always be lower case, please change `
177 				~ `to {"name": "%s"}. You can use {"targetName": "%s"} to keep the current `
178 				~ `executable name.`,
179 				m_rootPackage.name.toLower(), m_rootPackage.name);
180 		}
181 
182 		foreach (dn, ds; m_rootPackage.dependencies)
183 			if (ds.isExactVersion && ds.version_.isBranch) {
184 				logWarn("WARNING: A deprecated branch based version specification is used "
185 					~ "for the dependency %s. Please use numbered versions instead. Also "
186 					~ "note that you can still use the %s file to override a certain "
187 					~ "dependency to use a branch instead.",
188 					dn, SelectedVersions.defaultFile);
189 			}
190 
191 		bool[string] visited;
192 		void validateDependenciesRec(Package pack) {
193 			foreach (name, vspec_; pack.dependencies) {
194 				if (name in visited) continue;
195 				visited[name] = true;
196 
197 				auto basename = getBasePackageName(name);
198 				if (m_selections.hasSelectedVersion(basename)) {
199 					auto selver = m_selections.getSelectedVersion(basename);
200 					if (vspec_.merge(selver) == Dependency.invalid) {
201 						logWarn("Selected package %s %s does not match the dependency specification %s in package %s. Need to \"dub upgrade\"?",
202 							basename, selver, vspec_, pack.name);
203 					}
204 				}
205 
206 				auto deppack = getDependency(name, true);
207 				if (deppack) validateDependenciesRec(deppack);
208 			}
209 		}
210 		validateDependenciesRec(m_rootPackage);
211 	}
212 
213 	/// Rereads the applications state.
214 	void reinit()
215 	{
216 		m_dependencies = null;
217 		m_packageManager.refresh(false);
218 
219 		void collectDependenciesRec(Package pack)
220 		{
221 			logDebug("Collecting dependencies for %s", pack.name);
222 			foreach (name, vspec_; pack.dependencies) {
223 				Dependency vspec = vspec_;
224 				Package p;
225 				if (!vspec.path.empty) {
226 					Path path = vspec.path;
227 					if (!path.absolute) path = pack.path ~ path;
228 					logDiagnostic("Adding local %s", path);
229 					p = m_packageManager.getOrLoadPackage(path);
230 					if (name.canFind(':')) p = m_packageManager.getSubPackage(p, getSubPackageName(name), false);
231 					enforce(p.name == name,
232 						format("Path based dependency %s is referenced with a wrong name: %s vs. %s",
233 							path.toNativeString(), name, p.name));
234 				}
235 
236 				if (!p) {
237 					auto basename = getBasePackageName(name);
238 					if (name == m_rootPackage.basePackage.name) {
239 						vspec = Dependency(m_rootPackage.ver);
240 						p = m_rootPackage.basePackage;
241 					} else if (basename == m_rootPackage.basePackage.name) {
242 						vspec = Dependency(m_rootPackage.ver);
243 						try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, getSubPackageName(name), false);
244 						catch (Exception e) {
245 							logDiagnostic("Error getting sub package %s: %s", name, e.msg);
246 							continue;
247 						}
248 					} else if (m_selections.hasSelectedVersion(basename)) {
249 						vspec = m_selections.getSelectedVersion(basename);
250 						p = m_packageManager.getBestPackage(name, vspec);
251 					} else if (m_dependencies.canFind!(d => getBasePackageName(d.name) == basename)) {
252 						auto idx = m_dependencies.countUntil!(d => getBasePackageName(d.name) == basename);
253 						auto bp = m_dependencies[idx].basePackage;
254 						vspec = Dependency(bp.path);
255 						p = m_packageManager.getSubPackage(bp, getSubPackageName(name), false);
256 					} else {
257 						logDiagnostic("Version selection for dependency %s (%s) of %s is missing.",
258 							basename, name, pack.name);
259 						continue;
260 					}
261 				}
262 
263 				if (!p) {
264 					logDiagnostic("Missing dependency %s %s of %s", name, vspec, pack.name);
265 					continue;
266 				}
267 
268 				if (!m_dependencies.canFind(p)) {
269 					logDiagnostic("Found dependency %s %s", name, vspec.toString());
270 					m_dependencies ~= p;
271 					p.warnOnSpecialCompilerFlags();
272 					collectDependenciesRec(p);
273 				}
274 
275 				m_dependees[p] ~= pack;
276 				//enforce(p !is null, "Failed to resolve dependency "~name~" "~vspec.toString());
277 			}
278 		}
279 		collectDependenciesRec(m_rootPackage);
280 	}
281 
282 	/// Returns the applications name.
283 	@property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; }
284 
285 	@property string[] configurations() const { return m_rootPackage.configurations; }
286 
287 	/// Returns a map with the configuration for all packages in the dependency tree.
288 	string[string] getPackageConfigs(in BuildPlatform platform, string config, bool allow_non_library = true)
289 	const {
290 		struct Vertex { string pack, config; }
291 		struct Edge { size_t from, to; }
292 
293 		Vertex[] configs;
294 		Edge[] edges;
295 		string[][string] parents;
296 		parents[m_rootPackage.name] = null;
297 		foreach (p; getTopologicalPackageList())
298 			foreach (d; p.dependencies.byKey)
299 				parents[d] ~= p.name;
300 
301 
302 		size_t createConfig(string pack, string config) {
303 			foreach (i, v; configs)
304 				if (v.pack == pack && v.config == config)
305 					return i;
306 			logDebug("Add config %s %s", pack, config);
307 			configs ~= Vertex(pack, config);
308 			return configs.length-1;
309 		}
310 
311 		bool haveConfig(string pack, string config) {
312 			return configs.any!(c => c.pack == pack && c.config == config);
313 		}
314 
315 		size_t createEdge(size_t from, size_t to) {
316 			auto idx = edges.countUntil(Edge(from, to));
317 			if (idx >= 0) return idx;
318 			logDebug("Including %s %s -> %s %s", configs[from].pack, configs[from].config, configs[to].pack, configs[to].config);
319 			edges ~= Edge(from, to);
320 			return edges.length-1;
321 		}
322 
323 		void removeConfig(size_t i) {
324 			logDebug("Eliminating config %s for %s", configs[i].config, configs[i].pack);
325 			configs = configs.remove(i);
326 			edges = edges.filter!(e => e.from != i && e.to != i).array();
327 			foreach (ref e; edges) {
328 				if (e.from > i) e.from--;
329 				if (e.to > i) e.to--;
330 			}
331 		}
332 
333 		bool isReachable(string pack, string conf) {
334 			if (pack == configs[0].pack && configs[0].config == conf) return true;
335 			foreach (e; edges)
336 				if (configs[e.to].pack == pack && configs[e.to].config == conf)
337 					return true;
338 			return false;
339 			//return (pack == configs[0].pack && conf == configs[0].config) || edges.canFind!(e => configs[e.to].pack == pack && configs[e.to].config == config);
340 		}
341 
342 		bool isReachableByAllParentPacks(size_t cidx) {
343 			bool[string] r;
344 			foreach (p; parents[configs[cidx].pack]) r[p] = false;
345 			foreach (e; edges) {
346 				if (e.to != cidx) continue;
347 				if (auto pp = configs[e.from].pack in r) *pp = true;
348 			}
349 			foreach (bool v; r) if (!v) return false;
350 			return true;
351 		}
352 
353 		string[] allconfigs_path;
354 		// create a graph of all possible package configurations (package, config) -> (subpackage, subconfig)
355 		void determineAllConfigs(in Package p)
356 		{
357 			auto idx = allconfigs_path.countUntil(p.name);
358 			enforce(idx < 0, format("Detected dependency cycle: %s", (allconfigs_path[idx .. $] ~ p.name).join("->")));
359 			allconfigs_path ~= p.name;
360 			scope (exit) allconfigs_path.length--;
361 
362 			// first, add all dependency configurations
363 			foreach (dn; p.dependencies.byKey) {
364 				auto dp = getDependency(dn, true);
365 				if (!dp) continue;
366 				determineAllConfigs(dp);
367 			}
368 
369 			// for each configuration, determine the configurations usable for the dependencies
370 			outer: foreach (c; p.getPlatformConfigurations(platform, p is m_rootPackage && allow_non_library)) {
371 				string[][string] depconfigs;
372 				foreach (dn; p.dependencies.byKey) {
373 					auto dp = getDependency(dn, true);
374 					if (!dp) continue;
375 
376 					string[] cfgs;
377 					auto subconf = p.getSubConfiguration(c, dp, platform);
378 					if (!subconf.empty) cfgs = [subconf];
379 					else cfgs = dp.getPlatformConfigurations(platform);
380 					cfgs = cfgs.filter!(c => haveConfig(dn, c)).array;
381 
382 					// if no valid configuration was found for a dependency, don't include the
383 					// current configuration
384 					if (!cfgs.length) {
385 						logDebug("Skip %s %s (missing configuration for %s)", p.name, c, dp.name);
386 						continue outer;
387 					}
388 					depconfigs[dn] = cfgs;
389 				}
390 
391 				// add this configuration to the graph
392 				size_t cidx = createConfig(p.name, c);
393 				foreach (dn; p.dependencies.byKey)
394 					foreach (sc; depconfigs.get(dn, null))
395 						createEdge(cidx, createConfig(dn, sc));
396 			}
397 		}
398 		if (config.length) createConfig(m_rootPackage.name, config);
399 		determineAllConfigs(m_rootPackage);
400 
401 		// successively remove configurations until only one configuration per package is left
402 		bool changed;
403 		do {
404 			// remove all configs that are not reachable by all parent packages
405 			changed = false;
406 			for (size_t i = 0; i < configs.length; ) {
407 				if (!isReachableByAllParentPacks(i)) {
408 					logDebug("NOT REACHABLE by (%s):", parents[configs[i].pack]);
409 					removeConfig(i);
410 					changed = true;
411 				} else i++;
412 			}
413 
414 			// when all edges are cleaned up, pick one package and remove all but one config
415 			if (!changed) {
416 				foreach (p; getTopologicalPackageList()) {
417 					size_t cnt = 0;
418 					for (size_t i = 0; i < configs.length; ) {
419 						if (configs[i].pack == p.name) {
420 							if (++cnt > 1) {
421 								logDebug("NON-PRIMARY:");
422 								removeConfig(i);
423 							} else i++;
424 						} else i++;
425 					}
426 					if (cnt > 1) {
427 						changed = true;
428 						break;
429 					}
430 				}
431 			}
432 		} while (changed);
433 
434 		// print out the resulting tree
435 		foreach (e; edges) logDebug("    %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config);
436 
437 		// return the resulting configuration set as an AA
438 		string[string] ret;
439 		foreach (c; configs) {
440 			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]));
441 			logDebug("Using configuration '%s' for %s", c.config, c.pack);
442 			ret[c.pack] = c.config;
443 		}
444 
445 		// check for conflicts (packages missing in the final configuration graph)
446 		void checkPacksRec(in Package pack) {
447 			auto pc = pack.name in ret;
448 			enforce(pc !is null, "Could not resolve configuration for package "~pack.name);
449 			foreach (p, dep; pack.getDependencies(*pc)) {
450 				auto deppack = getDependency(p, dep.optional);
451 				if (deppack) checkPacksRec(deppack);
452 			}
453 		}
454 		checkPacksRec(m_rootPackage);
455 
456 		return ret;
457 	}
458 
459 	/**
460 	 * Fills dst with values from this project.
461 	 *
462 	 * dst gets initialized according to the given platform and config.
463 	 *
464 	 * Params:
465 	 *   dst = The BuildSettings struct to fill with data.
466 	 *   platform = The platform to retrieve the values for.
467 	 *   config = Values of the given configuration will be retrieved.
468 	 *   root_package = If non null, use it instead of the project's real root package.
469 	 *   shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary.
470 	 */
471 	void addBuildSettings(ref BuildSettings dst, in BuildPlatform platform, string config, in Package root_package = null, bool shallow = false)
472 	const {
473 		auto configs = getPackageConfigs(platform, config);
474 
475 		foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) {
476 			auto pkg_path = pkg.path.toNativeString();
477 			dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]);
478 
479 			assert(pkg.name in configs, "Missing configuration for "~pkg.name);
480 			logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]);
481 
482 			auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]);
483 			if (psettings.targetType != TargetType.none) {
484 				if (shallow && pkg !is m_rootPackage)
485 					psettings.sourceFiles = null;
486 				processVars(dst, this, pkg, psettings);
487 				if (psettings.importPaths.empty)
488 					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]);
489 				if (psettings.mainSourceFile.empty && pkg is m_rootPackage && psettings.targetType == TargetType.executable)
490 					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);
491 			}
492 			if (pkg is m_rootPackage) {
493 				if (!shallow) {
494 					enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build.");
495 					enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build.");
496 				}
497 				dst.targetType = psettings.targetType;
498 				dst.targetPath = psettings.targetPath;
499 				dst.targetName = psettings.targetName;
500 				if (!psettings.workingDirectory.empty)
501 					dst.workingDirectory = processVars(psettings.workingDirectory, this, pkg, true);
502 				if (psettings.mainSourceFile.length)
503 					dst.mainSourceFile = processVars(psettings.mainSourceFile, this, pkg, true);
504 			}
505 		}
506 
507 		// always add all version identifiers of all packages
508 		foreach (pkg; this.getTopologicalPackageList(false, null, configs)) {
509 			auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]);
510 			dst.addVersions(psettings.versions);
511 		}
512 	}
513 
514 	void addBuildTypeSettings(ref BuildSettings dst, in BuildPlatform platform, string build_type)
515 	{
516 		bool usedefflags = !(dst.requirements & BuildRequirements.noDefaultFlags);
517 		if (usedefflags) {
518 			BuildSettings btsettings;
519 			m_rootPackage.addBuildTypeSettings(btsettings, platform, build_type);
520 			processVars(dst, this, m_rootPackage, btsettings);
521 		}
522 	}
523 
524 	/// Determines if the given dependency is already indirectly referenced by other dependencies of pack.
525 	bool isRedundantDependency(in Package pack, in Package dependency)
526 	const {
527 		foreach (dep; pack.dependencies.byKey) {
528 			auto dp = getDependency(dep, true);
529 			if (!dp) continue;
530 			if (dp is dependency) continue;
531 			foreach (ddp; getTopologicalPackageList(false, dp))
532 				if (ddp is dependency) return true;
533 		}
534 		return false;
535 	}
536 
537 	/*bool iterateDependencies(bool delegate(Package pack, string dep_name, Dependency dep_spec) del)
538 	{
539 		bool all_found = true;
540 
541 		bool[string] visited;
542 		void iterate(Package pack)
543 		{
544 			if (pack.name in visited) return;
545 			visited[pack.name] = true;
546 
547 			foreach (dn, ds; pack.dependencies) {
548 				auto dep = del(pack, dn, ds);
549 				if (dep) iterateDependencies(dep);
550 				else all_found = false;
551 			}
552 		}
553 
554 		return all_found;
555 	}*/
556 
557 	/// Outputs a JSON description of the project, including its deoendencies.
558 	void describe(ref Json dst, BuildPlatform platform, string config)
559 	{
560 		dst.mainPackage = m_rootPackage.name; // deprecated
561 		dst.rootPackage = m_rootPackage.name;
562 
563 		auto configs = getPackageConfigs(platform, config);
564 
565 		// FIXME: use the generator system to collect the list of actually used build dependencies and source files
566 
567 		auto mp = Json.emptyObject;
568 		m_rootPackage.describe(mp, platform, config);
569 		dst.packages = Json([mp]);
570 
571 		foreach (dep; m_dependencies) {
572 			auto dp = Json.emptyObject;
573 			dep.describe(dp, platform, configs[dep.name]);
574 			dst.packages = dst.packages.get!(Json[]) ~ dp;
575 		}
576 	}
577 
578 	void saveSelections()
579 	{
580 		assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections).");
581 		if (m_selections.hasSelectedVersion(m_rootPackage.basePackage.name))
582 			m_selections.deselectVersion(m_rootPackage.basePackage.name);
583 
584 		auto path = m_rootPackage.path ~ SelectedVersions.defaultFile;
585 		if (m_selections.dirty || !existsFile(path))
586 			m_selections.save(path);
587 	}
588 
589 	bool isUpgradeCacheUpToDate()
590 	{
591 		try {
592 			auto datestr = m_packageSettings["dub"].opt!(Json[string]).get("lastUpgrade", Json("")).get!string;
593 			if (!datestr.length) return false;
594 			auto date = SysTime.fromISOExtString(datestr);
595 			if ((Clock.currTime() - date) > 1.days) return false;
596 			return true;
597 		} catch (Exception t) {
598 			logDebug("Failed to get the last upgrade time: %s", t.msg);
599 			return false;
600 		}
601 	}
602 
603 	Dependency[string] getUpgradeCache()
604 	{
605 		try {
606 			Dependency[string] ret;
607 			foreach (string p, d; m_packageSettings["dub"].opt!(Json[string]).get("cachedUpgrades", Json.emptyObject))
608 				ret[p] = SelectedVersions.dependencyFromJson(d);
609 			return ret;
610 		} catch (Exception t) {
611 			logDebug("Failed to get cached upgrades: %s", t.msg);
612 			return null;
613 		}
614 	}
615 
616 	void setUpgradeCache(Dependency[string] versions)
617 	{
618 		logDebug("markUpToDate");
619 		Json create(ref Json json, string object) {
620 			if( object !in json ) json[object] = Json.emptyObject;
621 			return json[object];
622 		}
623 		create(m_packageSettings, "dub");
624 		m_packageSettings["dub"]["lastUpgrade"] = Clock.currTime().toISOExtString();
625 
626 		create(m_packageSettings["dub"], "cachedUpgrades");
627 		foreach (p, d; versions)
628 			m_packageSettings["dub"]["cachedUpgrades"][p] = SelectedVersions.dependencyToJson(d);
629 
630 		writeDubJson();
631 	}
632 
633 	private void writeDubJson() {
634 		// don't bother to write an empty file
635 		if( m_packageSettings.length == 0 ) return;
636 
637 		try {
638 			logDebug("writeDubJson");
639 			auto dubpath = m_rootPackage.path~".dub";
640 			if( !exists(dubpath.toNativeString()) ) mkdir(dubpath.toNativeString());
641 			auto dstFile = openFile((dubpath~"dub.json").toString(), FileMode.CreateTrunc);
642 			scope(exit) dstFile.close();
643 			dstFile.writePrettyJsonString(m_packageSettings);
644 		} catch( Exception e ){
645 			logWarn("Could not write .dub/dub.json.");
646 		}
647 	}
648 }
649 
650 /// Actions to be performed by the dub
651 struct Action {
652 	enum Type {
653 		fetch,
654 		remove,
655 		conflict,
656 		failure
657 	}
658 
659 	immutable {
660 		Type type;
661 		string packageId;
662 		PlacementLocation location;
663 		Dependency vers;
664 		Version existingVersion;
665 	}
666 	const Package pack;
667 	const Dependency[string] issuer;
668 
669 	static Action get(string pkg, PlacementLocation location, in Dependency dep, Dependency[string] context, Version old_version = Version.UNKNOWN)
670 	{
671 		return Action(Type.fetch, pkg, location, dep, context, old_version);
672 	}
673 
674 	static Action remove(Package pkg, Dependency[string] context)
675 	{
676 		return Action(Type.remove, pkg, context);
677 	}
678 
679 	static Action conflict(string pkg, in Dependency dep, Dependency[string] context)
680 	{
681 		return Action(Type.conflict, pkg, PlacementLocation.user, dep, context);
682 	}
683 
684 	static Action failure(string pkg, in Dependency dep, Dependency[string] context)
685 	{
686 		return Action(Type.failure, pkg, PlacementLocation.user, dep, context);
687 	}
688 
689 	private this(Type id, string pkg, PlacementLocation location, in Dependency d, Dependency[string] issue, Version existing_version = Version.UNKNOWN)
690 	{
691 		this.type = id;
692 		this.packageId = pkg;
693 		this.location = location;
694 		this.vers = d;
695 		this.issuer = issue;
696 		this.existingVersion = existing_version;
697 	}
698 
699 	private this(Type id, Package pkg, Dependency[string] issue)
700 	{
701 		pack = pkg;
702 		type = id;
703 		packageId = pkg.name;
704 		vers = cast(immutable)Dependency(pkg.ver);
705 		issuer = issue;
706 	}
707 
708 	string toString() const {
709 		return to!string(type) ~ ": " ~ packageId ~ ", " ~ to!string(vers);
710 	}
711 }
712 
713 
714 /// Indicates where a package has been or should be placed to.
715 enum PlacementLocation {
716 	/// Packages retrived with 'local' will be placed in the current folder
717 	/// using the package name as destination.
718 	local,
719 	/// Packages with 'userWide' will be placed in a folder accessible by
720 	/// all of the applications from the current user.
721 	user,
722 	/// Packages retrieved with 'systemWide' will be placed in a shared folder,
723 	/// which can be accessed by all users of the system.
724 	system
725 }
726 
727 /// The default placement location of fetched packages. Can be changed by --local or --system.
728 auto defaultPlacementLocation = PlacementLocation.user;
729 
730 void processVars(ref BuildSettings dst, in Project project, in Package pack, BuildSettings settings, bool include_target_settings = false)
731 
732 {
733 	dst.addDFlags(processVars(project, pack, settings.dflags));
734 	dst.addLFlags(processVars(project, pack, settings.lflags));
735 	dst.addLibs(processVars(project, pack, settings.libs));
736 	dst.addSourceFiles(processVars(project, pack, settings.sourceFiles, true));
737 	dst.addImportFiles(processVars(project, pack, settings.importFiles, true));
738 	dst.addStringImportFiles(processVars(project, pack, settings.stringImportFiles, true));
739 	dst.addCopyFiles(processVars(project, pack, settings.copyFiles, true));
740 	dst.addVersions(processVars(project, pack, settings.versions));
741 	dst.addDebugVersions(processVars(project, pack, settings.debugVersions));
742 	dst.addImportPaths(processVars(project, pack, settings.importPaths, true));
743 	dst.addStringImportPaths(processVars(project, pack, settings.stringImportPaths, true));
744 	dst.addPreGenerateCommands(processVars(project, pack, settings.preGenerateCommands));
745 	dst.addPostGenerateCommands(processVars(project, pack, settings.postGenerateCommands));
746 	dst.addPreBuildCommands(processVars(project, pack, settings.preBuildCommands));
747 	dst.addPostBuildCommands(processVars(project, pack, settings.postBuildCommands));
748 	dst.addRequirements(settings.requirements);
749 	dst.addOptions(settings.options);
750 
751 	if (include_target_settings) {
752 		dst.targetType = settings.targetType;
753 		dst.targetPath = processVars(settings.targetPath, project, pack, true);
754 		dst.targetName = settings.targetName;
755 		if (!settings.workingDirectory.empty)
756 			dst.workingDirectory = processVars(settings.workingDirectory, project, pack, true);
757 		if (settings.mainSourceFile.length)
758 			dst.mainSourceFile = processVars(settings.mainSourceFile, project, pack, true);
759 	}
760 }
761 
762 private string[] processVars(in Project project, in Package pack, string[] vars, bool are_paths = false)
763 {
764 	auto ret = appender!(string[])();
765 	processVars(ret, project, pack, vars, are_paths);
766 	return ret.data;
767 
768 }
769 private void processVars(ref Appender!(string[]) dst, in Project project, in Package pack, string[] vars, bool are_paths = false)
770 {
771 	foreach (var; vars) dst.put(processVars(var, project, pack, are_paths));
772 }
773 
774 private string processVars(string var, in Project project, in Package pack, bool is_path)
775 {
776 	auto idx = std..string.indexOf(var, '$');
777 	if (idx >= 0) {
778 		auto vres = appender!string();
779 		while (idx >= 0) {
780 			if (idx+1 >= var.length) break;
781 			if (var[idx+1] == '$') {
782 				vres.put(var[0 .. idx+1]);
783 				var = var[idx+2 .. $];
784 			} else {
785 				vres.put(var[0 .. idx]);
786 				var = var[idx+1 .. $];
787 
788 				size_t idx2 = 0;
789 				while( idx2 < var.length && isIdentChar(var[idx2]) ) idx2++;
790 				auto varname = var[0 .. idx2];
791 				var = var[idx2 .. $];
792 
793 				vres.put(getVariable(varname, project, pack));
794 			}
795 			idx = std..string.indexOf(var, '$');
796 		}
797 		vres.put(var);
798 		var = vres.data;
799 	}
800 	if (is_path) {
801 		auto p = Path(var);
802 		if (!p.absolute) {
803 			logDebug("Fixing relative path: %s ~ %s", pack.path.toNativeString(), p.toNativeString());
804 			return (pack.path ~ p).toNativeString();
805 		} else return p.toNativeString();
806 	} else return var;
807 }
808 
809 private string getVariable(string name, in Project project, in Package pack)
810 {
811 	if (name == "PACKAGE_DIR") return pack.path.toNativeString();
812 	if (name == "ROOT_PACKAGE_DIR") return project.rootPackage.path.toNativeString();
813 
814 	if (name.endsWith("_PACKAGE_DIR")) {
815 		auto pname = name[0 .. $-12];
816 		foreach (prj; project.getTopologicalPackageList())
817 			if (prj.name.toUpper().replace("-", "_") == pname)
818 				return prj.path.toNativeString();
819 	}
820 
821 	if (auto envvar = environment.get(name)) return envvar;
822 
823 	throw new Exception("Invalid variable: "~name);
824 }
825 
826 private bool isIdentChar(dchar ch)
827 {
828 	return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '_';
829 }
830 
831 string stripDlangSpecialChars(string s)
832 {
833 	import std.array;
834 	import std.uni;
835 	auto ret = appender!string();
836 	foreach(ch; s)
837 		ret.put(isIdentChar(ch) ? ch : '_');
838 	return ret.data;
839 }
840 
841 final class SelectedVersions {
842 	private struct Selected {
843 		Dependency dep;
844 		//Dependency[string] packages;
845 	}
846 	private {
847 		enum FileVersion = 1;
848 		Selected[string] m_selections;
849 		bool m_dirty = false; // has changes since last save
850 	}
851 
852 	enum defaultFile = "dub.selections.json";
853 
854 	this() {}
855 
856 	this(Json data)
857 	{
858 		deserialize(data);
859 		m_dirty = false;
860 	}
861 
862 	this(Path path)
863 	{
864 		auto json = jsonFromFile(path);
865 		deserialize(json);
866 		m_dirty = false;
867 	}
868 
869 	@property string[] selectedPackages() const { return m_selections.keys; }
870 
871 	@property bool dirty() const { return m_dirty; }
872 
873 	void clear()
874 	{
875 		m_selections = null;
876 		m_dirty = true;
877 	}
878 
879 	void set(SelectedVersions versions)
880 	{
881 		m_selections = versions.m_selections.dup;
882 		m_dirty = true;
883 	}
884 
885 	void selectVersion(string package_id, Version version_)
886 	{
887 		if (auto ps = package_id in m_selections) {
888 			if (ps.dep == Dependency(version_))
889 				return;
890 		}
891 		m_selections[package_id] = Selected(Dependency(version_)/*, issuer*/);
892 		m_dirty = true;
893 	}
894 
895 	void selectVersion(string package_id, Path path)
896 	{
897 		if (auto ps = package_id in m_selections) {
898 			if (ps.dep == Dependency(path))
899 				return;
900 		}
901 		m_selections[package_id] = Selected(Dependency(path));
902 		m_dirty = true;
903 	}
904 
905 	void deselectVersion(string package_id)
906 	{
907 		m_selections.remove(package_id);
908 		m_dirty = true;
909 	}
910 
911 	bool hasSelectedVersion(string packageId)
912 	const {
913 		return (packageId in m_selections) !is null;
914 	}
915 
916 	Dependency getSelectedVersion(string packageId)
917 	const {
918 		enforce(hasSelectedVersion(packageId));
919 		return m_selections[packageId].dep;
920 	}
921 
922 	void save(Path path)
923 	{
924 		Json json = serialize();
925 		auto file = openFile(path, FileMode.CreateTrunc);
926 		scope(exit) file.close();
927 		file.writePrettyJsonString(json);
928 		file.put('\n');
929 		m_dirty = false;
930 	}
931 
932 	static Json dependencyToJson(Dependency d)
933 	{
934 		if (d.path.empty) return Json(d.version_.toString());
935 		else return serializeToJson(["path": d.path.toString()]);
936 	}
937 
938 	static Dependency dependencyFromJson(Json j)
939 	{
940 		if (j.type == Json.Type..string)
941 			return Dependency(Version(j.get!string));
942 		else if (j.type == Json.Type.object)
943 			return Dependency(Path(j.path.get!string));
944 		else throw new Exception(format("Unexpected type for dependency: %s", j.type));
945 	}
946 
947 	Json serialize()
948 	const {
949 		Json json = serializeToJson(m_selections);
950 		Json serialized = Json.emptyObject;
951 		serialized.fileVersion = FileVersion;
952 		serialized.versions = Json.emptyObject;
953 		foreach (p, v; m_selections)
954 			serialized.versions[p] = dependencyToJson(v.dep);
955 		return serialized;
956 	}
957 
958 	private void deserialize(Json json)
959 	{
960 		enforce(cast(int)json["fileVersion"] == FileVersion, "Mismatched dub.select.json version: " ~ to!string(cast(int)json["fileVersion"]) ~ "vs. " ~to!string(FileVersion));
961 		clear();
962 		scope(failure) clear();
963 		foreach (string p, v; json.versions)
964 			m_selections[p] = Selected(dependencyFromJson(v));
965 	}
966 }