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