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