1 module install; 2 3 import std.algorithm; 4 import std.array; 5 import std.exception; 6 import std.file; 7 import std.path; 8 import std..string; 9 10 import ae.sys.file; 11 import ae.utils.array; 12 import ae.utils.json; 13 14 import common; 15 import config : config; 16 import custom; 17 18 static import std.process; 19 20 alias copy = std.file.copy; // https://issues.dlang.org/show_bug.cgi?id=14817 21 22 version (Windows) 23 { 24 enum string binExt = ".exe"; 25 enum string dmdConfigName = "sc.ini"; 26 } 27 else 28 { 29 enum string binExt = ""; 30 enum string dmdConfigName = "dmd.conf"; 31 } 32 33 version (Posix) 34 { 35 import core.sys.posix.unistd; 36 import std.conv : octal; 37 } 38 39 version (Windows) 40 enum string platformDir = "windows"; 41 else 42 version (linux) 43 enum string platformDir = "linux"; 44 else 45 version (OSX) 46 enum string platformDir = "osx"; 47 else 48 version (FreeBSD) 49 enum string platformDir = "freebsd"; 50 else 51 enum string platformDir = null; 52 53 string[] findDMD() 54 { 55 string[] result; 56 foreach (pathEntry; std.process.environment.get("PATH", null).split(pathSeparator)) 57 { 58 auto dmd = pathEntry.buildPath("dmd" ~ binExt); 59 if (dmd.exists) 60 result ~= dmd; 61 } 62 return result; 63 } 64 65 string selectInstallPath(string location) 66 { 67 string[] candidates; 68 if (location) 69 { 70 auto dmd = location.absolutePath(); 71 @property bool ok() { return dmd.exists && dmd.isFile; } 72 73 if (!ok) 74 { 75 string newDir; 76 77 bool dirOK(string dir) 78 { 79 newDir = dmd.buildPath(dir); 80 return newDir.exists && newDir.isDir; 81 } 82 83 bool tryDir(string dir) 84 { 85 if (dirOK(dir)) 86 { 87 dmd = newDir; 88 return true; 89 } 90 return false; 91 } 92 93 tryDir("dmd2"); 94 95 static if (platformDir) 96 tryDir(platformDir); 97 98 enforce(!dirOK("bin32") || !dirOK("bin64"), 99 "Ambiguous model in path - please specify full path to DMD binary"); 100 tryDir("bin") || tryDir("bin32") || tryDir("bin64"); 101 102 dmd = dmd.buildPath("dmd" ~ binExt); 103 enforce(ok, "DMD installation not detected at " ~ location); 104 } 105 106 candidates = [dmd]; 107 } 108 else 109 { 110 candidates = findDMD(); 111 enforce(candidates.length, "DMD not found in PATH - " ~ 112 "add DMD to PATH or specify install location explicitly"); 113 } 114 115 foreach (candidate; candidates) 116 { 117 if (candidate.buildNormalizedPath.startsWith(config.local.workDir.buildNormalizedPath)) 118 { 119 log("Skipping DMD installation under Digger workDir (" ~ config.local.workDir ~ "): " ~ candidate); 120 continue; 121 } 122 123 log("Found DMD executable: " ~ candidate); 124 return candidate; 125 } 126 127 throw new Exception("No suitable DMD installation found."); 128 } 129 130 string findConfig(string dmdPath) 131 { 132 string configPath; 133 134 bool pathOK(string path) 135 { 136 configPath = path.buildPath(dmdConfigName); 137 return configPath.exists; 138 } 139 140 if (pathOK(dmdPath.dirName)) 141 return configPath; 142 143 auto home = std.process.environment.get("HOME", null); 144 if (home && pathOK(home)) 145 return configPath; 146 147 version (Posix) 148 if (pathOK("/etc/")) 149 return configPath; 150 151 throw new Exception("Can't find DMD configuration file %s " ~ 152 "corresponding to DMD located at %s".format(dmdConfigName, dmdPath)); 153 } 154 155 struct ComponentPaths 156 { 157 string binPath; 158 string libPath; 159 string phobosPath; 160 string druntimePath; 161 } 162 163 string commonModel(ref BuildInfo buildInfo) 164 { 165 enforce(buildInfo.config.components.common.models.length == 1, "Multi-model install is not yet supported"); 166 return buildInfo.config.components.common.models[0]; 167 } 168 169 ComponentPaths parseConfig(string dmdPath, BuildInfo buildInfo) 170 { 171 auto configPath = findConfig(dmdPath); 172 log("Found DMD configuration: " ~ configPath); 173 174 string[string] vars = std.process.environment.toAA(); 175 bool parsing = false; 176 foreach (line; configPath.readText().splitLines()) 177 { 178 if (line.startsWith("[") && line.endsWith("]")) 179 { 180 auto sectionName = line[1..$-1]; 181 parsing = sectionName == "Environment" 182 || sectionName == "Environment" ~ buildInfo.commonModel; 183 } 184 else 185 if (parsing && line.canFind("=")) 186 { 187 string name, value; 188 list(name, null, value) = line.findSplit("="); 189 auto parts = value.split("%"); 190 for (size_t n = 1; n < parts.length; n+=2) 191 if (!parts[n].length) 192 parts[n] = "%"; 193 else 194 if (parts[n] == "@P") 195 parts[n] = configPath.dirName(); 196 else 197 parts[n] = vars.get(parts[n], parts[n]); 198 value = parts.join(); 199 vars[name] = value; 200 } 201 } 202 203 string[] parseParameters(string s, char escape = '\\', char separator = ' ') 204 { 205 string[] result; 206 while (s.length) 207 if (s[0] == separator) 208 s = s[1..$]; 209 else 210 { 211 string p; 212 if (s[0] == '"') 213 { 214 s = s[1..$]; 215 bool escaping, end; 216 while (s.length && !end) 217 { 218 auto c = s[0]; 219 s = s[1..$]; 220 if (!escaping && c == '"') 221 end = true; 222 else 223 if (!escaping && c == escape) 224 escaping = true; 225 else 226 { 227 if (escaping && c != escape) 228 p ~= escape; 229 p ~= c; 230 escaping = false; 231 } 232 } 233 } 234 else 235 list(p, null, s) = s.findSplit([separator]); 236 result ~= p; 237 } 238 return result; 239 } 240 241 string[] dflags = parseParameters(vars.get("DFLAGS", null)); 242 string[] importPaths = dflags 243 .filter!(s => s.startsWith("-I")) 244 .map!(s => s[2..$].split(";")) 245 .join(); 246 247 version (Windows) 248 string[] libPaths = parseParameters(vars.get("LIB", null), 0, ';'); 249 else 250 string[] libPaths = dflags 251 .filter!(s => s.startsWith("-L-L")) 252 .map!(s => s[4..$]) 253 .array(); 254 255 string findPath(string[] paths, string name, string[] testFiles) 256 { 257 auto results = paths.find!(path => testFiles.any!(testFile => path.buildPath(testFile).exists)); 258 enforce(!results.empty, "Can't find %s (%-(%s or %)). Looked in: %s".format(name, testFiles, paths)); 259 auto result = results.front.buildNormalizedPath(); 260 auto testFile = testFiles.find!(testFile => result.buildPath(testFile).exists).front; 261 log("Found %s (%s): %s".format(name, testFile, result)); 262 return result; 263 } 264 265 ComponentPaths result; 266 result.binPath = dmdPath.dirName(); 267 result.libPath = findPath(libPaths, "Phobos static library", [getLibFileName(buildInfo)]); 268 result.phobosPath = findPath(importPaths, "Phobos source code", ["std/stdio.d"]); 269 result.druntimePath = findPath(importPaths, "Druntime import files", ["object.d", "object.di"]); 270 return result; 271 } 272 273 string getLibFileName(BuildInfo buildInfo) 274 { 275 version (Windows) 276 { 277 auto model = buildInfo.commonModel; 278 return "phobos%s.lib".format(model == "32" ? "" : model); 279 } 280 else 281 return "libphobos2.a"; 282 } 283 284 struct InstalledObject 285 { 286 /// File name in backup directory 287 string name; 288 289 /// Original location. 290 /// Path is relative to uninstall.json's directory. 291 string path; 292 293 /// MD5 sum of the NEW object's contents 294 /// (not the one in the install directory). 295 /// For directories, this is the MD5 sum 296 /// of all files sorted by name (see mdDir). 297 string hash; 298 } 299 300 struct UninstallData 301 { 302 InstalledObject*[] objects; 303 } 304 305 void install(bool yes, bool dryRun, string location = null) 306 { 307 assert(!yes || !dryRun, "Mutually exclusive options"); 308 auto dmdPath = selectInstallPath(location); 309 310 auto buildInfoPath = resultDir.buildPath(buildInfoFileName); 311 enforce(buildInfoPath.exists, 312 buildInfoPath ~ " not found - please purge cache and rebuild"); 313 auto buildInfo = buildInfoPath.readText().jsonParse!BuildInfo(); 314 315 auto componentPaths = parseConfig(dmdPath, buildInfo); 316 317 auto verb = dryRun ? "Would" : "Will"; 318 log("%s install:".format(verb)); 319 log(" - Binaries to: " ~ componentPaths.binPath); 320 log(" - Libraries to: " ~ componentPaths.libPath); 321 log(" - Phobos source code to: " ~ componentPaths.phobosPath); 322 log(" - Druntime includes to: " ~ componentPaths.druntimePath); 323 324 auto uninstallPath = buildPath(componentPaths.binPath, ".digger-install"); 325 auto uninstallFileName = buildPath(uninstallPath, "uninstall.json"); 326 bool updating = uninstallFileName.exists; 327 if (updating) 328 { 329 log("Found previous installation data in " ~ uninstallPath); 330 log("%s update existing installation.".format(verb)); 331 } 332 else 333 { 334 log("This %s be a new Digger installation.".format(verb.toLower)); 335 log("Backups and uninstall data %s be saved in %s".format(verb.toLower, uninstallPath)); 336 } 337 338 auto libFileName = getLibFileName(buildInfo); 339 auto libName = libFileName.stripExtension ~ "-" ~ buildInfo.commonModel ~ libFileName.extension; 340 341 static struct Item 342 { 343 string name, srcPath, dstPath; 344 } 345 346 Item[] items = 347 [ 348 Item("dmd" ~ binExt, buildPath(resultDir, "bin", "dmd" ~ binExt), dmdPath), 349 Item("rdmd" ~ binExt, buildPath(resultDir, "bin", "rdmd" ~ binExt), buildPath(componentPaths.binPath, "rdmd" ~ binExt)), 350 Item(libName , buildPath(resultDir, "lib", libFileName) , buildPath(componentPaths.libPath, libFileName)), 351 Item("object.di" , buildPath(resultDir, "import", "object.{d,di}").globFind, 352 buildPath(componentPaths.druntimePath, "object.{d,di}").globFind), 353 Item("core" , buildPath(resultDir, "import", "core") , buildPath(componentPaths.druntimePath, "core")), 354 Item("std" , buildPath(resultDir, "import", "std") , buildPath(componentPaths.phobosPath, "std")), 355 Item("etc" , buildPath(resultDir, "import", "etc") , buildPath(componentPaths.phobosPath, "etc")), 356 ]; 357 358 InstalledObject*[string] existingComponents; 359 bool[string] updateNeeded; 360 361 UninstallData uninstallData; 362 363 if (updating) 364 { 365 uninstallData = uninstallFileName.readText.jsonParse!UninstallData; 366 foreach (obj; uninstallData.objects) 367 existingComponents[obj.name] = obj; 368 } 369 370 log("Preparing object list..."); 371 372 foreach (item; items) 373 { 374 log(" - " ~ item.name); 375 376 auto obj = new InstalledObject(item.name, item.dstPath.relativePath(uninstallPath), mdObject(item.srcPath)); 377 auto pexistingComponent = item.name in existingComponents; 378 if (pexistingComponent) 379 { 380 auto existingComponent = *pexistingComponent; 381 382 enforce(existingComponent.path == obj.path, 383 "Updated component has a different path (%s vs %s), aborting." 384 .format(existingComponents[item.name].path, obj.path)); 385 386 verifyObject(existingComponent, uninstallPath, "update"); 387 388 updateNeeded[item.name] = existingComponent.hash != obj.hash; 389 existingComponent.hash = obj.hash; 390 } 391 else 392 uninstallData.objects ~= obj; 393 } 394 395 log("Testing write access and filesystem boundaries:"); 396 397 string[] dirs = items.map!(item => item.dstPath.dirName).array.sort().uniq().array; 398 foreach (dir; dirs) 399 { 400 log(" - %s".format(dir)); 401 auto testPathA = dir.buildPath(".digger-test"); 402 auto testPathB = componentPaths.binPath.buildPath(".digger-test2"); 403 404 std.file.write(testPathA, "test"); 405 { 406 scope(failure) remove(testPathA); 407 rename(testPathA, testPathB); 408 } 409 remove(testPathB); 410 } 411 412 version (Posix) 413 { 414 int owner = dmdPath.getOwner(); 415 int group = dmdPath.getGroup(); 416 int mode = items.front.dstPath.getAttributes() & octal!666; 417 log("UID=%d GID=%d Mode=%03o".format(owner, group, mode)); 418 } 419 420 log("Things to do:"); 421 422 foreach (item; items) 423 { 424 enforce(item.srcPath.exists, "Can't find source for component %s: %s".format(item.name, item.srcPath)); 425 enforce(item.dstPath.exists, "Can't find target for component %s: %s".format(item.name, item.dstPath)); 426 427 string action; 428 if (updating && item.name in existingComponents) 429 action = updateNeeded[item.name] ? "Update" : "Skip unchanged"; 430 else 431 action = "Install"; 432 log(" - %s component %s from %s to %s".format(action, item.name, item.srcPath, item.dstPath)); 433 } 434 435 log("You %s be able to undo this action by running `digger uninstall`.".format(verb.toLower)); 436 437 if (dryRun) 438 { 439 log("Dry run, exiting."); 440 return; 441 } 442 443 if (yes) 444 log("Proceeding with installation."); 445 else 446 { 447 import std.stdio : stdin, stderr; 448 449 string result; 450 do 451 { 452 stderr.write("Proceed with installation? [Y/n] "); stderr.flush(); 453 result = stdin.readln().chomp().toLower(); 454 } while (result != "y" && result != "n" && result != ""); 455 if (result == "n") 456 return; 457 } 458 459 enforce(updating || !uninstallPath.exists, "Uninstallation directory exists without uninstall.json: " ~ uninstallPath); 460 461 log("Saving uninstall information..."); 462 463 if (!updating) 464 mkdir(uninstallPath); 465 std.file.write(uninstallFileName, toJson(uninstallData)); 466 467 log("Backing up original files..."); 468 469 foreach (item; items) 470 if (item.name !in existingComponents) 471 { 472 log(" - " ~ item.name); 473 auto backupPath = buildPath(uninstallPath, item.name); 474 rename(item.dstPath, backupPath); 475 } 476 477 if (updating) 478 { 479 log("Cleaning up existing Digger-installed files..."); 480 481 foreach (item; items) 482 if (item.name in existingComponents && updateNeeded[item.name]) 483 { 484 log(" - " ~ item.name); 485 rmObject(item.dstPath); 486 } 487 } 488 489 log("Installing new files..."); 490 491 foreach (item; items) 492 if (item.name !in existingComponents || updateNeeded[item.name]) 493 { 494 log(" - " ~ item.name); 495 atomic!cpObject(item.srcPath, item.dstPath); 496 } 497 498 version (Posix) 499 { 500 log("Applying attributes..."); 501 502 bool isRoot = geteuid()==0; 503 504 foreach (item; items) 505 if (item.name !in existingComponents || updateNeeded[item.name]) 506 { 507 log(" - " ~ item.name); 508 item.dstPath.recursive!setMode(mode); 509 510 if (isRoot) 511 item.dstPath.recursive!setOwner(owner, group); 512 else 513 if (item.dstPath.getOwner() != owner || item.dstPath.getGroup() != group) 514 log("Warning: UID/GID mismatch for " ~ item.dstPath); 515 } 516 } 517 518 log("Install OK."); 519 log("You can undo this action by running `digger uninstall`."); 520 } 521 522 void uninstall(bool dryRun, bool force, string location = null) 523 { 524 string uninstallPath; 525 if (location.canFind(".digger-install")) 526 uninstallPath = location; 527 else 528 { 529 auto dmdPath = selectInstallPath(location); 530 auto binPath = dmdPath.dirName(); 531 uninstallPath = buildPath(binPath, ".digger-install"); 532 } 533 auto uninstallFileName = buildPath(uninstallPath, "uninstall.json"); 534 enforce(uninstallFileName.exists, "Can't find uninstallation data: " ~ uninstallFileName); 535 auto uninstallData = uninstallFileName.readText.jsonParse!UninstallData; 536 537 if (!force) 538 { 539 log("Verifying files to be uninstalled..."); 540 541 foreach (obj; uninstallData.objects) 542 verifyObject(obj, uninstallPath, "uninstall"); 543 544 log("Verify OK."); 545 } 546 547 log(dryRun ? "Actions to run:" : "Uninstalling..."); 548 549 void runAction(void delegate() action) 550 { 551 if (!force) 552 action(); 553 else 554 try 555 action(); 556 catch (Exception e) 557 log("Ignoring error: " ~ e.msg); 558 } 559 560 void uninstallObject(InstalledObject* obj) 561 { 562 auto src = buildNormalizedPath(uninstallPath, obj.name); 563 auto dst = buildNormalizedPath(uninstallPath, obj.path); 564 565 if (!src.exists) // --force 566 { 567 log(" - %s component %s with no backup".format(dryRun ? "Would skip" : "Skipping", obj.name)); 568 return; 569 } 570 571 if (dryRun) 572 { 573 log(" - Would remove " ~ dst); 574 log(" Would move " ~ src ~ " to " ~ dst); 575 } 576 else 577 { 578 log(" - Removing " ~ dst); 579 runAction({ rmObject(dst); }); 580 log(" Moving " ~ src ~ " to " ~ dst); 581 runAction({ rename(src, dst); }); 582 } 583 } 584 585 foreach (obj; uninstallData.objects) 586 runAction({ uninstallObject(obj); }); 587 588 if (dryRun) 589 return; 590 591 remove(uninstallFileName); 592 593 if (!force) 594 rmdir(uninstallPath); // should be empty now 595 else 596 rmdirRecurse(uninstallPath); 597 598 log("Uninstall OK."); 599 } 600 601 string globFind(string path) 602 { 603 auto results = dirEntries(path.dirName, path.baseName, SpanMode.shallow); 604 enforce(!results.empty, "Can't find: " ~ path); 605 auto result = results.front; 606 results.popFront(); 607 enforce(results.empty, "Multiple matches: " ~ path); 608 return result; 609 } 610 611 void verifyObject(InstalledObject* obj, string uninstallPath, string verb) 612 { 613 auto path = buildNormalizedPath(uninstallPath, obj.path); 614 enforce(path.exists, "Can't find item to %s: %s".format(verb, path)); 615 auto hash = mdObject(path); 616 enforce(hash == obj.hash, 617 "Object changed since it was installed: %s\nPlease %s manually.".format(path, verb)); 618 } 619 620 version(Posix) bool attrIsExec(int attr) { return (attr & octal!111) != 0; } 621 622 /// Set access modes while preserving executable bit. 623 version(Posix) 624 void setMode(string fn, int mode) 625 { 626 auto attr = fn.getAttributes(); 627 mode |= attr & ~octal!777; 628 if (attr.attrIsExec || attr.attrIsDir) 629 mode = mode | ((mode & octal!444) >> 2); // executable iff readable 630 fn.setAttributes(mode); 631 } 632 633 /// Apply a function recursively to all files and directories under given path. 634 template recursive(alias fun) 635 { 636 void recursive(Args...)(string fn, auto ref Args args) 637 { 638 fun(fn, args); 639 if (fn.isDir) 640 foreach (de; fn.dirEntries(SpanMode.shallow)) 641 recursive(de.name, args); 642 } 643 } 644 645 void rmObject(string path) { path.isDir ? path.rmdirRecurse() : path.remove(); } 646 647 void cpObject(string src, string dst) 648 { 649 if (src.isDir) 650 { 651 mkdir(dst); 652 dst.setAttributes(src.getAttributes()); 653 foreach (de; src.dirEntries(SpanMode.shallow)) 654 cpObject(de.name, buildPath(dst, de.baseName)); 655 } 656 else 657 { 658 src.copy(dst); 659 dst.setAttributes(src.getAttributes()); 660 } 661 } 662 663 string mdDir(string dir) 664 { 665 import std.stdio : File; 666 import std.digest.md; 667 668 auto dataChunks = dir 669 .dirEntries(SpanMode.breadth) 670 .filter!(de => de.isFile) 671 .map!(de => de.name.replace(`\`, `/`)) 672 .array() 673 .sort() 674 .map!(name => File(name, "rb").byChunk(4096)) 675 .joiner(); 676 677 MD5 digest; 678 digest.start(); 679 foreach (chunk; dataChunks) 680 digest.put(chunk); 681 auto result = digest.finish(); 682 // https://issues.dlang.org/show_bug.cgi?id=9279 683 auto str = result.toHexString(); 684 return str[].idup; 685 } 686 687 string mdObject(string path) 688 { 689 import std.digest.digest; 690 691 if (path.isDir) 692 return path.mdDir(); 693 else 694 { 695 auto result = path.mdFile(); 696 // https://issues.dlang.org/show_bug.cgi?id=9279 697 auto str = result.toHexString(); 698 return str[].idup; 699 } 700 }