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