1 /**
2  * Code to manage a D checkout and its dependencies.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.sys.d.manager;
15 
16 import std.algorithm;
17 import std.array;
18 import std.conv;
19 import std.datetime;
20 import std.exception;
21 import std.file;
22 import std.path;
23 import std.process : spawnProcess, wait, escapeShellCommand;
24 import std.range;
25 import std.regex;
26 import std.string;
27 import std.typecons;
28 
29 import ae.sys.d.cache;
30 import ae.sys.d.repo;
31 import ae.sys.file;
32 import ae.sys.git;
33 import ae.utils.aa;
34 import ae.utils.array;
35 import ae.utils.digest;
36 import ae.utils.json;
37 import ae.utils.meta;
38 import ae.utils.regex;
39 
40 alias ensureDirExists = ae.sys.file.ensureDirExists;
41 
42 version (Windows)
43 {
44 	import ae.sys.install.dmc;
45 	import ae.sys.install.msys;
46 	import ae.sys.install.vs;
47 
48 	extern(Windows) void SetErrorMode(int);
49 }
50 
51 import ae.sys.install.dmd;
52 import ae.sys.install.git;
53 import ae.sys.install.kindlegen;
54 
55 static import std.process;
56 
57 /// Class which manages a D checkout and its dependencies.
58 class DManager : ICacheHost
59 {
60 	// **************************** Configuration ****************************
61 
62 	struct Config /// DManager configuration.
63 	{
64 		struct Build /// Build configuration
65 		{
66 			struct Components
67 			{
68 				bool[string] enable;
69 
70 				string[] getEnabledComponentNames()
71 				{
72 					foreach (componentName; enable.byKey)
73 						enforce(allComponents.canFind(componentName), "Unknown component: " ~ componentName);
74 					return allComponents
75 						.filter!(componentName =>
76 							enable.get(componentName, defaultComponents.canFind(componentName)))
77 						.array
78 						.dup;
79 				}
80 
81 				Component.CommonConfig common;
82 				DMD.Config dmd;
83 				Website.Config website;
84 			}
85 			Components components;
86 
87 			/// Additional environment variables.
88 			/// Supports %VAR% expansion - see applyEnv.
89 			string[string] environment;
90 
91 			/// Optional cache key.
92 			/// Can be used to force a rebuild and bypass the cache for one build.
93 			string cacheKey;
94 		}
95 		Build build; /// ditto
96 
97 		/// Machine-local configuration
98 		/// These settings should not affect the build output.
99 		struct Local
100 		{
101 			/// URL of D git repository hosting D components.
102 			/// Defaults to (and must have the layout of) D.git:
103 			/// https://github.com/CyberShadow/D-dot-git
104 			string repoUrl = "https://bitbucket.org/cybershadow/d.git";
105 
106 			/// Location for the checkout, temporary files, etc.
107 			string workDir;
108 
109 			/// If present, passed to GNU make via -j parameter.
110 			/// Can also be "auto" or "unlimited".
111 			string makeJobs;
112 
113 			/// Don't get latest updates from GitHub.
114 			bool offline;
115 
116 			/// How to cache built files.
117 			string cache;
118 
119 			/// Maximum execution time, in seconds, of any single
120 			/// command.
121 			int timeout;
122 		}
123 		Local local; /// ditto
124 	}
125 	Config config; /// ditto
126 
127 	// Behavior options that generally depend on the host program.
128 
129 	/// Automatically re-clone the repository in case
130 	/// "git reset --hard" fails.
131 	bool autoClean;
132 
133 	/// Whether to verify working tree state
134 	/// to make sure we don't clobber user changes
135 	bool verifyWorkTree;
136 
137 	/// Whether we should cache failed builds.
138 	bool cacheFailures = true;
139 
140 	/// Current build environment.
141 	struct Environment
142 	{
143 		struct Deps /// Configuration for software dependencies
144 		{
145 			string dmcDir;   /// Where dmc.zip is unpacked.
146 			string vsDir;    /// Where Visual Studio is installed
147 			string sdkDir;   /// Where the Windows SDK is installed
148 			string hostDC;   /// Host D compiler (for DDMD bootstrapping)
149 		}
150 		Deps deps; /// ditto
151 
152 		/// Calculated local environment, incl. dependencies
153 		string[string] vars;
154 	}
155 
156 	/// Get a specific subdirectory of the work directory.
157 	@property string subDir(string name)() { return buildPath(config.local.workDir, name); }
158 
159 	alias repoDir    = subDir!"repo";        /// The git repository location.
160 	alias buildDir   = subDir!"build";       /// The build directory.
161 	alias dlDir      = subDir!"dl";          /// The directory for downloaded software.
162 	alias tmpDir     = subDir!"tmp";         /// Directory for $TMPDIR etc.
163 	alias homeDir    = subDir!"home";        /// Directory for $HOME.
164 	alias binDir     = subDir!"bin" ;        /// For wrapper scripts.
165 
166 	/// This number increases with each incompatible change to cached data.
167 	enum cacheVersion = 3;
168 
169 	string cacheEngineDir(string engineName)
170 	{
171 		// Keep compatibility with old cache paths
172 		string engineDirName =
173 			engineName.isOneOf("directory", "true") ? "cache"      :
174 			engineName.isOneOf("", "none", "false") ? "temp-cache" :
175 			"cache-" ~ engineName;
176 		return buildPath(
177 			config.local.workDir,
178 			engineDirName,
179 			"v%d".format(cacheVersion),
180 		);
181 	}
182 
183 	version (Windows)
184 	{
185 		enum string binExt = ".exe";
186 		enum configFileName = "sc.ini";
187 	}
188 	else
189 	{
190 		enum string binExt = "";
191 		enum configFileName = "dmd.conf";
192 	}
193 
194 	static bool needConfSwitch() { return exists(std.process.environment.get("HOME", null).buildPath(configFileName)); }
195 
196 	// **************************** Repositories *****************************
197 
198 	class DManagerRepository : ManagedRepository
199 	{
200 		this()
201 		{
202 			this.offline = config.local.offline;
203 			this.verify = this.outer.verifyWorkTree;
204 		}
205 
206 		override void log(string s) { return this.outer.log(s); }
207 	}
208 
209 	class MetaRepository : DManagerRepository
210 	{
211 		override void needRepo()
212 		{
213 			needGit();
214 
215 			if (!repoDir.exists)
216 			{
217 				log("Cloning initial repository...");
218 				atomic!performClone(config.local.repoUrl, repoDir);
219 			}
220 
221 			if (!git.path)
222 				git = Repository(repoDir);
223 		}
224 
225 		static void performClone(string url, string target)
226 		{
227 			import ae.sys.cmd;
228 			run(["git", "clone", url, target]);
229 		}
230 
231 		override void performCheckout(string hash)
232 		{
233 			super.performCheckout(hash);
234 			submodules = null;
235 		}
236 
237 		string[string][string] submoduleCache;
238 
239 		string[string] getSubmoduleCommits(string head)
240 		{
241 			auto pcacheEntry = head in submoduleCache;
242 			if (pcacheEntry)
243 				return (*pcacheEntry).dup;
244 
245 			string[string] result;
246 			needRepo();
247 			foreach (line; git.query("ls-tree", head).splitLines())
248 			{
249 				auto parts = line.split();
250 				if (parts.length == 4 && parts[1] == "commit")
251 					result[parts[3]] = parts[2];
252 			}
253 			assert(result.length, "No submodules found");
254 			submoduleCache[head] = result;
255 			return result.dup;
256 		}
257 
258 		/// Get the submodule state for all commits in the history.
259 		/// Returns: result[commitHash][submoduleName] == submoduleCommitHash
260 		string[string][string] getSubmoduleHistory(string[] refs)
261 		{
262 			auto marksFile = buildPath(config.local.workDir, "temp", "marks.txt");
263 			ensurePathExists(marksFile);
264 			scope(exit) if (marksFile.exists) marksFile.remove();
265 			log("Running fast-export...");
266 			auto fastExportData = git.query([
267 				"fast-export",
268 				"--full-tree",
269 				"--no-data",
270 				"--export-marks=" ~ marksFile.absolutePath,
271 				] ~ refs
272 			);
273 
274 			log("Parsing fast-export marks...");
275 
276 			auto markLines = marksFile.readText.strip.splitLines;
277 			auto marks = new string[markLines.length];
278 			foreach (line; markLines)
279 			{
280 				auto parts = line.split(' ');
281 				auto markIndex = parts[0][1..$].to!int-1;
282 				marks[markIndex] = parts[1];
283 			}
284 
285 			log("Parsing fast-export data...");
286 
287 			string[string][string] result;
288 			foreach (i, commitData; fastExportData.split("deleteall\n")[1..$])
289 				result[marks[i]] = commitData
290 					.matchAll(re!(`^M 160000 ([0-9a-f]{40}) (\S+)$`, "m"))
291 					.map!(m => tuple(m.captures[2], m.captures[1]))
292 					.assocArray
293 				;
294 			return result;
295 		}
296 	}
297 
298 	class SubmoduleRepository : DManagerRepository
299 	{
300 		string dir;
301 
302 		override void needRepo()
303 		{
304 			getMetaRepo().needRepo();
305 
306 			if (!git.path)
307 				git = Repository(dir);
308 		}
309 
310 		override void needHead(string hash)
311 		{
312 			if (!autoClean)
313 				super.needHead(hash);
314 			else
315 			try
316 				super.needHead(hash);
317 			catch (RepositoryCleanException e)
318 			{
319 				log("Error during repository cleanup.");
320 
321 				log("Nuking %s...".format(dir));
322 				rmdirRecurse(dir);
323 
324 				auto name = baseName(dir);
325 				auto gitDir = buildPath(dirName(dir), ".git", "modules", name);
326 				log("Nuking %s...".format(gitDir));
327 				rmdirRecurse(gitDir);
328 
329 				log("Updating submodule...");
330 				getMetaRepo().git.run(["submodule", "update", name]);
331 
332 				reset();
333 
334 				log("Trying again...");
335 				super.needHead(hash);
336 			}
337 		}
338 	}
339 
340 	/// The meta-repository, which contains the sub-project submodules.
341 	private MetaRepository metaRepo;
342 
343 	MetaRepository getMetaRepo() /// ditto
344 	{
345 		if (!metaRepo)
346 			metaRepo = new MetaRepository;
347 		return metaRepo;
348 	}
349 
350 	/// Sub-project repositories.
351 	private SubmoduleRepository[string] submodules;
352 
353 	ManagedRepository getSubmodule(string name) /// ditto
354 	{
355 		assert(name, "This component is not associated with a submodule");
356 		if (name !in submodules)
357 		{
358 			getMetaRepo().needRepo();
359 			enforce(name in getMetaRepo().getSubmoduleCommits(getMetaRepo().getRef("origin/master")),
360 				"Unknown submodule: " ~ name);
361 
362 			auto path = buildPath(metaRepo.git.path, name);
363 			auto gitPath = buildPath(path, ".git");
364 
365 			if (!gitPath.exists)
366 			{
367 				log("Initializing and updating submodule %s...".format(name));
368 				getMetaRepo().git.run(["submodule", "update", "--init", name]);
369 			}
370 
371 			submodules[name] = new SubmoduleRepository();
372 			submodules[name].dir = path;
373 		}
374 
375 		return submodules[name];
376 	}
377 
378 	// ***************************** Components ******************************
379 
380 	/// Base class for a D component.
381 	class Component
382 	{
383 		/// Name of this component, as registered in DManager.components AA.
384 		string name;
385 
386 		/// Corresponding subproject repository name.
387 		@property abstract string submoduleName();
388 		@property ManagedRepository submodule() { return getSubmodule(submoduleName); }
389 
390 		/// Configuration applicable to multiple (not all) components.
391 		// Note: don't serialize this structure whole!
392 		// Only serialize used fields.
393 		struct CommonConfig
394 		{
395 			version (Windows)
396 				enum defaultModel = "32";
397 			else
398 			version (D_LP64)
399 				enum defaultModel = "64";
400 			else
401 				enum defaultModel = "32";
402 
403 			/// Target comma-separated models ("32", "64", and on Windows, "32mscoff").
404 			/// Controls the models of the built Phobos and Druntime libraries.
405 			string model = defaultModel;
406 
407 			@property string[] models() { return model.split(","); }
408 			@property void models(string[] value) { this.model = value.join(","); }
409 
410 			string[] makeArgs; /// Additional make parameters,
411 			                   /// e.g. "HOST_CC=g++48"
412 		}
413 
414 		/// A string description of this component's configuration.
415 		abstract @property string configString();
416 
417 		/// Commit in the component's repo from which to build this component.
418 		@property string commit() { return incrementalBuild ? "incremental" : getComponentCommit(name); }
419 
420 		/// The components the source code of which this component depends on.
421 		/// Used for calculating the cache key.
422 		@property abstract string[] sourceDependencies();
423 
424 		/// The components the state and configuration of which this component depends on.
425 		/// Used for calculating the cache key.
426 		@property abstract string[] dependencies();
427 
428 		/// This metadata is saved to a .json file,
429 		/// and is also used to calculate the cache key.
430 		struct Metadata
431 		{
432 			int cacheVersion;
433 			string name;
434 			string commit;
435 			string configString;
436 			string[] sourceDepCommits;
437 			Metadata[] dependencyMetadata;
438 			@JSONOptional string cacheKey;
439 		}
440 
441 		Metadata getMetadata() /// ditto
442 		{
443 			return Metadata(
444 				cacheVersion,
445 				name,
446 				commit,
447 				configString,
448 				sourceDependencies.map!(
449 					dependency => getComponent(dependency).commit
450 				).array(),
451 				dependencies.map!(
452 					dependency => getComponent(dependency).getMetadata()
453 				).array(),
454 				config.build.cacheKey,
455 			);
456 		}
457 
458 		void saveMetaData(string target)
459 		{
460 			std.file.write(buildPath(target, "digger-metadata.json"), getMetadata().toJson());
461 			// Use a separate file to avoid double-encoding JSON
462 			std.file.write(buildPath(target, "digger-config.json"), configString);
463 		}
464 
465 		/// Calculates the cache key, which should be unique and immutable
466 		/// for the same source, build parameters, and build algorithm.
467 		string getBuildID()
468 		{
469 			auto configBlob = getMetadata().toJson() ~ configString;
470 			return "%s-%s-%s".format(
471 				name,
472 				commit,
473 				configBlob.getDigestString!MD5().toLower(),
474 			);
475 		}
476 
477 		@property string sourceDir() { submodule.needRepo(); return submodule.git.path; }
478 
479 		/// Directory to which built files are copied to.
480 		/// This will then be atomically added to the cache.
481 		protected string stageDir;
482 
483 		/// Prepare the source checkout for this component.
484 		/// Usually needed by other components.
485 		void needSource()
486 		{
487 			tempError++; scope(success) tempError--;
488 
489 			if (incrementalBuild)
490 				return;
491 			if (!submoduleName)
492 				return;
493 			foreach (component; getSubmoduleComponents(submoduleName))
494 				component.haveBuild = false;
495 
496 			submodule.needHead(commit);
497 			submodule.clean = false;
498 		}
499 
500 		private bool haveBuild;
501 
502 		/// Build the component in-place, as needed,
503 		/// without moving the built files anywhere.
504 		void needBuild()
505 		{
506 			if (haveBuild) return;
507 			scope(success) haveBuild = true;
508 
509 			log("needBuild: " ~ getBuildID());
510 
511 			needSource();
512 
513 			prepareEnv();
514 
515 			log("Building " ~ getBuildID());
516 			performBuild();
517 			log(getBuildID() ~ " built OK!");
518 		}
519 
520 		/// Set up / clean the build environment.
521 		private void prepareEnv()
522 		{
523 			// Nuke any additional directories cloned by makefiles
524 			if (!incrementalBuild)
525 			{
526 				getMetaRepo().git.run(["clean", "-ffdx"]);
527 
528 				foreach (dir; [tmpDir, homeDir])
529 				{
530 					if (dir.exists && !dir.dirEntries(SpanMode.shallow).empty)
531 						log("Clearing %s ...".format(dir));
532 					dir.recreateEmptyDirectory();
533 				}
534 			}
535 
536 			// Set up compiler wrappers.
537 			recreateEmptyDirectory(binDir);
538 			version (linux)
539 			{
540 				foreach (cc; ["cc", "gcc", "c++", "g++"])
541 				{
542 					auto fileName = binDir.buildPath(cc);
543 					write(fileName, q"EOF
544 #!/bin/sh
545 set -eu
546 
547 tool=$(basename "$0")
548 next=/usr/bin/$tool
549 flagfile=$TMP/nopie-flag-$tool
550 
551 if [ ! -e "$flagfile" ]
552 then
553 	echo 'Testing for -no-pie...' 1>&2
554 	testfile=$TMP/test-$$.c
555 	echo 'int main(){return 0;}' > $testfile
556 	if $next -no-pie -c -o$testfile.o $testfile
557 	then
558 		printf "%s" "-no-pie" > "$flagfile".$$.tmp
559 		mv "$flagfile".$$.tmp "$flagfile"
560 	else
561 		touch "$flagfile"
562 	fi
563 	rm -f "$testfile" "$testfile.o"
564 fi
565 
566 exec "$next" $(cat "$flagfile") "$@"
567 EOF");
568 					setAttributes(fileName, octal!755);
569 				}
570 			}
571 		}
572 
573 		private bool haveInstalled;
574 
575 		/// Build and "install" the component to buildDir as necessary.
576 		void needInstalled()
577 		{
578 			if (haveInstalled) return;
579 			scope(success) haveInstalled = true;
580 
581 			auto buildID = getBuildID();
582 			log("needInstalled: " ~ buildID);
583 
584 			needCacheEngine();
585 			if (cacheEngine.haveEntry(buildID))
586 			{
587 				log("Cache hit!");
588 				if (cacheEngine.listFiles(buildID).canFind(unbuildableMarker))
589 					throw new Exception(buildID ~ " was cached as unbuildable");
590 			}
591 			else
592 			{
593 				log("Cache miss.");
594 
595 				auto tempDir = buildPath(config.local.workDir, "temp");
596 				if (tempDir.exists)
597 					tempDir.removeRecurse();
598 				stageDir = buildPath(tempDir, buildID);
599 				stageDir.mkdirRecurse();
600 
601 				bool failed = false;
602 				tempError = 0;
603 
604 				// Save the results to cache, failed or not
605 				void saveToCache()
606 				{
607 					// Use a separate function to work around
608 					// "cannot put scope(success) statement inside scope(exit)"
609 
610 					int currentTempError = tempError;
611 
612 					// Treat cache errors an environmental errors
613 					// (for when needInstalled is invoked to build a dependency)
614 					tempError++; scope(success) tempError--;
615 
616 					// tempDir might be removed by a dependency's build failure.
617 					if (!tempDir.exists)
618 						log("Not caching %s dependency build failure.".format(name));
619 					else
620 					// Don't cache failed build results due to temporary/environment problems
621 					if (failed && currentTempError > 0)
622 					{
623 						log("Not caching %s build failure due to temporary/environment error.".format(name));
624 						rmdirRecurse(tempDir);
625 					}
626 					else
627 					// Don't cache failed build results during delve
628 					if (failed && !cacheFailures)
629 					{
630 						log("Not caching failed %s build.".format(name));
631 						rmdirRecurse(tempDir);
632 					}
633 					else
634 					if (cacheEngine.haveEntry(buildID))
635 					{
636 						// Can happen due to force==true
637 						log("Already in cache.");
638 						rmdirRecurse(tempDir);
639 					}
640 					else
641 					{
642 						log("Saving to cache.");
643 						saveMetaData(stageDir);
644 						cacheEngine.add(buildID, stageDir);
645 						rmdirRecurse(tempDir);
646 					}
647 				}
648 
649 				scope (exit)
650 					saveToCache();
651 
652 				// An incomplete build is useless, nuke the directory
653 				// and create a new one just for the "unbuildable" marker.
654 				scope (failure)
655 				{
656 					failed = true;
657 					if (stageDir.exists)
658 					{
659 						rmdirRecurse(stageDir);
660 						mkdir(stageDir);
661 						buildPath(stageDir, unbuildableMarker).touch();
662 					}
663 				}
664 
665 				needBuild();
666 
667 				performStage();
668 			}
669 
670 			install();
671 		}
672 
673 		/// Build the component in-place, without moving the built files anywhere.
674 		void performBuild() {}
675 
676 		/// Place resulting files to stageDir
677 		void performStage() {}
678 
679 		/// Update the environment post-install, to allow
680 		/// building components that depend on this one.
681 		void updateEnv(ref Environment env) {}
682 
683 		/// Copy build results from cacheDir to buildDir
684 		void install()
685 		{
686 			log("Installing " ~ getBuildID());
687 			needCacheEngine().extract(getBuildID(), buildDir, de => !de.baseName.startsWith("digger-"));
688 		}
689 
690 		/// Prepare the dependencies then run the component's tests.
691 		void test()
692 		{
693 			log("Testing " ~ getBuildID());
694 
695 			needSource();
696 
697 			submodule.clean = false;
698 			performTest();
699 			log(getBuildID() ~ " tests OK!");
700 		}
701 
702 		/// Run the component's tests.
703 		void performTest() {}
704 
705 	protected final:
706 		// Utility declarations for component implementations
707 
708 		string modelSuffix(string model) { return model == "32" ? "" : model; }
709 		version (Windows)
710 		{
711 			enum string makeFileName = "win32.mak";
712 			string makeFileNameModel(string model)
713 			{
714 				if (model == "32mscoff")
715 					model = "64";
716 				return "win"~model~".mak";
717 			}
718 			enum string binExt = ".exe";
719 		}
720 		else
721 		{
722 			enum string makeFileName = "posix.mak";
723 			string makeFileNameModel(string model) { return "posix.mak"; }
724 			enum string binExt = "";
725 		}
726 
727 		version (Windows)
728 			enum platform = "windows";
729 		else
730 		version (linux)
731 			enum platform = "linux";
732 		else
733 		version (OSX)
734 			enum platform = "osx";
735 		else
736 		version (FreeBSD)
737 			enum platform = "freebsd";
738 		else
739 			static assert(false);
740 
741 		/// Returns the command for the make utility.
742 		string[] getMake(in ref Environment env)
743 		{
744 			return [env.vars.get("MAKE", "make")];
745 		}
746 
747 		/// Returns the path to the built dmd executable.
748 		@property string dmd() { return buildPath(buildDir, "bin", "dmd" ~ binExt).absolutePath(); }
749 
750 		/// Escape a path for d_do_test's very "special" criteria.
751 		/// Spaces must be escaped, but there must be no double-quote at the end.
752 		private static string dDoTestEscape(string str)
753 		{
754 			return str.replaceAll(re!`\\([^\\ ]*? [^\\]*)(?=\\)`, `\"$1"`);
755 		}
756 
757 		unittest
758 		{
759 			assert(dDoTestEscape(`C:\Foo boo bar\baz quuz\derp.exe`) == `C:\"Foo boo bar"\"baz quuz"\derp.exe`);
760 		}
761 
762 		string[] getPlatformMakeVars(in ref Environment env, string model, bool quote = true)
763 		{
764 			string[] args;
765 
766 			args ~= "MODEL=" ~ model;
767 
768 			version (Windows)
769 				if (model != "32")
770 				{
771 					args ~= "VCDIR="  ~ env.deps.vsDir.buildPath("VC").absolutePath();
772 					args ~= "SDKDIR=" ~ env.deps.sdkDir.absolutePath();
773 
774 					// Work around https://github.com/dlang/druntime/pull/2438
775 					auto quoteStr = quote ? `"` : ``;
776 					args ~= "CC=" ~ quoteStr ~ env.deps.vsDir.buildPath("VC", "bin", msvcModelDir(model), "cl.exe").absolutePath() ~ quoteStr;
777 					args ~= "LD=" ~ quoteStr ~ env.deps.vsDir.buildPath("VC", "bin", msvcModelDir(model), "link.exe").absolutePath() ~ quoteStr;
778 					args ~= "AR=" ~ quoteStr ~ env.deps.vsDir.buildPath("VC", "bin", msvcModelDir(model), "lib.exe").absolutePath() ~ quoteStr;
779 				}
780 
781 			return args;
782 		}
783 
784 		@property string[] gnuMakeArgs()
785 		{
786 			string[] args;
787 			if (config.local.makeJobs)
788 			{
789 				if (config.local.makeJobs == "auto")
790 				{
791 					import std.parallelism, std.conv;
792 					args ~= "-j" ~ text(totalCPUs);
793 				}
794 				else
795 				if (config.local.makeJobs == "unlimited")
796 					args ~= "-j";
797 				else
798 					args ~= "-j" ~ config.local.makeJobs;
799 			}
800 			return args;
801 		}
802 
803 		@property string[] dMakeArgs()
804 		{
805 			version (Windows)
806 				return null; // On Windows, DigitalMars make is used for all makefiles except the dmd test suite
807 			else
808 				return gnuMakeArgs;
809 		}
810 
811 		/// Older versions did not use the posix.mak/win32.mak convention.
812 		static string findMakeFile(string dir, string fn)
813 		{
814 			version (OSX)
815 				if (!dir.buildPath(fn).exists && dir.buildPath("osx.mak").exists)
816 					return "osx.mak";
817 			version (Posix)
818 				if (!dir.buildPath(fn).exists && dir.buildPath("linux.mak").exists)
819 					return "linux.mak";
820 			return fn;
821 		}
822 
823 		void needCC(ref Environment env, string model, string dmcVer = null)
824 		{
825 			version (Windows)
826 			{
827 				needDMC(env, dmcVer); // We need DMC even for 64-bit builds (for DM make)
828 				if (model != "32")
829 					needVC(env, model);
830 			}
831 		}
832 
833 		void run(const(string)[] args, in string[string] newEnv, string dir)
834 		{
835 			// Apply user environment
836 			auto env = applyEnv(newEnv, config.build.environment);
837 
838 			// Temporarily apply PATH from newEnv to our process,
839 			// so process creation lookup can use it.
840 			string oldPath = std.process.environment["PATH"];
841 			scope (exit) std.process.environment["PATH"] = oldPath;
842 			std.process.environment["PATH"] = env["PATH"];
843 
844 			// Apply timeout setting
845 			if (config.local.timeout)
846 				args = ["timeout", config.local.timeout.text] ~ args;
847 
848 			foreach (name, value; env)
849 				log("Environment: " ~ name ~ "=" ~ value);
850 			log("Working directory: " ~ dir);
851 			log("Running: " ~ escapeShellCommand(args));
852 
853 			auto status = spawnProcess(args, env, std.process.Config.newEnv, dir).wait();
854 			enforce(status == 0, "Command %s failed with status %d".format(args, status));
855 		}
856 	}
857 
858 	/// The dmd executable
859 	final class DMD : Component
860 	{
861 		@property override string submoduleName  () { return "dmd"; }
862 		@property override string[] sourceDependencies() { return []; }
863 		@property override string[] dependencies() { return []; }
864 
865 		struct Config
866 		{
867 			/// Whether to build a debug DMD.
868 			/// Debug builds are faster to build,
869 			/// but run slower.
870 			@JSONOptional bool debugDMD = false;
871 
872 			/// Whether to build a release DMD.
873 			/// Mutually exclusive with debugDMD.
874 			@JSONOptional bool releaseDMD = false;
875 
876 			/// Model for building DMD itself (on Windows).
877 			/// Can be used to build a 64-bit DMD, to avoid 4GB limit.
878 			@JSONOptional string dmdModel = CommonConfig.defaultModel;
879 
880 			/// How to build DMD versions written in D.
881 			/// We can either download a pre-built binary DMD
882 			/// package, or build an  earlier version from source
883 			/// (e.g. starting with the last C++-only version.)
884 			struct Bootstrap
885 			{
886 				/// Whether to download a pre-built D version,
887 				/// or build one from source. If set, then build
888 				/// from source according to the value of ver,
889 				@JSONOptional bool fromSource = false;
890 
891 				/// Version specification.
892 				/// When building from source, syntax can be defined
893 				/// by outer application (see parseSpec method);
894 				/// When the bootstrapping compiler is not built from source,
895 				/// it is understood as a version number, such as "v2.070.2",
896 				/// which also doubles as a tag name.
897 				/// By default (when set to null), an appropriate version
898 				/// is selected automatically.
899 				@JSONOptional string ver = null;
900 
901 				/// Build configuration for the compiler used for bootstrapping.
902 				/// If not set, then use the default build configuration.
903 				/// Used when fromSource is set.
904 				@JSONOptional DManager.Config.Build* build;
905 			}
906 			@JSONOptional Bootstrap bootstrap; /// ditto
907 
908 			/// Use Visual C++ to build DMD instead of DMC.
909 			/// Currently, this is a hack, as msbuild will consult the system
910 			/// registry and use the system-wide installation of Visual Studio.
911 			/// Only relevant for older versions, as newer versions are written in D.
912 			@JSONOptional bool useVC;
913 		}
914 
915 		@property override string configString()
916 		{
917 			static struct FullConfig
918 			{
919 				Config config;
920 				string[] makeArgs;
921 
922 				// Include the common models as well as the DMD model (from config).
923 				// Necessary to ensure the correct sc.ini is generated on Windows
924 				// (we don't want to pull in MSVC unless either DMD or Phobos are
925 				// built as 64-bit, but also we can't reuse a DMD build with 32-bit
926 				// DMD and Phobos for a 64-bit Phobos build because it won't have
927 				// the VC vars set up in its sc.ini).
928 				// Possibly refactor the compiler configuration to a separate
929 				// component in the future to avoid the inefficiency of rebuilding
930 				// DMD just to generate a different sc.ini.
931 				@JSONOptional string commonModel = Component.CommonConfig.defaultModel;
932 			}
933 
934 			return FullConfig(
935 				config.build.components.dmd,
936 				config.build.components.common.makeArgs,
937 				config.build.components.common.model,
938 			).toJson();
939 		}
940 
941 		@property string vsConfiguration() { return config.build.components.dmd.debugDMD ? "Debug" : "Release"; }
942 		@property string vsPlatform     () { return config.build.components.dmd.dmdModel == "64" ? "x64" : "Win32"; }
943 
944 		override void performBuild()
945 		{
946 			// We need an older DMC for older DMD versions
947 			string dmcVer = null;
948 			auto idgen = buildPath(sourceDir, "src", "idgen.c");
949 			if (idgen.exists && idgen.readText().indexOf(`{ "alignof" },`) >= 0)
950 				dmcVer = "850";
951 
952 			auto env = baseEnvironment;
953 			needCC(env, config.build.components.dmd.dmdModel, dmcVer); // Need VC too for VSINSTALLDIR
954 
955 			auto srcDir = buildPath(sourceDir, "src");
956 			string dmdMakeFileName = findMakeFile(srcDir, makeFileName);
957 			string dmdMakeFullName = srcDir.buildPath(dmdMakeFileName);
958 
959 			if (buildPath(sourceDir, "src", "idgen.d").exists ||
960 			    buildPath(sourceDir, "src", "ddmd", "idgen.d").exists ||
961 			    buildPath(sourceDir, "src", "ddmd", "mars.d").exists ||
962 			    buildPath(sourceDir, "src", "dmd", "mars.d").exists)
963 			{
964 				// Need an older DMD for bootstrapping.
965 				string dmdVer = "v2.067.1";
966 				if (sourceDir.buildPath("test/compilable/staticforeach.d").exists)
967 					dmdVer = "v2.068.0";
968 				version (Windows)
969 					if (config.build.components.dmd.dmdModel != Component.CommonConfig.defaultModel)
970 						dmdVer = "v2.070.2"; // dmd/src/builtin.d needs core.stdc.math.fabsl. 2.068.2 generates a dmd which crashes on building Phobos
971 				if (sourceDir.buildPath("src/dmd/backend/dvec.d").exists) // 2.079 is needed since 2.080
972 					dmdVer = "v2.079.0";
973 				needDMD(env, dmdVer);
974 
975 				// Go back to our commit (in case we bootstrapped from source).
976 				needSource();
977 				submodule.clean = false;
978 			}
979 
980 			if (config.build.components.dmd.useVC) // Mostly obsolete, see useVC ddoc
981 			{
982 				version (Windows)
983 				{
984 					needVC(env, config.build.components.dmd.dmdModel);
985 
986 					env.vars["PATH"] = env.vars["PATH"] ~ pathSeparator ~ env.deps.hostDC.dirName;
987 
988 					auto solutionFile = `dmd_msc_vs10.sln`;
989 					if (!exists(srcDir.buildPath(solutionFile)))
990 						solutionFile = `vcbuild\dmd.sln`;
991 					if (!exists(srcDir.buildPath(solutionFile)))
992 						throw new Exception("Can't find Visual Studio solution file");
993 
994 					return run(["msbuild", "/p:Configuration=" ~ vsConfiguration, "/p:Platform=" ~ vsPlatform, solutionFile], env.vars, srcDir);
995 				}
996 				else
997 					throw new Exception("Can only use Visual Studio on Windows");
998 			}
999 
1000 			version (Windows)
1001 				auto scRoot = env.deps.dmcDir.absolutePath();
1002 
1003 			string modelFlag = config.build.components.dmd.dmdModel;
1004 			if (dmdMakeFullName.readText().canFind("MODEL=-m32"))
1005 				modelFlag = "-m" ~ modelFlag;
1006 
1007 			version (Windows)
1008 			{
1009 				// A make argument is insufficient,
1010 				// because of recursive make invocations
1011 				auto m = dmdMakeFullName.readText();
1012 				m = m
1013 					.replace(`CC=\dm\bin\dmc`, `CC=dmc`)
1014 					.replace(`SCROOT=$D\dm`, `SCROOT=` ~ scRoot)
1015 				;
1016 				dmdMakeFullName.write(m);
1017 			}
1018 			else
1019 			{
1020 				auto m = dmdMakeFullName.readText();
1021 				m = m
1022 					// Fix hard-coded reference to gcc as linker
1023 					.replace(`gcc -m32 -lstdc++`, `g++ -m32 -lstdc++`)
1024 					.replace(`gcc $(MODEL) -lstdc++`, `g++ $(MODEL) -lstdc++`)
1025 					// Fix compilation of older versions of go.c with GCC 6
1026 					.replace(`-Wno-deprecated`, `-Wno-deprecated -Wno-narrowing`)
1027 				;
1028 				// Fix pthread linker error
1029 				version (linux)
1030 					m = m.replace(`-lpthread`, `-pthread`);
1031 				dmdMakeFullName.write(m);
1032 			}
1033 
1034 			submodule.saveFileState("src/" ~ dmdMakeFileName);
1035 
1036 			// Fix compilation error of older DMDs with glibc >= 2.25
1037 			version (linux)
1038 			{{
1039 				auto fn = srcDir.buildPath("root", "port.c");
1040 				if (fn.exists)
1041 				{
1042 					fn.write(fn.readText
1043 						.replace(`#include <bits/mathdef.h>`, `#include <complex.h>`)
1044 					);
1045 					submodule.saveFileState(fn.relativePath(sourceDir));
1046 				}
1047 			}}
1048 
1049 			// Fix alignment issue in older DMDs with GCC >= 7
1050 			// See https://issues.dlang.org/show_bug.cgi?id=17726
1051 			version (Posix)
1052 			{
1053 				foreach (fn; [srcDir.buildPath("tk", "mem.c"), srcDir.buildPath("ddmd", "tk", "mem.c")])
1054 					if (fn.exists)
1055 					{
1056 						fn.write(fn.readText.replace(
1057 								// `#if defined(__llvm__) && (defined(__GNUC__) || defined(__clang__))`,
1058 								// `#if defined(__GNUC__) || defined(__clang__)`,
1059 								`numbytes = (numbytes + 3) & ~3;`,
1060 								`numbytes = (numbytes + 0xF) & ~0xF;`
1061 						));
1062 						submodule.saveFileState(fn.relativePath(sourceDir));
1063 					}
1064 			}
1065 
1066 			string[] extraArgs, targets;
1067 			version (Posix)
1068 			{
1069 				if (config.build.components.dmd.debugDMD)
1070 					extraArgs ~= "DEBUG=1";
1071 				if (config.build.components.dmd.releaseDMD)
1072 					extraArgs ~= "ENABLE_RELEASE=1";
1073 			}
1074 			else
1075 			{
1076 				if (config.build.components.dmd.debugDMD)
1077 					targets ~= [];
1078 				else
1079 				if (config.build.components.dmd.releaseDMD && dmdMakeFullName.readText().canFind("reldmd"))
1080 					targets ~= ["reldmd"];
1081 				else
1082 					targets ~= ["dmd"];
1083 			}
1084 
1085 			version (Windows)
1086 			{
1087 				if (config.build.components.dmd.dmdModel != CommonConfig.defaultModel)
1088 				{
1089 					dmdMakeFileName = "win64.mak";
1090 					dmdMakeFullName = srcDir.buildPath(dmdMakeFileName);
1091 					enforce(dmdMakeFullName.exists, "dmdModel not supported for this DMD version");
1092 					extraArgs ~= "DMODEL=-m" ~ config.build.components.dmd.dmdModel;
1093 					if (config.build.components.dmd.dmdModel == "32mscoff")
1094 					{
1095 						auto objFiles = dmdMakeFullName.readText().splitLines().filter!(line => line.startsWith("OBJ_MSVC="));
1096 						enforce(!objFiles.empty, "Can't find OBJ_MSVC in win64.mak");
1097 						extraArgs ~= "OBJ_MSVC=" ~ objFiles.front.findSplit("=")[2].split().filter!(obj => obj != "ldfpu.obj").join(" ");
1098 					}
1099 				}
1100 			}
1101 
1102 			// Avoid HOST_DC reading ~/dmd.conf
1103 			string hostDC = env.deps.hostDC;
1104 			version (Posix)
1105 			if (hostDC && needConfSwitch())
1106 			{
1107 				auto dcProxy = buildPath(config.local.workDir, "host-dc-proxy.sh");
1108 				std.file.write(dcProxy, escapeShellCommand(["exec", hostDC, "-conf=" ~ buildPath(dirName(hostDC), configFileName)]) ~ ` "$@"`);
1109 				setAttributes(dcProxy, octal!755);
1110 				hostDC = dcProxy;
1111 			}
1112 
1113 			run(getMake(env) ~ [
1114 					"-f", dmdMakeFileName,
1115 					"MODEL=" ~ modelFlag,
1116 					"HOST_DC=" ~ hostDC,
1117 				] ~ config.build.components.common.makeArgs ~ dMakeArgs ~ extraArgs ~ targets,
1118 				env.vars, srcDir
1119 			);
1120 		}
1121 
1122 		override void performStage()
1123 		{
1124 			if (config.build.components.dmd.useVC)
1125 			{
1126 				foreach (ext; [".exe", ".pdb"])
1127 					cp(
1128 						buildPath(sourceDir, "src", "vcbuild", vsPlatform, vsConfiguration, "dmd_msc" ~ ext),
1129 						buildPath(stageDir , "bin", "dmd" ~ ext),
1130 					);
1131 			}
1132 			else
1133 			{
1134 				string dmdPath = buildPath(sourceDir, "generated", platform, "release", config.build.components.dmd.dmdModel, "dmd" ~ binExt);
1135 				if (!dmdPath.exists)
1136 					dmdPath = buildPath(sourceDir, "src", "dmd" ~ binExt); // legacy
1137 				enforce(dmdPath.exists && dmdPath.isFile, "Can't find built DMD executable");
1138 
1139 				cp(
1140 					dmdPath,
1141 					buildPath(stageDir , "bin", "dmd" ~ binExt),
1142 				);
1143 			}
1144 
1145 			version (Windows)
1146 			{
1147 				auto env = baseEnvironment;
1148 				needCC(env, config.build.components.dmd.dmdModel);
1149 				foreach (model; config.build.components.common.models)
1150 					needCC(env, model);
1151 
1152 				auto ini = q"EOS
1153 [Environment]
1154 LIB="%@P%\..\lib"
1155 DFLAGS="-I%@P%\..\import"
1156 DMC=__DMC__
1157 LINKCMD=%DMC%\link.exe
1158 EOS"
1159 				.replace("__DMC__", env.deps.dmcDir.buildPath(`bin`).absolutePath())
1160 			;
1161 
1162 				if (env.deps.vsDir && env.deps.sdkDir)
1163 				{
1164 					ini ~= q"EOS
1165 
1166 [Environment64]
1167 LIB="%@P%\..\lib"
1168 DFLAGS=%DFLAGS% -L/OPT:NOICF
1169 VSINSTALLDIR=__VS__\
1170 VCINSTALLDIR=%VSINSTALLDIR%VC\
1171 PATH=%PATH%;%VCINSTALLDIR%\bin\__MODELDIR__;%VCINSTALLDIR%\bin
1172 WindowsSdkDir=__SDK__
1173 LINKCMD=%VCINSTALLDIR%\bin\__MODELDIR__\link.exe
1174 LIB=%LIB%;"%VCINSTALLDIR%\lib\amd64"
1175 LIB=%LIB%;"%WindowsSdkDir%\Lib\x64"
1176 
1177 [Environment32mscoff]
1178 LIB="%@P%\..\lib"
1179 DFLAGS=%DFLAGS% -L/OPT:NOICF
1180 VSINSTALLDIR=__VS__\
1181 VCINSTALLDIR=%VSINSTALLDIR%VC\
1182 PATH=%PATH%;%VCINSTALLDIR%\bin
1183 WindowsSdkDir=__SDK__
1184 LINKCMD=%VCINSTALLDIR%\bin\link.exe
1185 LIB=%LIB%;"%VCINSTALLDIR%\lib"
1186 LIB=%LIB%;"%WindowsSdkDir%\Lib"
1187 EOS"
1188 						.replace("__VS__"      , env.deps.vsDir .absolutePath())
1189 						.replace("__SDK__"     , env.deps.sdkDir.absolutePath())
1190 						.replace("__MODELDIR__", msvcModelDir("64"))
1191 					;
1192 				}
1193 
1194 				buildPath(stageDir, "bin", configFileName).write(ini);
1195 			}
1196 			else version (OSX)
1197 			{
1198 				auto ini = q"EOS
1199 [Environment]
1200 DFLAGS="-I%@P%/../import" "-L-L%@P%/../lib"
1201 EOS";
1202 				buildPath(stageDir, "bin", configFileName).write(ini);
1203 			}
1204 			else version (linux)
1205 			{
1206 				auto ini = q"EOS
1207 [Environment32]
1208 DFLAGS="-I%@P%/../import" "-L-L%@P%/../lib" -L--export-dynamic
1209 
1210 [Environment64]
1211 DFLAGS="-I%@P%/../import" "-L-L%@P%/../lib" -L--export-dynamic -fPIC
1212 EOS";
1213 				buildPath(stageDir, "bin", configFileName).write(ini);
1214 			}
1215 			else
1216 			{
1217 				auto ini = q"EOS
1218 [Environment]
1219 DFLAGS="-I%@P%/../import" "-L-L%@P%/../lib" -L--export-dynamic
1220 EOS";
1221 				buildPath(stageDir, "bin", configFileName).write(ini);
1222 			}
1223 		}
1224 
1225 		override void updateEnv(ref Environment env)
1226 		{
1227 			// Add the DMD we built for Phobos/Druntime/Tools
1228 			env.vars["PATH"] = buildPath(buildDir, "bin").absolutePath() ~ pathSeparator ~ env.vars["PATH"];
1229 		}
1230 
1231 		override void performTest()
1232 		{
1233 			foreach (dep; ["dmd", "druntime", "phobos"])
1234 				getComponent(dep).needBuild();
1235 
1236 			foreach (model; config.build.components.common.models)
1237 			{
1238 				auto env = baseEnvironment;
1239 				version (Windows)
1240 				{
1241 					// In this order so it uses the MSYS make
1242 					needCC(env, model);
1243 					needMSYS(env);
1244 
1245 					disableCrashDialog();
1246 				}
1247 
1248 				auto makeArgs = getMake(env) ~ config.build.components.common.makeArgs ~ getPlatformMakeVars(env, model) ~ gnuMakeArgs;
1249 				version (Windows)
1250 				{
1251 					makeArgs ~= ["OS=win" ~ model[0..2], "SHELL=bash"];
1252 					if (model == "32")
1253 					{
1254 						auto extrasDir = needExtras();
1255 						// The autotester seems to pass this via environment. Why does that work there???
1256 						makeArgs ~= "LIB=" ~ extrasDir.buildPath("localextras-windows", "dmd2", "windows", "lib") ~ `;..\..\phobos`;
1257 					}
1258 					else
1259 					{
1260 						// Fix path for d_do_test and its special escaping (default is the system VS2010 install)
1261 						// We can't use the same syntax in getPlatformMakeVars because win64.mak uses "CC=\$(CC32)"\""
1262 						auto cl = env.deps.vsDir.buildPath("VC", "bin", msvcModelDir(model), "cl.exe");
1263 						foreach (ref arg; makeArgs)
1264 							if (arg.startsWith("CC="))
1265 								arg = "CC=" ~ dDoTestEscape(cl);
1266 					}
1267 				}
1268 
1269 				version (test)
1270 				{
1271 					// Only try a few tests during CI runs, to check for
1272 					// platform integration and correct invocation.
1273 					// For this purpose, the C++ ABI tests will do nicely.
1274 					makeArgs ~= [
1275 					//	"test_results/runnable/cppa.d.out", // https://github.com/dlang/dmd/pull/5686
1276 						"test_results/runnable/cpp_abi_tests.d.out",
1277 						"test_results/runnable/cabi1.d.out",
1278 					];
1279 				}
1280 
1281 				run(makeArgs, env.vars, sourceDir.buildPath("test"));
1282 			}
1283 		}
1284 	}
1285 
1286 	/// Phobos import files.
1287 	/// In older versions of D, Druntime depended on Phobos modules.
1288 	final class PhobosIncludes : Component
1289 	{
1290 		@property override string submoduleName() { return "phobos"; }
1291 		@property override string[] sourceDependencies() { return []; }
1292 		@property override string[] dependencies() { return []; }
1293 		@property override string configString() { return null; }
1294 
1295 		override void performStage()
1296 		{
1297 			foreach (f; ["std", "etc", "crc32.d"])
1298 				if (buildPath(sourceDir, f).exists)
1299 					cp(
1300 						buildPath(sourceDir, f),
1301 						buildPath(stageDir , "import", f),
1302 					);
1303 		}
1304 	}
1305 
1306 	/// Druntime. Installs only import files, but builds the library too.
1307 	final class Druntime : Component
1308 	{
1309 		@property override string submoduleName    () { return "druntime"; }
1310 		@property override string[] sourceDependencies() { return ["phobos", "phobos-includes"]; }
1311 		@property override string[] dependencies() { return ["dmd"]; }
1312 
1313 		@property override string configString()
1314 		{
1315 			static struct FullConfig
1316 			{
1317 				string model;
1318 				string[] makeArgs;
1319 			}
1320 
1321 			return FullConfig(
1322 				config.build.components.common.model,
1323 				config.build.components.common.makeArgs,
1324 			).toJson();
1325 		}
1326 
1327 		override void performBuild()
1328 		{
1329 			getComponent("phobos").needSource();
1330 			getComponent("dmd").needSource();
1331 			getComponent("dmd").needInstalled();
1332 			getComponent("phobos-includes").needInstalled();
1333 
1334 			foreach (model; config.build.components.common.models)
1335 			{
1336 				auto env = baseEnvironment;
1337 				needCC(env, model);
1338 
1339 				mkdirRecurse(sourceDir.buildPath("import"));
1340 				mkdirRecurse(sourceDir.buildPath("lib"));
1341 
1342 				setTimes(sourceDir.buildPath("src", "rt", "minit.obj"), Clock.currTime(), Clock.currTime()); // Don't rebuild
1343 				submodule.saveFileState("src/rt/minit.obj");
1344 
1345 				runMake(env, model, "import");
1346 				runMake(env, model);
1347 			}
1348 		}
1349 
1350 		override void performStage()
1351 		{
1352 			cp(
1353 				buildPath(sourceDir, "import"),
1354 				buildPath(stageDir , "import"),
1355 			);
1356 		}
1357 
1358 		override void performTest()
1359 		{
1360 			getComponent("druntime").needBuild();
1361 			getComponent("dmd").needInstalled();
1362 
1363 			foreach (model; config.build.components.common.models)
1364 			{
1365 				auto env = baseEnvironment;
1366 				needCC(env, model);
1367 				runMake(env, model, "unittest");
1368 			}
1369 		}
1370 
1371 		private final void runMake(ref Environment env, string model, string target = null)
1372 		{
1373 			// Work around https://github.com/dlang/druntime/pull/2438
1374 			bool quotePaths = !(isVersion!"Windows" && model != "32" && sourceDir.buildPath("win64.mak").readText().canFind(`"$(CC)"`));
1375 
1376 			string[] args =
1377 				getMake(env) ~
1378 				["-f", makeFileNameModel(model)] ~
1379 				(target ? [target] : []) ~
1380 				["DMD=" ~ dmd] ~
1381 				config.build.components.common.makeArgs ~
1382 				getPlatformMakeVars(env, model, quotePaths) ~
1383 				dMakeArgs;
1384 			run(args, env.vars, sourceDir);
1385 		}
1386 	}
1387 
1388 	/// Phobos library and imports.
1389 	final class Phobos : Component
1390 	{
1391 		@property override string submoduleName    () { return "phobos"; }
1392 		@property override string[] sourceDependencies() { return []; }
1393 		@property override string[] dependencies() { return ["druntime", "dmd"]; }
1394 
1395 		@property override string configString()
1396 		{
1397 			static struct FullConfig
1398 			{
1399 				string model;
1400 				string[] makeArgs;
1401 			}
1402 
1403 			return FullConfig(
1404 				config.build.components.common.model,
1405 				config.build.components.common.makeArgs,
1406 			).toJson();
1407 		}
1408 
1409 		string[] targets;
1410 
1411 		override void performBuild()
1412 		{
1413 			getComponent("dmd").needSource();
1414 			getComponent("dmd").needInstalled();
1415 			getComponent("druntime").needBuild();
1416 
1417 			targets = null;
1418 
1419 			foreach (model; config.build.components.common.models)
1420 			{
1421 				// Clean up old object files with mismatching model.
1422 				// Necessary for a consecutive 32/64 build.
1423 				version (Windows)
1424 				{
1425 					foreach (de; dirEntries(sourceDir.buildPath("etc", "c", "zlib"), "*.obj", SpanMode.shallow))
1426 					{
1427 						auto data = cast(ubyte[])read(de.name);
1428 
1429 						string fileModel;
1430 						if (data.length < 4)
1431 							fileModel = "invalid";
1432 						else
1433 						if (data[0] == 0x80)
1434 							fileModel = "32"; // OMF
1435 						else
1436 						if (data[0] == 0x01 && data[0] == 0x4C)
1437 							fileModel = "32mscoff"; // COFF - IMAGE_FILE_MACHINE_I386
1438 						else
1439 						if (data[0] == 0x86 && data[0] == 0x64)
1440 							fileModel = "64"; // COFF - IMAGE_FILE_MACHINE_AMD64
1441 						else
1442 							fileModel = "unknown";
1443 
1444 						if (fileModel != model)
1445 						{
1446 							log("Cleaning up object file '%s' with mismatching model (file is %s, building %s)".format(de.name, fileModel, model));
1447 							remove(de.name);
1448 						}
1449 					}
1450 				}
1451 
1452 				auto env = baseEnvironment;
1453 				needCC(env, model);
1454 
1455 				string phobosMakeFileName = findMakeFile(sourceDir, makeFileNameModel(model));
1456 				string phobosMakeFullName = sourceDir.buildPath(phobosMakeFileName);
1457 
1458 				version (Windows)
1459 				{
1460 					auto lib = "phobos%s.lib".format(modelSuffix(model));
1461 					runMake(env, model, lib);
1462 					enforce(sourceDir.buildPath(lib).exists);
1463 					targets ~= ["phobos%s.lib".format(modelSuffix(model))];
1464 				}
1465 				else
1466 				{
1467 					string[] makeArgs;
1468 					if (phobosMakeFullName.readText().canFind("DRUNTIME = $(DRUNTIME_PATH)/lib/libdruntime-$(OS)$(MODEL).a") &&
1469 						getComponent("druntime").sourceDir.buildPath("lib").dirEntries(SpanMode.shallow).walkLength == 0 &&
1470 						exists(getComponent("druntime").sourceDir.buildPath("generated")))
1471 					{
1472 						auto dir = getComponent("druntime").sourceDir.buildPath("generated");
1473 						auto aFile  = dir.dirEntries("libdruntime.a", SpanMode.depth);
1474 						if (!aFile .empty) makeArgs ~= ["DRUNTIME="   ~ aFile .front];
1475 						auto soFile = dir.dirEntries("libdruntime.so.a", SpanMode.depth);
1476 						if (!soFile.empty) makeArgs ~= ["DRUNTIMESO=" ~ soFile.front];
1477 					}
1478 					runMake(env, model, makeArgs);
1479 					targets ~= sourceDir
1480 						.buildPath("generated")
1481 						.dirEntries(SpanMode.depth)
1482 						.filter!(de => de.name.endsWith(".a") || de.name.endsWith(".so"))
1483 						.map!(de => de.name.relativePath(sourceDir))
1484 						.array()
1485 					;
1486 				}
1487 			}
1488 		}
1489 
1490 		override void performStage()
1491 		{
1492 			assert(targets.length, "Druntime stage without build");
1493 			foreach (lib; targets)
1494 				cp(
1495 					buildPath(sourceDir, lib),
1496 					buildPath(stageDir , "lib", lib.baseName()),
1497 				);
1498 		}
1499 
1500 		override void performTest()
1501 		{
1502 			getComponent("druntime").needBuild();
1503 			getComponent("phobos").needBuild();
1504 			getComponent("dmd").needInstalled();
1505 
1506 			foreach (model; config.build.components.common.models)
1507 			{
1508 				auto env = baseEnvironment;
1509 				needCC(env, model);
1510 				version (Windows)
1511 				{
1512 					getComponent("curl").needInstalled();
1513 					getComponent("curl").updateEnv(env);
1514 
1515 					// Patch out std.datetime unittest to work around Digger test
1516 					// suite failure on AppVeyor due to Windows time zone changes
1517 					auto stdDateTime = buildPath(sourceDir, "std", "datetime.d");
1518 					if (stdDateTime.exists && !stdDateTime.readText().canFind("Altai Standard Time"))
1519 					{
1520 						auto m = stdDateTime.readText();
1521 						m = m
1522 							.replace(`assert(tzName !is null, format("TZName which is missing: %s", winName));`, ``)
1523 							.replace(`assert(tzDatabaseNameToWindowsTZName(tzName) !is null, format("TZName which failed: %s", tzName));`, `{}`)
1524 							.replace(`assert(windowsTZNameToTZDatabaseName(tzName) !is null, format("TZName which failed: %s", tzName));`, `{}`)
1525 						;
1526 						stdDateTime.write(m);
1527 						submodule.saveFileState("std/datetime.d");
1528 					}
1529 
1530 					if (model == "32")
1531 						getComponent("extras").needInstalled();
1532 				}
1533 				runMake(env, model, "unittest");
1534 			}
1535 		}
1536 
1537 		private final void runMake(ref Environment env, string model, string[] makeArgs...)
1538 		{
1539 			// Work around https://github.com/dlang/druntime/pull/2438
1540 			bool quotePaths = !(isVersion!"Windows" && model != "32" && sourceDir.buildPath("win64.mak").readText().canFind(`"$(CC)"`));
1541 
1542 			string[] args =
1543 				getMake(env) ~
1544 				["-f", makeFileNameModel(model)] ~
1545 				makeArgs ~
1546 				["DMD=" ~ dmd] ~
1547 				config.build.components.common.makeArgs ~
1548 				getPlatformMakeVars(env, model, quotePaths) ~
1549 				dMakeArgs;
1550 			run(args, env.vars, sourceDir);
1551 		}
1552 	}
1553 
1554 	/// The rdmd build tool by itself.
1555 	/// It predates the tools package.
1556 	final class RDMD : Component
1557 	{
1558 		@property override string submoduleName() { return "tools"; }
1559 		@property override string[] sourceDependencies() { return []; }
1560 		@property override string[] dependencies() { return ["dmd", "druntime", "phobos"]; }
1561 
1562 		@property string model() { return config.build.components.common.models.get(0); }
1563 
1564 		@property override string configString()
1565 		{
1566 			static struct FullConfig
1567 			{
1568 				string model;
1569 			}
1570 
1571 			return FullConfig(
1572 				this.model,
1573 			).toJson();
1574 		}
1575 
1576 		override void performBuild()
1577 		{
1578 			foreach (dep; ["dmd", "druntime", "phobos", "phobos-includes"])
1579 				getComponent(dep).needInstalled();
1580 
1581 			auto env = baseEnvironment;
1582 			needCC(env, this.model);
1583 
1584 			// Just build rdmd
1585 			bool needModel; // Need -mXX switch?
1586 
1587 			if (sourceDir.buildPath("posix.mak").exists)
1588 				needModel = true; // Known to be needed for recent versions
1589 
1590 			string[] args;
1591 			if (needConfSwitch())
1592 				args ~= ["-conf=" ~ buildPath(buildDir , "bin", configFileName)];
1593 			args ~= ["rdmd"];
1594 
1595 			if (!needModel)
1596 				try
1597 					run([dmd] ~ args, env.vars, sourceDir);
1598 				catch (Exception e)
1599 					needModel = true;
1600 
1601 			if (needModel)
1602 				run([dmd, "-m" ~ this.model] ~ args, env.vars, sourceDir);
1603 		}
1604 
1605 		override void performStage()
1606 		{
1607 			cp(
1608 				buildPath(sourceDir, "rdmd" ~ binExt),
1609 				buildPath(stageDir , "bin", "rdmd" ~ binExt),
1610 			);
1611 		}
1612 
1613 		override void performTest()
1614 		{
1615 			auto env = baseEnvironment;
1616 			version (Windows)
1617 				needDMC(env); // Need DigitalMars Make
1618 
1619 			string[] args;
1620 			if (sourceDir.buildPath(makeFileName).readText.canFind("\ntest_rdmd"))
1621 				args = getMake(env) ~ ["-f", makeFileName, "test_rdmd", "DFLAGS=-g -m" ~ model] ~ config.build.components.common.makeArgs ~ getPlatformMakeVars(env, model) ~ dMakeArgs;
1622 			else
1623 			{
1624 				// Legacy (before makefile rules)
1625 
1626 				args = ["dmd", "-m" ~ this.model, "-run", "rdmd_test.d"];
1627 				if (sourceDir.buildPath("rdmd_test.d").readText.canFind("modelSwitch"))
1628 					args ~= "--model=" ~ this.model;
1629 				else
1630 				{
1631 					version (Windows)
1632 						if (this.model != "32")
1633 						{
1634 							// Can't test rdmd on non-32-bit Windows until compiler model matches Phobos model.
1635 							// rdmd_test does not use -m when building rdmd, thus linking will fail
1636 							// (because of model mismatch with the phobos we built).
1637 							log("Can't test rdmd with model " ~ this.model ~ ", skipping");
1638 							return;
1639 						}
1640 				}
1641 			}
1642 
1643 			foreach (dep; ["dmd", "druntime", "phobos", "phobos-includes"])
1644 				getComponent(dep).needInstalled();
1645 
1646 			getComponent("dmd").updateEnv(env);
1647 			run(args, env.vars, sourceDir);
1648 		}
1649 	}
1650 
1651 	/// Tools package with all its components, including rdmd.
1652 	final class Tools : Component
1653 	{
1654 		@property override string submoduleName() { return "tools"; }
1655 		@property override string[] sourceDependencies() { return []; }
1656 		@property override string[] dependencies() { return ["dmd", "druntime", "phobos"]; }
1657 
1658 		@property string model() { return config.build.components.common.models.get(0); }
1659 
1660 		@property override string configString()
1661 		{
1662 			static struct FullConfig
1663 			{
1664 				string model;
1665 				string[] makeArgs;
1666 			}
1667 
1668 			return FullConfig(
1669 				this.model,
1670 				config.build.components.common.makeArgs,
1671 			).toJson();
1672 		}
1673 
1674 		override void performBuild()
1675 		{
1676 			getComponent("dmd").needSource();
1677 			foreach (dep; ["dmd", "druntime", "phobos"])
1678 				getComponent(dep).needInstalled();
1679 
1680 			auto env = baseEnvironment;
1681 			needCC(env, this.model);
1682 
1683 			run(getMake(env) ~ ["-f", makeFileName, "DMD=" ~ dmd] ~ config.build.components.common.makeArgs ~ getPlatformMakeVars(env, this.model) ~ dMakeArgs, env.vars, sourceDir);
1684 		}
1685 
1686 		override void performStage()
1687 		{
1688 			foreach (os; buildPath(sourceDir, "generated").dirEntries(SpanMode.shallow))
1689 				foreach (de; os.buildPath(this.model).dirEntries(SpanMode.shallow))
1690 					if (de.extension == binExt)
1691 						cp(de, buildPath(stageDir, "bin", de.baseName));
1692 		}
1693 	}
1694 
1695 	/// Website (dlang.org). Only buildable on POSIX.
1696 	final class Website : Component
1697 	{
1698 		@property override string submoduleName() { return "dlang.org"; }
1699 		@property override string[] sourceDependencies() { return ["druntime", "phobos"]; }
1700 		@property override string[] dependencies() { return ["dmd", "druntime", "phobos", "rdmd"]; }
1701 
1702 		struct Config
1703 		{
1704 			/// Do not include timestamps, line numbers, or other
1705 			/// volatile dynamic content in generated .ddoc files.
1706 			/// Improves cache efficiency and allows meaningful diffs.
1707 			bool diffable = false;
1708 
1709 			deprecated alias noDateTime = diffable;
1710 		}
1711 
1712 		@property override string configString()
1713 		{
1714 			static struct FullConfig
1715 			{
1716 				Config config;
1717 			}
1718 
1719 			return FullConfig(
1720 				config.build.components.website,
1721 			).toJson();
1722 		}
1723 
1724 		/// Get the latest version of DMD at the time.
1725 		/// Needed for the makefile's "LATEST" parameter.
1726 		string getLatest()
1727 		{
1728 			auto dmd = getComponent("dmd").submodule;
1729 			dmd.needRepo();
1730 
1731 			auto t = dmd.git.query(["log", "--pretty=format:%ct"]).splitLines.map!(to!int).filter!(n => n > 0).front;
1732 
1733 			foreach (line; dmd.git.query(["log", "--decorate=full", "--tags", "--pretty=format:%ct%d"]).splitLines())
1734 				if (line.length > 10 && line[0..10].to!int < t)
1735 					if (line[10..$].startsWith(" (") && line.endsWith(")"))
1736 					{
1737 						foreach (r; line[12..$-1].split(", "))
1738 							if (r.skipOver("tag: refs/tags/"))
1739 								if (r.match(re!`^v2\.\d\d\d(\.\d)?$`))
1740 									return r[1..$];
1741 					}
1742 			throw new Exception("Can't find any DMD version tags at this point!");
1743 		}
1744 
1745 		private enum Target { build, test }
1746 
1747 		private void make(Target target)
1748 		{
1749 			foreach (dep; ["dmd", "druntime", "phobos"])
1750 			{
1751 				auto c = getComponent(dep);
1752 				c.needInstalled();
1753 
1754 				// Need DMD source because https://github.com/dlang/phobos/pull/4613#issuecomment-266462596
1755 				// Need Druntime/Phobos source because we are building its documentation from there.
1756 				c.needSource();
1757 			}
1758 			getComponent("tools").needSource(); // for changed.d
1759 
1760 			auto env = baseEnvironment;
1761 
1762 			version (Windows)
1763 				throw new Exception("The dlang.org website is only buildable on POSIX platforms.");
1764 			else
1765 			{
1766 				getComponent("dmd").updateEnv(env);
1767 
1768 				// Need an in-tree build for SYSCONFDIR.imp, which is
1769 				// needed to parse .d files for the DMD API
1770 				// documentation.
1771 				getComponent("dmd").needBuild();
1772 
1773 				needKindleGen(env);
1774 
1775 				foreach (dep; dependencies)
1776 					getComponent(dep).submodule.clean = false;
1777 
1778 				auto makeFullName = sourceDir.buildPath(makeFileName);
1779 				auto makeSrc = makeFullName.readText();
1780 				makeSrc
1781 					// https://github.com/D-Programming-Language/dlang.org/pull/1011
1782 					.replace(": modlist.d", ": modlist.d $(DMD)")
1783 					// https://github.com/D-Programming-Language/dlang.org/pull/1017
1784 					.replace("dpl-docs: ${DUB} ${STABLE_DMD}\n\tDFLAGS=", "dpl-docs: ${DUB} ${STABLE_DMD}\n\t${DUB} upgrade --missing-only --root=${DPL_DOCS_PATH}\n\tDFLAGS=")
1785 					.toFile(makeFullName)
1786 				;
1787 				submodule.saveFileState(makeFileName);
1788 
1789 				// Retroactive OpenSSL 1.1.0 fix
1790 				// See https://github.com/dlang/dlang.org/pull/1654
1791 				auto dubJson = sourceDir.buildPath("dpl-docs/dub.json");
1792 				dubJson
1793 					.readText()
1794 					.replace(`"versions": ["VibeCustomMain"]`, `"versions": ["VibeCustomMain", "VibeNoSSL"]`)
1795 					.toFile(dubJson);
1796 				submodule.saveFileState("dpl-docs/dub.json");
1797 				scope(exit) submodule.saveFileState("dpl-docs/dub.selections.json");
1798 
1799 				string latest = null;
1800 				if (!sourceDir.buildPath("VERSION").exists)
1801 				{
1802 					latest = getLatest();
1803 					log("LATEST=" ~ latest);
1804 				}
1805 				else
1806 					log("VERSION file found, not passing LATEST parameter");
1807 
1808 				string[] diffable = null;
1809 
1810 				string[] targets =
1811 					[
1812 						config.build.components.website.diffable
1813 						? makeSrc.indexOf("dautotest") >= 0
1814 							? ["dautotest"]
1815 							: ["all", "verbatim", "pdf"] ~ (
1816 								makeSrc.indexOf("diffable-intermediaries") >= 0
1817 								? ["diffable-intermediaries"]
1818 								: ["dlangspec.html"])
1819 						: ["all", "verbatim", "pdf", "kindle"],
1820 						["test"]
1821 					][target];
1822 
1823 				if (config.build.components.website.diffable)
1824 				{
1825 					if (makeSrc.indexOf("DIFFABLE") >= 0)
1826 						diffable = ["DIFFABLE=1"];
1827 					else
1828 						diffable = ["NODATETIME=nodatetime.ddoc"];
1829 
1830 					env.vars["SOURCE_DATE_EPOCH"] = "0";
1831 				}
1832 
1833 				auto args =
1834 					getMake(env) ~
1835 					[ "-f", makeFileName ] ~
1836 					diffable ~
1837 					(latest ? ["LATEST=" ~ latest] : []) ~
1838 					targets ~
1839 					gnuMakeArgs;
1840 				run(args, env.vars, sourceDir);
1841 			}
1842 		}
1843 
1844 		override void performBuild()
1845 		{
1846 			make(Target.build);
1847 		}
1848 
1849 		override void performTest()
1850 		{
1851 			make(Target.test);
1852 		}
1853 
1854 		override void performStage()
1855 		{
1856 			foreach (item; ["web", "dlangspec.tex", "dlangspec.html"])
1857 			{
1858 				auto src = buildPath(sourceDir, item);
1859 				auto dst = buildPath(stageDir , item);
1860 				if (src.exists)
1861 					cp(src, dst);
1862 			}
1863 		}
1864 	}
1865 
1866 	/// Extras not built from source (DigitalMars and third-party tools and libraries)
1867 	final class Extras : Component
1868 	{
1869 		@property override string submoduleName() { return null; }
1870 		@property override string[] sourceDependencies() { return []; }
1871 		@property override string[] dependencies() { return []; }
1872 		@property override string configString() { return null; }
1873 
1874 		override void performBuild()
1875 		{
1876 			needExtras();
1877 		}
1878 
1879 		override void performStage()
1880 		{
1881 			auto extrasDir = needExtras();
1882 
1883 			void copyDir(string source, string target)
1884 			{
1885 				source = buildPath(extrasDir, "localextras-" ~ platform, "dmd2", platform, source);
1886 				target = buildPath(stageDir, target);
1887 				if (source.exists)
1888 					cp(source, target);
1889 			}
1890 
1891 			copyDir("bin", "bin");
1892 			foreach (model; config.build.components.common.models)
1893 				copyDir("bin" ~ model, "bin");
1894 			copyDir("lib", "lib");
1895 
1896 			version (Windows)
1897 				foreach (model; config.build.components.common.models)
1898 					if (model == "32")
1899 					{
1900 						// The version of snn.lib bundled with DMC will be newer.
1901 						Environment env;
1902 						needDMC(env);
1903 						cp(buildPath(env.deps.dmcDir, "lib", "snn.lib"), buildPath(stageDir, "lib", "snn.lib"));
1904 					}
1905 		}
1906 	}
1907 
1908 	/// libcurl DLL and import library for Windows.
1909 	final class Curl : Component
1910 	{
1911 		@property override string submoduleName() { return null; }
1912 		@property override string[] sourceDependencies() { return []; }
1913 		@property override string[] dependencies() { return []; }
1914 		@property override string configString() { return null; }
1915 
1916 		override void performBuild()
1917 		{
1918 			version (Windows)
1919 				needCurl();
1920 			else
1921 				log("Not on Windows, skipping libcurl download");
1922 		}
1923 
1924 		override void performStage()
1925 		{
1926 			version (Windows)
1927 			{
1928 				auto curlDir = needCurl();
1929 
1930 				void copyDir(string source, string target)
1931 				{
1932 					source = buildPath(curlDir, "dmd2", "windows", source);
1933 					target = buildPath(stageDir, target);
1934 					if (source.exists)
1935 						cp(source, target);
1936 				}
1937 
1938 				foreach (model; config.build.components.common.models)
1939 				{
1940 					auto suffix = model == "64" ? "64" : "";
1941 					copyDir("bin" ~ suffix, "bin");
1942 					copyDir("lib" ~ suffix, "lib");
1943 				}
1944 			}
1945 			else
1946 				log("Not on Windows, skipping libcurl install");
1947 		}
1948 
1949 		override void updateEnv(ref Environment env)
1950 		{
1951 			env.vars["PATH"] = buildPath(buildDir, "bin").absolutePath() ~ pathSeparator ~ env.vars["PATH"];
1952 		}
1953 	}
1954 
1955 	private int tempError;
1956 
1957 	private Component[string] components;
1958 
1959 	Component getComponent(string name)
1960 	{
1961 		if (name !in components)
1962 		{
1963 			Component c;
1964 
1965 			switch (name)
1966 			{
1967 				case "dmd":
1968 					c = new DMD();
1969 					break;
1970 				case "phobos-includes":
1971 					c = new PhobosIncludes();
1972 					break;
1973 				case "druntime":
1974 					c = new Druntime();
1975 					break;
1976 				case "phobos":
1977 					c = new Phobos();
1978 					break;
1979 				case "rdmd":
1980 					c = new RDMD();
1981 					break;
1982 				case "tools":
1983 					c = new Tools();
1984 					break;
1985 				case "website":
1986 					c = new Website();
1987 					break;
1988 				case "extras":
1989 					c = new Extras();
1990 					break;
1991 				case "curl":
1992 					c = new Curl();
1993 					break;
1994 				default:
1995 					throw new Exception("Unknown component: " ~ name);
1996 			}
1997 
1998 			c.name = name;
1999 			return components[name] = c;
2000 		}
2001 
2002 		return components[name];
2003 	}
2004 
2005 	Component[] getSubmoduleComponents(string submoduleName)
2006 	{
2007 		return components
2008 			.byValue
2009 			.filter!(component => component.submoduleName == submoduleName)
2010 			.array();
2011 	}
2012 
2013 	// **************************** Customization ****************************
2014 
2015 	/// Fetch latest D history.
2016 	void update()
2017 	{
2018 		getMetaRepo().update();
2019 	}
2020 
2021 	struct SubmoduleState
2022 	{
2023 		string[string] submoduleCommits;
2024 	}
2025 
2026 	/// Begin customization, starting at the specified commit.
2027 	SubmoduleState begin(string commit)
2028 	{
2029 		log("Starting at meta repository commit " ~ commit);
2030 		return SubmoduleState(getMetaRepo().getSubmoduleCommits(commit));
2031 	}
2032 
2033 	/// Applies a merge onto the given SubmoduleState.
2034 	void merge(ref SubmoduleState submoduleState, string submoduleName, string branch)
2035 	{
2036 		log("Merging %s commit %s".format(submoduleName, branch));
2037 		enforce(submoduleName in submoduleState.submoduleCommits, "Unknown submodule: " ~ submoduleName);
2038 		auto submodule = getSubmodule(submoduleName);
2039 		auto head = submoduleState.submoduleCommits[submoduleName];
2040 		auto result = submodule.getMerge(head, branch);
2041 		submoduleState.submoduleCommits[submoduleName] = result;
2042 	}
2043 
2044 	/// Removes a merge from the given SubmoduleState.
2045 	void unmerge(ref SubmoduleState submoduleState, string submoduleName, string branch)
2046 	{
2047 		log("Unmerging %s commit %s".format(submoduleName, branch));
2048 		enforce(submoduleName in submoduleState.submoduleCommits, "Unknown submodule: " ~ submoduleName);
2049 		auto submodule = getSubmodule(submoduleName);
2050 		auto head = submoduleState.submoduleCommits[submoduleName];
2051 		auto result = submodule.getUnMerge(head, branch);
2052 		submoduleState.submoduleCommits[submoduleName] = result;
2053 	}
2054 
2055 	/// Reverts a commit from the given SubmoduleState.
2056 	/// parent is the 1-based mainline index (as per `man git-revert`),
2057 	/// or 0 if commit is not a merge commit.
2058 	void revert(ref SubmoduleState submoduleState, string submoduleName, string commit, int parent)
2059 	{
2060 		log("Reverting %s commit %s".format(submoduleName, commit));
2061 		enforce(submoduleName in submoduleState.submoduleCommits, "Unknown submodule: " ~ submoduleName);
2062 		auto submodule = getSubmodule(submoduleName);
2063 		auto head = submoduleState.submoduleCommits[submoduleName];
2064 		auto result = submodule.getRevert(head, commit, parent);
2065 		submoduleState.submoduleCommits[submoduleName] = result;
2066 	}
2067 
2068 	/// Returns the commit hash for the given pull request #.
2069 	/// The result can then be used with addMerge/removeMerge.
2070 	string getPull(string submoduleName, int pullNumber)
2071 	{
2072 		return getSubmodule(submoduleName).getPull(pullNumber);
2073 	}
2074 
2075 	/// Returns the commit hash for the given GitHub fork.
2076 	/// The result can then be used with addMerge/removeMerge.
2077 	string getFork(string submoduleName, string user, string branch)
2078 	{
2079 		return getSubmodule(submoduleName).getFork(user, branch);
2080 	}
2081 
2082 	/// Find the child of a commit (starting with the current submodule state),
2083 	/// and, if the commit was a merge, the mainline index of said commit for the child.
2084 	void getChild(ref SubmoduleState submoduleState, string submoduleName, string commit, out string child, out int mainline)
2085 	{
2086 		enforce(submoduleName in submoduleState.submoduleCommits, "Unknown submodule: " ~ submoduleName);
2087 		auto head = submoduleState.submoduleCommits[submoduleName];
2088 		return getSubmodule(submoduleName).getChild(head, commit, child, mainline);
2089 	}
2090 
2091 	// ****************************** Building *******************************
2092 
2093 	private SubmoduleState submoduleState;
2094 	private bool incrementalBuild;
2095 
2096 	@property string cacheEngineName()
2097 	{
2098 		if (incrementalBuild)
2099 			return "none";
2100 		else
2101 			return config.local.cache;
2102 	}
2103 
2104 	private string getComponentCommit(string componentName)
2105 	{
2106 		auto submoduleName = getComponent(componentName).submoduleName;
2107 		auto commit = submoduleState.submoduleCommits.get(submoduleName, null);
2108 		enforce(commit, "Unknown commit to build for component %s (submodule %s)"
2109 			.format(componentName, submoduleName));
2110 		return commit;
2111 	}
2112 
2113 	static const string[] defaultComponents = ["dmd", "druntime", "phobos-includes", "phobos", "rdmd"];
2114 	static const string[] additionalComponents = ["tools", "website", "extras", "curl"];
2115 	static const string[] allComponents = defaultComponents ~ additionalComponents;
2116 
2117 	/// Build the specified components according to the specified configuration.
2118 	void build(SubmoduleState submoduleState, bool incremental = false)
2119 	{
2120 		auto componentNames = config.build.components.getEnabledComponentNames();
2121 		log("Building components %-(%s, %)".format(componentNames));
2122 
2123 		this.components = null;
2124 		this.submoduleState = submoduleState;
2125 		this.incrementalBuild = incremental;
2126 
2127 		if (buildDir.exists)
2128 			buildDir.removeRecurse();
2129 		enforce(!buildDir.exists);
2130 
2131 		scope(exit) if (cacheEngine) cacheEngine.finalize();
2132 
2133 		foreach (componentName; componentNames)
2134 			getComponent(componentName).needInstalled();
2135 	}
2136 
2137 	/// Shortcut for begin + build
2138 	void buildRev(string rev)
2139 	{
2140 		auto submoduleState = begin(rev);
2141 		build(submoduleState);
2142 	}
2143 
2144 	/// Simply check out the source code for the given submodules.
2145 	void checkout(SubmoduleState submoduleState)
2146 	{
2147 		auto componentNames = config.build.components.getEnabledComponentNames();
2148 		log("Checking out components %-(%s, %)".format(componentNames));
2149 
2150 		this.components = null;
2151 		this.submoduleState = submoduleState;
2152 		this.incrementalBuild = false;
2153 
2154 		foreach (componentName; componentNames)
2155 			getComponent(componentName).needSource();
2156 	}
2157 
2158 	/// Rerun build without cleaning up any files.
2159 	void rebuild()
2160 	{
2161 		build(SubmoduleState(null), true);
2162 	}
2163 
2164 	/// Run all tests for the current checkout (like rebuild).
2165 	void test()
2166 	{
2167 		auto componentNames = config.build.components.getEnabledComponentNames();
2168 		log("Testing components %-(%s, %)".format(componentNames));
2169 
2170 		this.components = null;
2171 		this.submoduleState = SubmoduleState(null);
2172 		this.incrementalBuild = true;
2173 
2174 		foreach (componentName; componentNames)
2175 			getComponent(componentName).test();
2176 	}
2177 
2178 	bool isCached(SubmoduleState submoduleState)
2179 	{
2180 		this.components = null;
2181 		this.submoduleState = submoduleState;
2182 
2183 		needCacheEngine();
2184 		foreach (componentName; config.build.components.getEnabledComponentNames())
2185 			if (!cacheEngine.haveEntry(getComponent(componentName).getBuildID()))
2186 				return false;
2187 		return true;
2188 	}
2189 
2190 	/// Returns the isCached state for all commits in the history of the given ref.
2191 	bool[string] getCacheState(string[string][string] history)
2192 	{
2193 		log("Enumerating cache entries...");
2194 		auto cacheEntries = needCacheEngine().getEntries().toSet();
2195 
2196 		this.components = null;
2197 		auto componentNames = config.build.components.getEnabledComponentNames();
2198 		auto components = componentNames.map!(componentName => getComponent(componentName)).array;
2199 		auto requiredSubmodules = components
2200 			.map!(component => chain(component.name.only, component.sourceDependencies, component.dependencies))
2201 			.joiner
2202 			.map!(componentName => getComponent(componentName).submoduleName)
2203 			.array.sort().uniq().array
2204 		;
2205 
2206 		log("Collating cache state...");
2207 		bool[string] result;
2208 		foreach (commit, submoduleCommits; history)
2209 		{
2210 			import ae.utils.meta : I;
2211 			this.submoduleState.submoduleCommits = submoduleCommits;
2212 			result[commit] =
2213 				requiredSubmodules.all!(submoduleName => submoduleName in submoduleCommits) &&
2214 				componentNames.all!(componentName =>
2215 					getComponent(componentName).I!(component =>
2216 						component.getBuildID() in cacheEntries
2217 					)
2218 				);
2219 		}
2220 		return result;
2221 	}
2222 
2223 	/// ditto
2224 	bool[string] getCacheState(string[] refs)
2225 	{
2226 		auto history = getMetaRepo().getSubmoduleHistory(refs);
2227 		return getCacheState(history);
2228 	}
2229 
2230 	// **************************** Dependencies *****************************
2231 
2232 	private void needInstaller()
2233 	{
2234 		Installer.logger = &log;
2235 		Installer.installationDirectory = dlDir;
2236 	}
2237 
2238 	/// Pull in a built DMD as configured.
2239 	/// Note that this function invalidates the current repository state.
2240 	void needDMD(ref Environment env, string dmdVer)
2241 	{
2242 		tempError++; scope(success) tempError--;
2243 
2244 		// User setting overrides autodetection
2245 		if (config.build.components.dmd.bootstrap.ver)
2246 			dmdVer = config.build.components.dmd.bootstrap.ver;
2247 
2248 		if (config.build.components.dmd.bootstrap.fromSource)
2249 		{
2250 			log("Bootstrapping DMD " ~ dmdVer);
2251 
2252 			auto bootstrapBuildConfig = config.build.components.dmd.bootstrap.build;
2253 
2254 			// Back up and clear component state
2255 			enum backupTemplate = q{
2256 				auto VARBackup = this.VAR;
2257 				this.VAR = typeof(VAR).init;
2258 				scope(exit) this.VAR = VARBackup;
2259 			};
2260 			mixin(backupTemplate.replace(q{VAR}, q{components}));
2261 			mixin(backupTemplate.replace(q{VAR}, q{config}));
2262 			mixin(backupTemplate.replace(q{VAR}, q{submoduleState}));
2263 
2264 			config.local = configBackup.local;
2265 			if (bootstrapBuildConfig)
2266 				config.build = *bootstrapBuildConfig;
2267 
2268 			// Disable building rdmd in the bootstrap compiler by default
2269 			if ("rdmd" !in config.build.components.enable)
2270 				config.build.components.enable["rdmd"] = false;
2271 
2272 			build(parseSpec(dmdVer));
2273 
2274 			log("Built bootstrap DMD " ~ dmdVer ~ " successfully.");
2275 
2276 			auto bootstrapDir = buildPath(config.local.workDir, "bootstrap");
2277 			if (bootstrapDir.exists)
2278 				bootstrapDir.removeRecurse();
2279 			ensurePathExists(bootstrapDir);
2280 			rename(buildDir, bootstrapDir);
2281 
2282 			env.deps.hostDC = buildPath(bootstrapDir, "bin", "dmd" ~ binExt);
2283 		}
2284 		else
2285 		{
2286 			import std.ascii;
2287 			log("Preparing DMD " ~ dmdVer);
2288 			enforce(dmdVer.startsWith("v"), "Invalid DMD version spec for binary bootstrap. Did you forget to " ~
2289 				((dmdVer.length && dmdVer[0].isDigit && dmdVer.contains('.')) ? "add a leading 'v'" : "enable fromSource") ~ "?");
2290 			needInstaller();
2291 			auto dmdInstaller = new DMDInstaller(dmdVer[1..$]);
2292 			dmdInstaller.requireLocal(false);
2293 			env.deps.hostDC = dmdInstaller.exePath("dmd").absolutePath();
2294 		}
2295 
2296 		log("hostDC=" ~ env.deps.hostDC);
2297 	}
2298 
2299 	void needKindleGen(ref Environment env)
2300 	{
2301 		needInstaller();
2302 		kindleGenInstaller.requireLocal(false);
2303 		env.vars["PATH"] = kindleGenInstaller.directory ~ pathSeparator ~ env.vars["PATH"];
2304 	}
2305 
2306 	version (Windows)
2307 	void needMSYS(ref Environment env)
2308 	{
2309 		needInstaller();
2310 		MSYS.msysCORE.requireLocal(false);
2311 		MSYS.libintl.requireLocal(false);
2312 		MSYS.libiconv.requireLocal(false);
2313 		MSYS.libtermcap.requireLocal(false);
2314 		MSYS.libregex.requireLocal(false);
2315 		MSYS.coreutils.requireLocal(false);
2316 		MSYS.bash.requireLocal(false);
2317 		MSYS.make.requireLocal(false);
2318 		MSYS.grep.requireLocal(false);
2319 		MSYS.sed.requireLocal(false);
2320 		MSYS.diffutils.requireLocal(false);
2321 		env.vars["PATH"] = MSYS.bash.directory.buildPath("bin") ~ pathSeparator ~ env.vars["PATH"];
2322 	}
2323 
2324 	/// Get DMD unbuildable extras
2325 	/// (proprietary DigitalMars utilities, 32-bit import libraries)
2326 	string needExtras()
2327 	{
2328 		import ae.utils.meta : I, singleton;
2329 
2330 		static class DExtrasInstaller : Installer
2331 		{
2332 			@property override string name() { return "dmd-localextras"; }
2333 			string url = "http://semitwist.com/download/app/dmd-localextras.7z";
2334 
2335 			override void installImpl(string target)
2336 			{
2337 				url
2338 					.I!save()
2339 					.I!unpackTo(target);
2340 			}
2341 
2342 			static this()
2343 			{
2344 				urlDigests["http://semitwist.com/download/app/dmd-localextras.7z"] = "ef367c2d25d4f19f45ade56ab6991c726b07d3d9";
2345 			}
2346 		}
2347 
2348 		alias extrasInstaller = singleton!DExtrasInstaller;
2349 
2350 		needInstaller();
2351 		extrasInstaller.requireLocal(false);
2352 		return extrasInstaller.directory;
2353 	}
2354 
2355 	/// Get libcurl for Windows (DLL and import libraries)
2356 	version (Windows)
2357 	string needCurl()
2358 	{
2359 		import ae.utils.meta : I, singleton;
2360 
2361 		static class DCurlInstaller : Installer
2362 		{
2363 			@property override string name() { return "libcurl-" ~ curlVersion; }
2364 			string curlVersion = "7.47.1";
2365 			@property string url() { return "http://downloads.dlang.org/other/libcurl-" ~ curlVersion ~ "-WinSSL-zlib-x86-x64.zip"; }
2366 
2367 			override void installImpl(string target)
2368 			{
2369 				url
2370 					.I!save()
2371 					.I!unpackTo(target);
2372 			}
2373 
2374 			static this()
2375 			{
2376 				urlDigests["http://downloads.dlang.org/other/libcurl-7.47.1-WinSSL-zlib-x86-x64.zip"] = "4b8a7bb237efab25a96588093ae51994c821e097";
2377 			}
2378 		}
2379 
2380 		alias curlInstaller = singleton!DCurlInstaller;
2381 
2382 		needInstaller();
2383 		curlInstaller.requireLocal(false);
2384 		return curlInstaller.directory;
2385 	}
2386 
2387 	version (Windows)
2388 	void needDMC(ref Environment env, string ver = null)
2389 	{
2390 		tempError++; scope(success) tempError--;
2391 
2392 		needInstaller();
2393 
2394 		auto dmc = ver ? new LegacyDMCInstaller(ver) : dmcInstaller;
2395 		if (!dmc.installedLocally)
2396 			log("Preparing DigitalMars C++ " ~ ver);
2397 		dmc.requireLocal(false);
2398 		env.deps.dmcDir = dmc.directory;
2399 
2400 		auto binPath = buildPath(env.deps.dmcDir, `bin`).absolutePath();
2401 		log("DMC=" ~ binPath);
2402 		env.vars["DMC"] = binPath;
2403 		env.vars["PATH"] = binPath ~ pathSeparator ~ env.vars.get("PATH", null);
2404 	}
2405 
2406 	version (Windows)
2407 	auto getVSInstaller()
2408 	{
2409 		needInstaller();
2410 		return vs2013community;
2411 	}
2412 
2413 	version (Windows)
2414 	static string msvcModelStr(string model, string str32, string str64)
2415 	{
2416 		switch (model)
2417 		{
2418 			case "32":
2419 				throw new Exception("Shouldn't need VC for 32-bit builds");
2420 			case "64":
2421 				return str64;
2422 			case "32mscoff":
2423 				return str32;
2424 			default:
2425 				throw new Exception("Unknown model: " ~ model);
2426 		}
2427 	}
2428 
2429 	version (Windows)
2430 	static string msvcModelDir(string model, string dir64 = "x86_amd64")
2431 	{
2432 		return msvcModelStr(model, null, dir64);
2433 	}
2434 
2435 	version (Windows)
2436 	void needVC(ref Environment env, string model)
2437 	{
2438 		tempError++; scope(success) tempError--;
2439 
2440 		auto vs = getVSInstaller();
2441 
2442 		// At minimum, we want the C compiler (cl.exe) and linker (link.exe).
2443 		vs["vc_compilercore86"].requireLocal(false); // Contains both x86 and x86_amd64 cl.exe
2444 		vs["vc_compilercore86res"].requireLocal(false); // Contains clui.dll needed by cl.exe
2445 
2446 		// Include files. Needed when using VS to build either DMD or Druntime.
2447 		vs["vc_librarycore86"].requireLocal(false); // Contains include files, e.g. errno.h needed by Druntime
2448 
2449 		// C runtime. Needed for all programs built with VC.
2450 		vs[msvcModelStr(model, "vc_libraryDesktop_x86", "vc_libraryDesktop_x64")].requireLocal(false); // libcmt.lib
2451 
2452 		// XP-compatible import libraries.
2453 		vs["win_xpsupport"].requireLocal(false); // shell32.lib
2454 
2455 		// MSBuild, for the useVC option
2456 		if (config.build.components.dmd.useVC)
2457 			vs["Msi_BuildTools_MSBuild_x86"].requireLocal(false); // msbuild.exe
2458 
2459 		env.deps.vsDir  = vs.directory.buildPath("Program Files (x86)", "Microsoft Visual Studio 12.0").absolutePath();
2460 		env.deps.sdkDir = vs.directory.buildPath("Program Files", "Microsoft SDKs", "Windows", "v7.1A").absolutePath();
2461 
2462 		env.vars["PATH"] ~= pathSeparator ~ vs.modelBinPaths(msvcModelDir(model)).map!(path => vs.directory.buildPath(path).absolutePath()).join(pathSeparator);
2463 		env.vars["VisualStudioVersion"] = "12"; // Work-around for problem fixed in dmd 38da6c2258c0ff073b0e86e0a1f6ba190f061e5e
2464 		env.vars["VSINSTALLDIR"] = env.deps.vsDir ~ dirSeparator; // ditto
2465 		env.vars["VCINSTALLDIR"] = env.deps.vsDir.buildPath("VC") ~ dirSeparator;
2466 		env.vars["INCLUDE"] = env.deps.vsDir.buildPath("VC", "include") ~ ";" ~ env.deps.sdkDir.buildPath("Include");
2467 		env.vars["LIB"] = env.deps.vsDir.buildPath("VC", "lib", msvcModelDir(model, "amd64")) ~ ";" ~ env.deps.sdkDir.buildPath("Lib", msvcModelDir(model, "x64"));
2468 		env.vars["WindowsSdkDir"] = env.deps.sdkDir ~ dirSeparator;
2469 		env.vars["Platform"] = "x64";
2470 		env.vars["LINKCMD64"] = env.deps.vsDir.buildPath("VC", "bin", msvcModelDir(model), "link.exe"); // Used by dmd
2471 		env.vars["MSVC_CC"] = env.deps.vsDir.buildPath("VC", "bin", msvcModelDir(model), "cl.exe"); // For the msvc-dmc wrapper
2472 		env.vars["MSVC_AR"] = env.deps.vsDir.buildPath("VC", "bin", msvcModelDir(model), "lib.exe"); // For the msvc-lib wrapper
2473 		env.vars["CL"] = "-D_USING_V110_SDK71_"; // Work around __userHeader macro redifinition VS bug
2474 	}
2475 
2476 	private void needGit()
2477 	{
2478 		tempError++; scope(success) tempError--;
2479 
2480 		needInstaller();
2481 		gitInstaller.require();
2482 	}
2483 
2484 	/// Disable the "<program> has stopped working"
2485 	/// standard Windows dialog.
2486 	version (Windows)
2487 	static void disableCrashDialog()
2488 	{
2489 		enum : uint { SEM_FAILCRITICALERRORS = 1, SEM_NOGPFAULTERRORBOX = 2 }
2490 		SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
2491 	}
2492 
2493 	/// Create a build environment base.
2494 	protected @property Environment baseEnvironment()
2495 	{
2496 		Environment env;
2497 
2498 		// Build a new environment from scratch, to avoid tainting the build with the current environment.
2499 		string[] newPaths;
2500 
2501 		version (Windows)
2502 		{
2503 			import std.utf;
2504 			import ae.sys.windows.imports;
2505 			mixin(importWin32!q{winbase});
2506 			mixin(importWin32!q{winnt});
2507 
2508 			TCHAR[1024] buf;
2509 			// Needed for DLLs
2510 			auto winDir = buf[0..GetWindowsDirectory(buf.ptr, buf.length)].toUTF8();
2511 			auto sysDir = buf[0..GetSystemDirectory (buf.ptr, buf.length)].toUTF8();
2512 			newPaths ~= [sysDir, winDir];
2513 		}
2514 		else
2515 		{
2516 			// Needed for coreutils, make, gcc, git etc.
2517 			newPaths = ["/bin", "/usr/bin"];
2518 
2519 			version (linux)
2520 			{
2521 				// GCC wrappers
2522 				ensureDirExists(binDir);
2523 				newPaths = binDir ~ newPaths;
2524 			}
2525 		}
2526 
2527 		env.vars["PATH"] = newPaths.join(pathSeparator);
2528 
2529 		ensureDirExists(tmpDir);
2530 		env.vars["TMPDIR"] = env.vars["TEMP"] = env.vars["TMP"] = tmpDir;
2531 
2532 		version (Windows)
2533 		{
2534 			env.vars["SystemDrive"] = winDir.driveName;
2535 			env.vars["SystemRoot"] = winDir;
2536 		}
2537 
2538 		ensureDirExists(homeDir);
2539 		env.vars["HOME"] = homeDir;
2540 
2541 		return env;
2542 	}
2543 
2544 	/// Apply user modifications onto an environment.
2545 	/// Supports Windows-style %VAR% expansions.
2546 	static string[string] applyEnv(in string[string] target, in string[string] source)
2547 	{
2548 		// The source of variable expansions is variables in the target environment,
2549 		// if they exist, and the host environment otherwise, so e.g.
2550 		// `PATH=C:\...;%PATH%` and `MAKE=%MAKE%` work as expected.
2551 		auto oldEnv = std.process.environment.toAA();
2552 		foreach (name, value; target)
2553 			oldEnv[name] = value;
2554 
2555 		string[string] result;
2556 		foreach (name, value; target)
2557 			result[name] = value;
2558 		foreach (name, value; source)
2559 		{
2560 			string newValue = value;
2561 			foreach (oldName, oldValue; oldEnv)
2562 				newValue = newValue.replace("%" ~ oldName ~ "%", oldValue);
2563 			result[name] = oldEnv[name] = newValue;
2564 		}
2565 		return result;
2566 	}
2567 
2568 	// ******************************** Cache ********************************
2569 
2570 	enum unbuildableMarker = "unbuildable";
2571 
2572 	DCache cacheEngine;
2573 
2574 	DCache needCacheEngine()
2575 	{
2576 		if (!cacheEngine)
2577 		{
2578 			if (cacheEngineName == "git")
2579 				needGit();
2580 			cacheEngine = createCache(cacheEngineName, cacheEngineDir(cacheEngineName), this);
2581 		}
2582 		return cacheEngine;
2583 	}
2584 
2585 	void cp(string src, string dst)
2586 	{
2587 		needCacheEngine().cp(src, dst);
2588 	}
2589 
2590 	private string[] getComponentKeyOrder(string componentName)
2591 	{
2592 		auto submodule = getComponent(componentName).submodule;
2593 		submodule.needRepo();
2594 		return submodule
2595 			.git.query("log", "--pretty=format:%H", "--all", "--topo-order")
2596 			.splitLines()
2597 			.map!(commit => componentName ~ "-" ~ commit ~ "-")
2598 			.array
2599 		;
2600 	}
2601 
2602 	string componentNameFromKey(string key)
2603 	{
2604 		auto parts = key.split("-");
2605 		return parts[0..$-2].join("-");
2606 	}
2607 
2608 	string[][] getKeyOrder(string key)
2609 	{
2610 		if (key !is null)
2611 			return [getComponentKeyOrder(componentNameFromKey(key))];
2612 		else
2613 			return allComponents.map!(componentName => getComponentKeyOrder(componentName)).array;
2614 	}
2615 
2616 	/// Optimize entire cache.
2617 	void optimizeCache()
2618 	{
2619 		needCacheEngine().optimize();
2620 	}
2621 
2622 	bool shouldPurge(string key)
2623 	{
2624 		auto files = cacheEngine.listFiles(key);
2625 		if (files.canFind(unbuildableMarker))
2626 			return true;
2627 
2628 		if (componentNameFromKey(key) == "druntime")
2629 		{
2630 			if (!files.canFind("import/core/memory.d")
2631 			 && !files.canFind("import/core/memory.di"))
2632 				return true;
2633 		}
2634 
2635 		return false;
2636 	}
2637 
2638 	/// Delete cached "unbuildable" build results.
2639 	void purgeUnbuildable()
2640 	{
2641 		needCacheEngine()
2642 			.getEntries
2643 			.filter!(key => shouldPurge(key))
2644 			.each!((key)
2645 			{
2646 				log("Deleting: " ~ key);
2647 				cacheEngine.remove(key);
2648 			})
2649 		;
2650 	}
2651 
2652 	/// Move cached files from one cache engine to another.
2653 	void migrateCache(string sourceEngineName, string targetEngineName)
2654 	{
2655 		auto sourceEngine = createCache(sourceEngineName, cacheEngineDir(sourceEngineName), this);
2656 		auto targetEngine = createCache(targetEngineName, cacheEngineDir(targetEngineName), this);
2657 		auto tempDir = buildPath(config.local.workDir, "temp");
2658 		if (tempDir.exists)
2659 			tempDir.removeRecurse();
2660 		log("Enumerating source entries...");
2661 		auto sourceEntries = sourceEngine.getEntries();
2662 		log("Enumerating target entries...");
2663 		auto targetEntries = targetEngine.getEntries().sort();
2664 		foreach (key; sourceEntries)
2665 			if (!targetEntries.canFind(key))
2666 			{
2667 				log(key);
2668 				sourceEngine.extract(key, tempDir, fn => true);
2669 				targetEngine.add(key, tempDir);
2670 				if (tempDir.exists)
2671 					tempDir.removeRecurse();
2672 			}
2673 		targetEngine.optimize();
2674 	}
2675 
2676 	// **************************** Miscellaneous ****************************
2677 
2678 	struct LogEntry
2679 	{
2680 		string hash;
2681 		string[] message;
2682 		SysTime time;
2683 	}
2684 
2685 	/// Gets the D merge log (newest first).
2686 	LogEntry[] getLog(string refName = "refs/remotes/origin/master")
2687 	{
2688 		getMetaRepo().needRepo();
2689 		auto history = getMetaRepo().git.getHistory();
2690 		LogEntry[] logs;
2691 		auto master = history.commits[history.refs[refName]];
2692 		for (auto c = master; c; c = c.parents.length ? c.parents[0] : null)
2693 		{
2694 			auto time = SysTime(c.time.unixTimeToStdTime);
2695 			logs ~= LogEntry(c.hash.toString(), c.message, time);
2696 		}
2697 		return logs;
2698 	}
2699 
2700 	// ***************************** Integration *****************************
2701 
2702 	/// Override to add logging.
2703 	void log(string line)
2704 	{
2705 	}
2706 
2707 	/// Bootstrap description resolution.
2708 	/// See DMD.Config.Bootstrap.spec.
2709 	/// This is essentially a hack to allow the entire
2710 	/// Config structure to be parsed from an .ini file.
2711 	SubmoduleState parseSpec(string spec)
2712 	{
2713 		getMetaRepo().needRepo();
2714 		auto rev = getMetaRepo().getRef("refs/tags/" ~ spec);
2715 		log("Resolved " ~ spec ~ " to " ~ rev);
2716 		return begin(rev);
2717 	}
2718 
2719 	/// Override this method with one which returns a command,
2720 	/// which will invoke the unmergeRebaseEdit function below,
2721 	/// passing to it any additional parameters.
2722 	/// Note: Currently unused. Was previously used
2723 	/// for unmerging things using interactive rebase.
2724 	abstract string getCallbackCommand();
2725 
2726 	void callback(string[] args) { assert(false); }
2727 }