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