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