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