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 
35 
36 /// Representing a full project, with a root Package and several dependencies.
37 class Project {
38 	private {
39 		bool m_fixedPackage;
40 		Path m_root;
41 		PackageManager m_packageManager;
42 		Json m_json;
43 		Package m_main;
44 		//Package[string] m_packages;
45 		Package[] m_dependencies;
46 		Package[][Package] m_dependees;
47 	}
48 
49 	this(PackageManager package_manager, Path project_path)
50 	{
51 		m_packageManager = package_manager;
52 		m_root = project_path;
53 		m_fixedPackage = false;
54 		m_json = Json.emptyObject;
55 		reinit();
56 	}
57 
58 	this(PackageManager package_manager, Package pack)
59 	{
60 		m_packageManager = package_manager;
61 		m_root = pack.path;
62 		m_main = pack;
63 		m_fixedPackage = true;
64 		m_json = Json.emptyObject;
65 		reinit();
66 	}
67 
68 	/// Gathers information
69 	@property string info()
70 	const {
71 		if(!m_main)
72 			return "-Unrecognized application in '"~m_root.toNativeString()~"' (probably no package.json in this directory)";
73 		string s = "-Application identifier: " ~ m_main.name;
74 		s ~= "\n" ~ m_main.generateInfoString();
75 		s ~= "\n-Retrieved dependencies:";
76 		foreach(p; m_dependencies)
77 			s ~= "\n" ~ p.generateInfoString();
78 		return s;
79 	}
80 
81 	/// Gets all retrieved packages as a "packageId" = "version" associative array
82 	@property string[string] cachedPackagesIDs() const {
83 		string[string] pkgs;
84 		foreach(p; m_dependencies)
85 			pkgs[p.name] = p.vers;
86 		return pkgs;
87 	}
88 
89 	/// List of retrieved dependency Packages
90 	@property const(Package[]) dependencies() const { return m_dependencies; }
91 	
92 	/// Main package.
93 	@property inout(Package) mainPackage() inout { return m_main; }
94 
95 	/** Allows iteration of the dependency tree in topological order
96 	*/
97 	int delegate(int delegate(ref const Package)) getTopologicalPackageList(bool children_first = false, in Package root_package = null, string[string] configs = null)
98 	const {
99 		const(Package) rootpack = root_package ? root_package : m_main;
100 	
101 		int iterator(int delegate(ref const Package) del)
102 		{
103 			int ret = 0;
104 			bool[const(Package)] visited;
105 			void perform_rec(in Package p){
106 				if( p in visited ) return;
107 				visited[p] = true;
108 
109 				if( !children_first ){
110 					ret = del(p);
111 					if( ret ) return;
112 				}
113 
114 				auto cfg = configs.get(p.name, null);
115 
116 				foreach (dn, dv; p.dependencies) {
117 					// filter out dependencies not in the current configuration set
118 					if (!p.hasDependency(dn, cfg)) continue;
119 					auto dependency = getDependency(dn, dv.optional);
120 					if(dependency) perform_rec(dependency);
121 					if( ret ) return;
122 				}
123 
124 				if( children_first ){
125 					ret = del(p);
126 					if( ret ) return;
127 				}
128 			}
129 			perform_rec(rootpack);
130 			return ret;
131 		}
132 		
133 		return &iterator;
134 	}
135 
136 	inout(Package) getDependency(string name, bool isOptional)
137 	inout {
138 		foreach(dp; m_dependencies)
139 			if( dp.name == name )
140 				return dp;
141 		if(!isOptional) throw new Exception("Unknown dependency: "~name);
142 		else return null;
143 	}
144 
145 	string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true)
146 	const {
147 		auto cfgs = getPackageConfigs(platform, null, allow_non_library_configs);
148 		return cfgs[m_main.name];
149 	}
150 
151 	/// Rereads the applications state.
152 	void reinit()
153 	{
154 		scope(failure){
155 			logDiagnostic("Failed to initialize project. Assuming defaults.");
156 			if (!m_fixedPackage) m_main = new Package(serializeToJson(["name": "unknown"]), m_root);
157 		}
158 
159 		m_dependencies = null;
160 		m_packageManager.refresh(false);
161 
162 		try m_json = jsonFromFile(m_root ~ ".dub/dub.json", true);
163 		catch(Exception t) logDiagnostic("Failed to read .dub/dub.json: %s", t.msg);
164 
165 		// load package description
166 		if (!m_fixedPackage) {
167 			if (!Package.isPackageAt(m_root)) {
168 				logWarn("There was no package description found for the application in '%s'.", m_root.toNativeString());
169 				auto json = Json.emptyObject;
170 				json.name = "unknown";
171 				m_main = new Package(json, m_root);
172 				return;
173 			}
174 
175 			m_main = m_packageManager.getPackage(m_root);
176 		}
177 
178 		// some basic package lint
179 		m_main.warnOnSpecialCompilerFlags();
180 		if (m_main.name != m_main.name.toLower()) {
181 			logWarn(`DUB package names should always be lower case, please change to {"name": "%s"}. You can use {"targetName": "%s"} to keep the current executable name.`,
182 				m_main.name.toLower(), m_main.name);
183 		}
184 
185 		// TODO: compute the set of mutual dependencies first
186 		// (i.e. ">=0.0.1 <=0.0.5" and "<= 0.0.4" get ">=0.0.1 <=0.0.4")
187 		// conflicts would then also be detected.
188 		void collectDependenciesRec(Package pack)
189 		{
190 			logDiagnostic("Collecting dependencies for %s", pack.name);
191 			foreach( name, vspec; pack.dependencies ){
192 				Package p;
193 				if( !vspec.path.empty ){
194 					Path path = vspec.path;
195 					if( !path.absolute ) path = pack.path ~ path;
196 					logDiagnostic("Adding local %s %s", path, vspec.version_);
197 					p = m_packageManager.getTemporaryPackage(path, vspec.version_);
198 				} else {
199 					p = m_packageManager.getBestPackage(name, vspec);
200 				}
201 				if( !m_dependencies.canFind(p) ){
202 					logDiagnostic("Found dependency %s %s: %s", name, vspec.toString(), p !is null);
203 					if( p ){
204 						m_dependencies ~= p;
205 						p.warnOnSpecialCompilerFlags();
206 						collectDependenciesRec(p);
207 					}
208 				}
209 				m_dependees[p] ~= pack;
210 				//enforce(p !is null, "Failed to resolve dependency "~name~" "~vspec.toString());
211 			}
212 		}
213 		collectDependenciesRec(m_main);
214 	}
215 
216 	/// Returns the applications name.
217 	@property string name() const { return m_main ? m_main.name : "app"; }
218 
219 	@property string[] configurations() const { return m_main.configurations; }
220 
221 	/// Returns a map with the configuration for all packages in the dependency tree. 
222 	string[string] getPackageConfigs(in BuildPlatform platform, string config, bool allow_non_library = true)
223 	const {
224 		struct Vertex { string pack, config; }
225 		struct Edge { size_t from, to; }
226 
227 		Vertex[] configs;
228 		Edge[] edges;
229 		string[][string] parents;
230 		parents[m_main.name] = null;
231 		foreach (p; getTopologicalPackageList())
232 			foreach (d; p.dependencies.byKey)
233 				parents[d] ~= p.name;
234 
235 
236 		size_t createConfig(string pack, string config) {
237 			foreach (i, v; configs)
238 				if (v.pack == pack && v.config == config)
239 					return i;
240 			logDebug("Add config %s %s", pack, config);
241 			configs ~= Vertex(pack, config);
242 			return configs.length-1;
243 		}
244 
245 		bool haveConfig(string pack, string config) {
246 			return configs.any!(c => c.pack == pack && c.config == config);
247 		}
248 
249 		size_t createEdge(size_t from, size_t to) {
250 			auto idx = edges.countUntil(Edge(from, to));
251 			if (idx >= 0) return idx;
252 			logDebug("Including %s %s -> %s %s", configs[from].pack, configs[from].config, configs[to].pack, configs[to].config);
253 			edges ~= Edge(from, to);
254 			return edges.length-1;
255 		}
256 
257 		void removeConfig(size_t i) {
258 			logDebug("Eliminating config %s for %s", configs[i].config, configs[i].pack);
259 			configs = configs.remove(i);
260 			edges = edges.filter!(e => e.from != i && e.to != i).array();
261 			foreach (ref e; edges) {
262 				if (e.from > i) e.from--;
263 				if (e.to > i) e.to--;
264 			}
265 		}
266 
267 		bool isReachable(string pack, string conf) {
268 			if (pack == configs[0].pack && configs[0].config == conf) return true;
269 			foreach (e; edges)
270 				if (configs[e.to].pack == pack && configs[e.to].config == conf)
271 					return true;
272 			return false;
273 			//return (pack == configs[0].pack && conf == configs[0].config) || edges.canFind!(e => configs[e.to].pack == pack && configs[e.to].config == config);
274 		}
275 
276 		bool isReachableByAllParentPacks(size_t cidx) {
277 			bool[string] r;
278 			foreach (p; parents[configs[cidx].pack]) r[p] = false;
279 			foreach (e; edges) {
280 				if (e.to != cidx) continue;
281 				if (auto pp = configs[e.from].pack in r) *pp = true;
282 			}
283 			foreach (bool v; r) if (!v) return false;
284 			return true;
285 		}
286 
287 		// create a graph of all possible package configurations (package, config) -> (subpackage, subconfig)
288 		void determineAllConfigs(in Package p)
289 		{
290 			// first, add all dependency configurations
291 			foreach (dn; p.dependencies.byKey) {
292 				auto dp = getDependency(dn, true);
293 				if (!dp) continue;
294 				determineAllConfigs(dp);
295 			}
296 
297 			// for each configuration, determine the configurations usable for the dependencies
298 			outer: foreach (c; p.getPlatformConfigurations(platform, p is m_main && allow_non_library)) {
299 				string[][string] depconfigs;
300 				foreach (dn; p.dependencies.byKey) {
301 					auto dp = getDependency(dn, true);
302 					if (!dp) continue;
303 
304 					string[] cfgs;
305 					auto subconf = p.getSubConfiguration(c, dp, platform);
306 					if (!subconf.empty) cfgs = [subconf];
307 					else cfgs = dp.getPlatformConfigurations(platform);
308 					cfgs = cfgs.filter!(c => haveConfig(dn, c)).array;
309 
310 					// if no valid configuration was found for a dependency, don't include the
311 					// current configuration
312 					if (!cfgs.length) {
313 						logDebug("Skip %s %s (missing configuration for %s)", p.name, c, dp.name);
314 						continue outer;
315 					}
316 					depconfigs[dn] = cfgs;
317 				}
318 
319 				// add this configuration to the graph
320 				size_t cidx = createConfig(p.name, c);
321 				foreach (dn; p.dependencies.byKey)
322 					foreach (sc; depconfigs.get(dn, null))
323 						createEdge(cidx, createConfig(dn, sc));
324 			}
325 		}
326 		if (config.length) createConfig(m_main.name, config);
327 		determineAllConfigs(m_main);
328 
329 		// successively remove configurations until only one configuration per package is left
330 		bool changed;
331 		do {
332 			// remove all configs that are not reachable by all parent packages
333 			changed = false;
334 			for (size_t i = 0; i < configs.length; ) {
335 				if (!isReachableByAllParentPacks(i)) {
336 					logDebug("NOT REACHABLE by (%s):", parents[configs[i].pack]);
337 					removeConfig(i);
338 					changed = true;
339 				} else i++;
340 			}
341 
342 			// when all edges are cleaned up, pick one package and remove all but one config
343 			if (!changed) {
344 				foreach (p; getTopologicalPackageList()) {
345 					size_t cnt = 0;
346 					for (size_t i = 0; i < configs.length; ) {
347 						if (configs[i].pack == p.name) {
348 							if (++cnt > 1) {
349 								logDebug("NON-PRIMARY:");
350 								removeConfig(i);
351 							} else i++;
352 						} else i++;
353 					}
354 					if (cnt > 1) {
355 						changed = true;
356 						break;
357 					}
358 				}
359 			}
360 		} while (changed);
361 
362 		// print out the resulting tree
363 		foreach (e; edges) logDebug("    %s %s -> %s %s", configs[e.from].pack, configs[e.from].config, configs[e.to].pack, configs[e.to].config);
364 
365 		// return the resulting configuration set as an AA
366 		string[string] ret;
367 		foreach (c; configs) {
368 			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]));
369 			logDebug("Using configuration '%s' for %s", c.config, c.pack);
370 			ret[c.pack] = c.config;
371 		}
372 
373 		// check for conflicts (packages missing in the final configuration graph)
374 		foreach (p; getTopologicalPackageList())
375 			enforce(p.name in ret, "Could not resolve configuration for package "~p.name);
376 
377 		return ret;
378 	}
379 
380 	/**
381 	 * Fills dst with values from this project.
382 	 *
383 	 * dst gets initialized according to the given platform and config. 
384 	 *
385 	 * Params:
386 	 *   dst = The BuildSettings struct to fill with data.
387 	 *   platform = The platform to retrieve the values for.
388 	 *   config = Values of the given configuration will be retrieved.
389 	 *   root_package = If non null, use it instead of the project's real root package.
390 	 *   shallow = If true, collects only build settings for the main package (including inherited settings) and doesn't stop on target type none and sourceLibrary.
391 	 */
392 	void addBuildSettings(ref BuildSettings dst, in BuildPlatform platform, string config, in Package root_package = null, bool shallow = false)
393 	const {
394 		auto configs = getPackageConfigs(platform, config);
395 
396 		foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) {
397 			auto pkg_path = pkg.path.toNativeString();
398 			dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]);
399 
400 			assert(pkg.name in configs, "Missing configuration for "~pkg.name);
401 			logDebug("Gathering build settings for %s (%s)", pkg.name, configs[pkg.name]);
402 			
403 			auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]);
404 			if (psettings.targetType != TargetType.none) {
405 				if (shallow && pkg !is m_main)
406 					psettings.sourceFiles = null;
407 				processVars(dst, pkg_path, psettings);
408 				if (psettings.importPaths.empty)
409 					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]);
410 				if (psettings.mainSourceFile.empty && pkg is m_main && psettings.targetType == TargetType.executable)
411 					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);
412 			}
413 			if (pkg is m_main) {
414 				if (!shallow) {
415 					enforce(psettings.targetType != TargetType.none, "Main package has target type \"none\" - stopping build.");
416 					enforce(psettings.targetType != TargetType.sourceLibrary, "Main package has target type \"sourceLibrary\" which generates no target - stopping build.");
417 				}
418 				dst.targetType = psettings.targetType;
419 				dst.targetPath = psettings.targetPath;
420 				dst.targetName = psettings.targetName;
421 				dst.workingDirectory = processVars(psettings.workingDirectory, pkg_path, true);
422 				if (psettings.mainSourceFile.length)
423 					dst.mainSourceFile = processVars(psettings.mainSourceFile, pkg_path, true);
424 			}
425 		}
426 
427 		// always add all version identifiers of all packages
428 		foreach (pkg; this.getTopologicalPackageList(false, null, configs)) {
429 			auto psettings = pkg.getBuildSettings(platform, configs[pkg.name]);
430 			dst.addVersions(psettings.versions);
431 		}
432 	}
433 
434 	void addBuildTypeSettings(ref BuildSettings dst, in BuildPlatform platform, string build_type)
435 	{
436 		bool usedefflags = !(dst.requirements & BuildRequirements.noDefaultFlags);
437 		if (usedefflags) {
438 			BuildSettings btsettings;
439 			m_main.addBuildTypeSettings(btsettings, platform, build_type);
440 			processVars(dst, m_main.path.toNativeString(), btsettings);
441 		}
442 	}
443 
444 	/// Determines if the given dependency is already indirectly referenced by other dependencies of pack.
445 	bool isRedundantDependency(in Package pack, in Package dependency)
446 	const {
447 		foreach (dep; pack.dependencies.byKey) {
448 			auto dp = getDependency(dep, true);
449 			if (!dp) continue;
450 			if (dp is dependency) continue;
451 			foreach (ddp; getTopologicalPackageList(false, dp))
452 				if (ddp is dependency) return true;
453 		}
454 		return false;
455 	}
456 
457 
458 	/// Actions which can be performed to update the application.
459 	Action[] determineActions(PackageSupplier[] packageSuppliers, UpdateOptions option)
460 	{
461 		scope(exit) writeDubJson();
462 
463 		if(!m_main) {
464 			Action[] a;
465 			return a;
466 		}
467 
468 		auto graph = new DependencyGraph(m_main);
469 		if(!gatherMissingDependencies(packageSuppliers, graph) || graph.missing().length > 0) {
470 			// Check the conflicts first.
471 			auto conflicts = graph.conflicted();
472 			if(conflicts.length > 0) {
473 				logError("The dependency graph could not be filled, there are conflicts.");
474 				Action[] actions;
475 				foreach( string pkg, dbp; graph.conflicted())
476 					actions ~= Action.conflict(pkg, dbp.dependency, dbp.packages);
477 				
478 				// Missing dependencies could have some bogus results, therefore
479 				// return only the conflicts.
480 				return actions;
481 			}
482 
483 			// Then check unresolved dependencies.
484 			logError("The dependency graph could not be filled, there are unresolved dependencies.");
485 			Action[] actions;
486 			foreach( string pkg, rdp; graph.missing())
487 				actions ~= Action.failure(pkg, rdp.dependency, rdp.packages);
488 
489 			return actions;
490 		}
491 
492 		// Gather retrieved
493 		Package[string] retrieved;
494 		retrieved[m_main.name] = m_main;
495 		foreach(ref Package p; m_dependencies) {
496 			auto pbase = p.basePackage;
497 			auto pexist = retrieved.get(pbase.name, null);
498 			if (pexist && pexist !is pbase){
499 				logError("Conflicting package references found:");
500 				logError("  %s %s: %s", pexist.name, pexist.vers, pexist.path.toNativeString());
501 				logError("  %s %s: %s", pbase.name, pbase.vers, pbase.path.toNativeString());
502 				throw new Exception("Conflicting package multi-references.");
503 			}
504 			retrieved[pbase.name] = pbase;
505 		}
506 
507 		// Check against package list and add retrieval actions
508 		Action[] actions;
509 		void addAction(Action act) {
510 			if (!actions.any!(a => a.type == act.type && a.location == act.location && a.packageId == act.packageId && a.vers == act.vers))
511 				actions ~= act;
512 		}
513 		int[string] upgradePackages;
514 		foreach( string pkg, d; graph.needed() ) {
515 			auto basepkg = pkg.getBasePackage();
516 			auto p = basepkg in retrieved;
517 			// TODO: auto update to latest head revision
518 			if(!p || (!d.dependency.matches(p.vers) && !d.dependency.matches(Version.MASTER))) {
519 				if(!p) logDiagnostic("Triggering retrieval of required package '"~basepkg~"', which was not present.");
520 				else logDiagnostic("Triggering retrieval of required package '"~basepkg~"', which doesn't match the required versionh. Required '%s', available '%s'.", d.dependency, p.vers);
521 				addAction(Action.get(basepkg, PlacementLocation.userWide, d.dependency, d.packages));
522 			} else {
523 				if (option & UpdateOptions.upgrade) {
524 					auto existing = m_packageManager.getBestPackage(basepkg, d.dependency);
525 					// Only add one upgrade action for each package.
526 					if(basepkg !in upgradePackages && m_packageManager.isManagedPackage(existing)) {
527 						logDiagnostic("Required package '"~basepkg~"' found with version '"~p.vers~"', upgrading.");
528 						upgradePackages[basepkg] = 1;
529 						addAction(Action.get(basepkg, PlacementLocation.userWide, d.dependency, d.packages));
530 					}
531 				}
532 				else {
533 					logDiagnostic("Required package '"~basepkg~"' found with version '"~p.vers~"'");
534 				}
535 			}
536 		}
537 
538 		return actions;
539 	}
540 
541 	/// Outputs a JSON description of the project, including its deoendencies.
542 	void describe(ref Json dst, BuildPlatform platform, string config)
543 	{
544 		dst.mainPackage = m_main.name;
545 
546 		auto configs = getPackageConfigs(platform, config);
547 
548 		auto mp = Json.emptyObject;
549 		m_main.describe(mp, platform, config);
550 		dst.packages = Json([mp]);
551 
552 		foreach (dep; m_dependencies) {
553 			auto dp = Json.emptyObject;
554 			dep.describe(dp, platform, configs[dep.name]);
555 			dst.packages = dst.packages.get!(Json[]) ~ dp;
556 		}
557 	}
558 
559 	private bool gatherMissingDependencies(PackageSupplier[] packageSuppliers, DependencyGraph graph)
560 	{
561 		RequestedDependency[string] missing = graph.missing();
562 		RequestedDependency[string] oldMissing;
563 		while( missing.length > 0 ) {
564 			logDebug("Try to resolve %s", missing.keys);
565 			if( missing.keys == oldMissing.keys ){ // FIXME: should actually compare the complete AA here
566 				bool different = false;
567 				foreach(string pkg, reqDep; missing) {
568 					auto o = pkg in oldMissing;
569 					if(o && reqDep.dependency != o.dependency) {
570 						different = true;
571 						break;
572 					}
573 				}
574 				if(!different) {
575 					logWarn("Could not resolve dependencies");
576 					return false;
577 				}
578 			}
579 
580 			oldMissing = missing.dup;
581 			logDebug("There are %s packages missing.", missing.length);
582 
583 			auto toLookup = missing;
584 			foreach(id, dep; graph.optional()) {
585 				assert(id !in toLookup, "A missing dependency in the graph seems to be optional, which is an error.");
586 				toLookup[id] = dep;
587 			}
588 
589 			foreach(string pkg, reqDep; toLookup) {
590 				if(!reqDep.dependency.valid()) {
591 					logDebug("Dependency to "~pkg~" is invalid. Trying to fix by modifying others.");
592 					continue;
593 				}
594 					
595 				auto ppath = pkg.getSubPackagePath();
596 
597 				// TODO: auto update and update interval by time
598 				logDebug("Adding package to graph: "~pkg);
599 				Package p = m_packageManager.getBestPackage(pkg, reqDep.dependency);
600 				if( p ) logDebug("Found present package %s %s", pkg, p.ver);
601 
602 				// Don't bother with not available optional packages.
603 				if( !p && reqDep.dependency.optional ) continue;
604 				
605 				// Try an already present package first
606 				if( p && needsUpToDateCheck(p) ){
607 					logInfo("Triggering update of package %s", pkg);
608 					p = null;
609 				}
610 
611 				if( !p ) p = fetchPackageMetadata(packageSuppliers, pkg, reqDep);
612 				if( p ) graph.insert(p);
613 			}
614 			graph.clearUnused();
615 			
616 			// As the dependencies are filled in starting from the outermost 
617 			// packages, resolving those conflicts won't happen (?).
618 			if(graph.conflicted().length > 0) {
619 				logInfo("There are conflicts in the dependency graph.");
620 				return false;
621 			}
622 
623 			missing = graph.missing();
624 		}
625 
626 		return true;
627 	}
628 
629 	private Package fetchPackageMetadata(PackageSupplier[] packageSuppliers, string pkg, RequestedDependency reqDep) {
630 		Package p = null;
631 		try {
632 			logDiagnostic("Fetching package %s (%d suppliers registered)", pkg, packageSuppliers.length);
633 			auto ppath = pkg.getSubPackagePath();
634 			auto basepkg = pkg.getBasePackage();
635 			foreach (supplier; packageSuppliers) {
636 				try {
637 					// Get main package.
638 					auto sp = new Package(supplier.getPackageDescription(basepkg, reqDep.dependency, false));
639 					// Fetch subpackage, if one was requested.
640 					foreach (spn; ppath[1 .. $]) {
641 						try {
642 							// Some subpackages are shipped with the main package.
643 							sp = sp.getSubPackage(spn);
644 						} catch (Exception e) {
645 							// HACK: Support for external packages. Until the registry serves the
646 							//   metadata of the external packages, there is no way to get to
647 							//   know this information.
648 							//
649 							//   Eventually, the dependencies of all referenced packages will be
650 							//   fulfilled, but this is done by a hack where the metadata of the
651 							//   external package is inferred as having no additional dependencies.
652 							//   When the package is then fetched the state is re-evaluated and
653 							//   possible new dependencies will be resolved.
654 							string hacked_info = "{\"name\": \"" ~ spn ~ "\"}";
655 							auto info = parseJson(hacked_info);
656 							sp = new Package(info, Path(), sp);
657 						}
658 					}
659 					p = sp;
660 					break;
661 				} catch (Exception e) {
662 					logDiagnostic("No metadata for %s: %s", supplier.description, e.msg);
663 				}
664 			}
665 			enforce(p !is null, "Could not find package candidate for "~pkg~" "~reqDep.dependency.toString());
666 			markUpToDate(basepkg);
667 		}
668 		catch(Throwable e) {
669 			logError("Failed to retrieve metadata for package %s: %s", pkg, e.msg);
670 			logDiagnostic("Full error: %s", e.toString().sanitize());
671 		}
672 
673 		return p;
674 	}
675 
676 	private bool needsUpToDateCheck(Package pack) {
677 		version (none) { // needs to be updated for the new package system (where no project local packages exist)
678 			try {
679 				auto time = m_json["dub"]["lastUpdate"].opt!(Json[string]).get(pack.name, Json("")).get!string;
680 				if( !time.length ) return true;
681 				return (Clock.currTime() - SysTime.fromISOExtString(time)) > dur!"days"(1);
682 			} catch(Exception t) return true;
683 		} else return false;
684 	}
685 		
686 	private void markUpToDate(string packageId) {
687 		logDebug("markUpToDate(%s)", packageId);
688 		Json create(ref Json json, string object) {
689 			if( object !in json ) json[object] = Json.emptyObject;
690 			return json[object];
691 		}
692 		create(m_json, "dub");
693 		create(m_json["dub"], "lastUpdate");
694 		m_json["dub"]["lastUpdate"][packageId] = Json( Clock.currTime().toISOExtString() );
695 
696 		writeDubJson();
697 	}
698 
699 	private void writeDubJson() {
700 		// don't bother to write an empty file
701 		if( m_json.length == 0 ) return;
702 
703 		try {
704 			logDebug("writeDubJson");
705 			auto dubpath = m_root~".dub";
706 			if( !exists(dubpath.toNativeString()) ) mkdir(dubpath.toNativeString());
707 			auto dstFile = openFile((dubpath~"dub.json").toString(), FileMode.CreateTrunc);
708 			scope(exit) dstFile.close();
709 			dstFile.writePrettyJsonString(m_json);
710 		} catch( Exception e ){
711 			logWarn("Could not write .dub/dub.json.");
712 		}
713 	}
714 }
715 
716 /// Actions to be performed by the dub
717 struct Action {
718 	enum Type {
719 		fetch,
720 		remove,
721 		conflict,
722 		failure
723 	}
724 
725 	immutable {
726 		Type type;
727 		string packageId;
728 		PlacementLocation location;
729 		Dependency vers;
730 	}
731 	const Package pack;
732 	const Dependency[string] issuer;
733 
734 	static Action get(string pkg, PlacementLocation location, in Dependency dep, Dependency[string] context)
735 	{
736 		return Action(Type.fetch, pkg, location, dep, context);
737 	}
738 
739 	static Action remove(Package pkg, Dependency[string] context)
740 	{
741 		return Action(Type.remove, pkg, context);
742 	}
743 
744 	static Action conflict(string pkg, in Dependency dep, Dependency[string] context)
745 	{
746 		return Action(Type.conflict, pkg, PlacementLocation.userWide, dep, context);
747 	}
748 
749 	static Action failure(string pkg, in Dependency dep, Dependency[string] context)
750 	{
751 		return Action(Type.failure, pkg, PlacementLocation.userWide, dep, context);
752 	}
753 
754 	private this(Type id, string pkg, PlacementLocation location, in Dependency d, Dependency[string] issue)
755 	{
756 		this.type = id;
757 		this.packageId = pkg;
758 		this.location = location;
759 		this.vers = d;
760 		this.issuer = issue;
761 	}
762 
763 	private this(Type id, Package pkg, Dependency[string] issue)
764 	{
765 		pack = pkg;
766 		type = id;
767 		packageId = pkg.name;
768 		vers = cast(immutable)Dependency(pkg.ver);
769 		issuer = issue;
770 	}
771 
772 	string toString() const {
773 		return to!string(type) ~ ": " ~ packageId ~ ", " ~ to!string(vers);
774 	}
775 }
776 
777 
778 enum UpdateOptions
779 {
780 	none = 0,
781 	upgrade = 1<<1,
782 	preRelease = 1<<2 // inclde pre-release versions in upgrade
783 }
784 
785 
786 /// Indicates where a package has been or should be placed to.
787 enum PlacementLocation {
788 	/// Packages retrived with 'local' will be placed in the current folder 
789 	/// using the package name as destination.
790 	local,
791 	/// Packages with 'userWide' will be placed in a folder accessible by
792 	/// all of the applications from the current user.
793 	userWide,
794 	/// Packages retrieved with 'systemWide' will be placed in a shared folder,
795 	/// which can be accessed by all users of the system.
796 	systemWide
797 }
798 
799 void processVars(ref BuildSettings dst, string project_path, BuildSettings settings, bool include_target_settings = false)
800 {
801 	dst.addDFlags(processVars(project_path, settings.dflags));
802 	dst.addLFlags(processVars(project_path, settings.lflags));
803 	dst.addLibs(processVars(project_path, settings.libs));
804 	dst.addSourceFiles(processVars(project_path, settings.sourceFiles, true));
805 	dst.addImportFiles(processVars(project_path, settings.importFiles, true));
806 	dst.addStringImportFiles(processVars(project_path, settings.stringImportFiles, true));
807 	dst.addCopyFiles(processVars(project_path, settings.copyFiles, true));
808 	dst.addVersions(processVars(project_path, settings.versions));
809 	dst.addDebugVersions(processVars(project_path, settings.debugVersions));
810 	dst.addImportPaths(processVars(project_path, settings.importPaths, true));
811 	dst.addStringImportPaths(processVars(project_path, settings.stringImportPaths, true));
812 	dst.addPreGenerateCommands(processVars(project_path, settings.preGenerateCommands));
813 	dst.addPostGenerateCommands(processVars(project_path, settings.postGenerateCommands));
814 	dst.addPreBuildCommands(processVars(project_path, settings.preBuildCommands));
815 	dst.addPostBuildCommands(processVars(project_path, settings.postBuildCommands));
816 	dst.addRequirements(settings.requirements);
817 	dst.addOptions(settings.options);
818 
819 	if (include_target_settings) {
820 		dst.targetType = settings.targetType;
821 		dst.targetPath = processVars(settings.targetPath, project_path, true);
822 		dst.targetName = settings.targetName;
823 		dst.workingDirectory = processVars(settings.workingDirectory, project_path, true);
824 		if (settings.mainSourceFile.length)
825 			dst.mainSourceFile = processVars(settings.mainSourceFile, project_path, true);
826 	}
827 }
828 
829 private string[] processVars(string project_path, string[] vars, bool are_paths = false)
830 {
831 	auto ret = appender!(string[])();
832 	processVars(ret, project_path, vars, are_paths);
833 	return ret.data;
834 
835 }
836 private void processVars(ref Appender!(string[]) dst, string project_path, string[] vars, bool are_paths = false)
837 {
838 	foreach (var; vars) dst.put(processVars(var, project_path, are_paths));
839 }
840 
841 private string processVars(string var, string project_path, bool is_path)
842 {
843 	auto idx = std..string.indexOf(var, '$');
844 	if (idx >= 0) {
845 		auto vres = appender!string();
846 		while (idx >= 0) {
847 			if (idx+1 >= var.length) break;
848 			if (var[idx+1] == '$') {
849 				vres.put(var[0 .. idx+1]);
850 				var = var[idx+2 .. $];
851 			} else {
852 				vres.put(var[0 .. idx]);
853 				var = var[idx+1 .. $];
854 
855 				size_t idx2 = 0;
856 				while( idx2 < var.length && isIdentChar(var[idx2]) ) idx2++;
857 				auto varname = var[0 .. idx2];
858 				var = var[idx2 .. $];
859 
860 				string env_variable;
861 				if( varname == "PACKAGE_DIR" ) vres.put(project_path);
862 				else if( (env_variable = environment.get(varname)) != null) vres.put(env_variable);
863 				else enforce(false, "Invalid variable: "~varname);
864 			}
865 			idx = std..string.indexOf(var, '$');
866 		}
867 		vres.put(var);
868 		var = vres.data;
869 	}
870 	if (is_path) {
871 		auto p = Path(var);
872 		if (!p.absolute) {
873 			logDebug("Fixing relative path: %s ~ %s", project_path, p.toNativeString());
874 			p = Path(project_path) ~ p;
875 		}
876 		return p.toNativeString();
877 	} else return var;
878 }
879 
880 private bool isIdentChar(dchar ch)
881 {
882 	return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '_';
883 }
884 
885 string stripDlangSpecialChars(string s) 
886 {
887 	import std.array;
888 	import std.uni;
889 	auto ret = appender!string();
890 	foreach(ch; s)
891 		ret.put(isIdentChar(ch) ? ch : '_');
892 	return ret.data;
893 }