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