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