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