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