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 }