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