1 /* 2 Copyright (c) 2013-2014 Timur Gafarov 3 4 Boost Software License - Version 1.0 - August 17th, 2003 5 6 Permission is hereby granted, free of charge, to any person or organization 7 obtaining a copy of the software and accompanying documentation covered by 8 this license (the "Software") to use, reproduce, display, distribute, 9 execute, and transmit the Software, and to prepare derivative works of the 10 Software, and to permit third-parties to whom the Software is furnished to 11 do so, all subject to the following: 12 13 The copyright notices in the Software and this entire statement, including 14 the above license grant, this restriction and the following disclaimer, 15 must be included in all copies of the Software, in whole or in part, and 16 all derivative works of the Software, unless such copies or derivative 17 works are solely in the form of machine-executable object code generated by 18 a source language processor. 19 20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 DEALINGS IN THE SOFTWARE. 27 */ 28 29 module session; 30 31 import std.stdio; 32 import std.path; 33 import std.file; 34 import std..string; 35 import std.datetime; 36 import std.conv; 37 import std.digest.crc; 38 import std.process: executeShell; 39 import std.algorithm; 40 static import std.process; 41 import core.stdc.stdlib; 42 43 import cmdopt; 44 import conf; 45 import lexer; 46 import dmodule; 47 import project; 48 import exdep; 49 50 class BuildSession 51 { 52 CmdOptions ops; 53 Config config; 54 string[] versionIds; 55 string[] debugIds; 56 string[] externals; 57 bool[string] scanned; 58 59 // Quit at any time without throwing an exception 60 void quit(int code, string message = "") 61 { 62 if (message.length > 0) 63 writefln("Cook session failed: %s", message); 64 65 if (ops._debug_) 66 printConfig(); 67 68 version(Windows) 69 executeShell("pause"); 70 core.stdc.stdlib.exit(code); 71 } 72 73 void printConfig() 74 { 75 writeln("Configuration:"); 76 string[] confContents; 77 foreach(i, v; config.data) 78 { 79 string f = formatPattern(v, config, '%'); 80 confContents ~= std..string.format(" %s: %s", i, f); 81 } 82 confContents.sort; 83 foreach(i, v; confContents) 84 writeln(v); 85 } 86 87 this(CmdOptions cmdops, Config conf) 88 { 89 ops = cmdops; 90 config = conf; 91 prepare(); 92 } 93 94 void prepare() 95 { 96 // Set default configuration keys 97 string tempTarget = "main"; 98 config.set("profile", "default", false); 99 config.set("prephase", "", false); 100 config.set("postphase", "", false); 101 config.set("source.language", "d", false); 102 config.set("source.ext", ".d", false); 103 config.set("compiler", "dmd", false); 104 config.set("linker", "dmd", false); 105 version(Windows) config.set("librarian", "lib", false); 106 version(linux) config.set("librarian", "dmd", false); 107 config.set("cflags", "", false); 108 config.set("lflags", "", false); 109 //config.set("modules", ""); // TODO? 110 config.set("obj.path", "", false); 111 config.set("obj.path.use", "true", false); 112 config.set("obj.ext", "", false); 113 config.set("modules.main", "main", false); 114 config.set("modules.forced", "", false); 115 config.set("target", "", false); 116 config.set("rc", "", false); 117 config.set("modules.cache", "main.cache", false); 118 119 version(Windows) 120 { 121 config.set("obj.path", "o_windows/"); 122 config.set("obj.ext", ".obj"); 123 } 124 version(linux) 125 { 126 config.set("obj.path", "o_linux/"); 127 config.set("obj.ext", ".o"); 128 config.append("lflags", "-L-rpath -L\".\" -L-ldl "); 129 } 130 131 config.set("modules.maindir", "."); 132 133 if (ops.targets.length) 134 { 135 // FIXME: currently we use only first given target 136 string mainModule = ops.targets[0]; 137 config.set("modules.main", mainModule); 138 139 // strip extension, if any 140 if (extension(config.get("modules.main")) == ".d") 141 config.set("modules.main", config.get("modules.main")[0..$-2]); 142 143 string maindir = dirName(config.get("modules.main")); 144 config.set("modules.maindir", maindir); 145 } 146 147 // Set cache filename 148 string cacheFilePostfix = "-" ~ config.get("profile"); 149 version(Windows) 150 { 151 cacheFilePostfix ~= "-windows.cache"; 152 } 153 version(linux) 154 { 155 cacheFilePostfix ~= "-linux.cache"; 156 } 157 tempTarget = baseName(config.get("modules.main")); 158 config.set("modules.cache", config.get("modules.main") ~ cacheFilePostfix); 159 160 // Set responce file name 161 string rspFilePostfix = "-" ~ config.get("profile"); 162 version(Windows) 163 { 164 rspFilePostfix ~= "-windows.rsp"; 165 } 166 version(linux) 167 { 168 rspFilePostfix ~= "-linux.rsp"; 169 } 170 config.set("modules.rsp", config.get("modules.main") ~ rspFilePostfix); 171 172 config.set("project.compile", "%compiler% %cflags% -c %source% -of%object%", false); 173 if (ops.rsp) 174 config.set("project.link", "%linker% %lflags% -of%target% %packages% @" ~ config.get("modules.rsp"), false); 175 else 176 config.set("project.link", "%linker% %lflags% -of%target% %objects% %packages%", false); 177 178 if (ops.rsp) 179 { 180 version(Windows) config.set("project.linklib", "%librarian% %lflags% -c -p32 -of%target% @" ~ config.get("modules.rsp"), false); 181 version(linux) config.set("project.linklib", "%librarian% %lflags% -lib -of%target% @" ~ config.get("modules.rsp"), false); 182 version(Windows) config.set("project.linkpkg", "%librarian% %lflags% -c -p32 -of%package% @" ~ config.get("modules.rsp"), false); 183 version(linux) config.set("project.linkpkg", "%librarian% %lflags% -lib -of%package% @" ~ config.get("modules.rsp"), false); 184 } 185 else 186 { 187 version(Windows) config.set("project.linklib", "%librarian% %lflags% -c -p32 -of%target% %objects%", false); 188 version(linux) config.set("project.linklib", "%librarian% %lflags% -lib -of%target% %objects%", false); 189 version(Windows) config.set("project.linkpkg", "%librarian% %lflags% -c -p32 -of%package% %objects%", false); 190 version(linux) config.set("project.linkpkg", "%librarian% %lflags% -lib -of%package% %objects%", false); 191 } 192 193 version(linux) config.set("project.run", "./%target%", false); 194 version(Windows) config.set("project.run", "%target%", false); 195 config.set("project.packages", "", false); 196 197 if (ops.output.length) 198 { 199 tempTarget = ops.output; 200 } 201 202 if (ops.clean) 203 { 204 if (exists(config.get("modules.cache"))) 205 std.file.remove(config.get("modules.cache")); 206 if (exists(config.get("modules.rsp"))) 207 std.file.remove(config.get("modules.rsp")); 208 // FIXME: use config for these 209 if (exists("o_linux")) rmdirRecurse("o_linux"); 210 if (exists("o_windows")) rmdirRecurse("o_windows"); 211 quit(0); 212 } 213 214 if (ops.cache.length) 215 config.set("modules.cache", ops.cache); 216 217 if (ops.rc.length) 218 config.set("rc", ops.rc); 219 220 if (ops.cflags.length) 221 config.append("cflags", ops.cflags ~ " "); 222 223 if (ops.lflags.length) 224 config.append("lflags", ops.lflags ~ " "); 225 226 if ("./" ~ tempTarget != ops.program) 227 config.set("target", tempTarget); 228 else 229 quit(1, "illegal target name: \"" ~ tempTarget ~ "\" (conflicts with Cook executable)"); 230 231 version(Windows) 232 { 233 if (ops.noconsole) 234 config.append("lflags", "-L/exet:nt/su:windows "); 235 } 236 237 // Read user-wide default configuration 238 string userConfigFilename = home(".cook/default.conf", ops); 239 if (exists(userConfigFilename)) 240 { 241 readConfiguration(config, userConfigFilename); 242 } 243 244 // Read default configuration 245 string defaultConfigFilename = "default.conf"; 246 if (exists(defaultConfigFilename)) 247 { 248 readConfiguration(config, defaultConfigFilename); 249 } 250 251 // Read project configuration 252 if (ops.conf.length) 253 { 254 if (exists(ops.conf)) 255 readConfiguration(config, ops.conf); 256 } 257 else 258 { 259 string configFilename = "./" ~ config.get("target") ~ ".conf"; 260 if (exists(configFilename)) 261 readConfiguration(config, configFilename); 262 } 263 264 config.append("cflags", " -I%modules.maindir% "); 265 266 if (ops.release) 267 config.append("cflags", " -release -O -inline -noboundscheck "); 268 } 269 270 void build(Project proj) 271 { 272 string externalsStr = config.get("project.external"); 273 if (externalsStr.length) 274 externals = split(externalsStr); 275 276 setVersionIds(proj); 277 278 if (ops.rebuild) 279 { 280 writeln("Do you really want to rebuild the project? (Y/N)"); 281 bool rebuild = false; 282 bool confirmed = false; 283 while(!confirmed) 284 { 285 string input = readln(); 286 if (input.length) 287 { 288 if (input[0] == 'Y' || input[0] == 'y') 289 { 290 rebuild = true; 291 confirmed = true; 292 } 293 else if (input[0] == 'N' || input[0] == 'n') 294 { 295 confirmed = true; 296 } 297 } 298 299 if (!confirmed) 300 writeln("Please, write Y or N"); 301 } 302 303 if (!rebuild) 304 ops.rebuild = false; 305 } 306 307 if (!ops.rebuild) 308 readCache(proj); 309 310 scanProjectHierarchy(proj); 311 traceBackwardDependencies(proj); 312 addForcedModules(proj); 313 writeCache(proj); 314 315 if (ops.dump.length) 316 { 317 if (ops.dump in proj.modules) 318 { 319 auto m = proj.modules[ops.dump]; 320 writefln("Filename: %s", m.filename); 321 writefln("Package name: %s", m.packageName); 322 writefln("Last modified: %s", m.lastModified); 323 writefln("Imports: %s", m.importedFiles); 324 writefln("Version ids: %s", m.versionIds); 325 writefln("Debug ids: %s", m.debugIds); 326 } 327 } 328 329 doPrephase(); 330 compileAndLink(proj); 331 strip(); 332 run(); 333 if (ops._debug_) 334 printConfig(); 335 } 336 337 void setVersionIds(Project proj) 338 { 339 versionIds = split(config.get("version")); 340 debugIds = split(config.get("debug")); 341 342 foreach(v; versionIds) 343 { 344 proj.versionIds[v] = 1; 345 if (v == "Wine") 346 proj.versionIds["Windows"] = 1; 347 } 348 349 foreach(v; debugIds) 350 proj.debugIds[v] = 1; 351 352 string ver; 353 foreach(i, v; versionIds) 354 { 355 ver ~= " -version=" ~ v; 356 } 357 if (ver.length) 358 config.append("cflags", ver); 359 360 string deb; 361 foreach(i, v; debugIds) 362 { 363 deb ~= " -debug=" ~ v; 364 } 365 if (deb.length) 366 config.append("cflags", deb); 367 } 368 369 void readCache(Project proj) 370 { 371 string cacheFile = config.get("modules.cache"); 372 if (exists(cacheFile) && !ops.nocache) 373 { 374 string cache = readText(cacheFile); 375 foreach (line; splitLines(cache)) 376 { 377 auto tokens = split(line); 378 auto m = proj.addModule(tokens[0]); 379 m.lastModified = SysTime.fromISOExtString(tokens[1]); 380 m.globalFile = cast(bool)to!uint(tokens[2]); 381 foreach (i; tokens[3..$]) 382 m.addImportFile(i); 383 } 384 } 385 } 386 387 void scanProjectHierarchy(Project proj) 388 { 389 proj.mainModuleFilename = config.get("modules.main") ~ config.get("source.ext"); 390 if (exists(proj.mainModuleFilename)) 391 { 392 scanDependencies(proj, proj.mainModuleFilename); 393 } 394 else 395 quit(1, "no main module found"); 396 397 if (proj.modules.length == 0) 398 quit(1, "no source files found"); 399 } 400 401 void scanDependencies(Project proj, string fileName, bool globalFile = false) 402 { 403 scanned[fileName] = true; 404 405 DModule m; 406 407 if (!exists(fileName)) 408 return; 409 410 // if it is a new module 411 if (!(fileName in proj.modules)) 412 { 413 if (!ops.quiet) writefln("Analyzing \"%s\"...", fileName); 414 m = proj.addModule(fileName); 415 m.globalFile = globalFile; 416 m.lastModified = timeLastModified(fileName); 417 if (!m.buildDependencyList()) 418 quit(1, "cannot build proper dependency list"); 419 m.packageName = pathToModule(dirName(fileName)); 420 scanModule(proj, m); 421 } 422 else // if we already have it 423 { 424 m = proj.modules[fileName]; 425 426 //TODO: cache this 427 m.packageName = pathToModule(dirName(fileName)); 428 429 immutable auto lm = timeLastModified(fileName); 430 if (lm > m.lastModified) 431 { 432 if (!ops.quiet) writefln("Analyzing \"%s\"...", fileName); 433 m.lastModified = lm; 434 if (!m.buildDependencyList()) 435 quit(1, "cannot build proper dependency list"); 436 m.forceRebuild = true; 437 scanModule(proj, m); 438 } 439 } 440 441 foreach (mName; m.importedFiles) 442 if (!(mName in scanned)) 443 { 444 scanDependencies(proj, mName); 445 } 446 } 447 448 bool existsInExternals( 449 string filename, 450 ref string externalFilename) 451 { 452 foreach(e; externals) 453 { 454 string f = e ~ "/" ~ filename; 455 if (exists(f)) 456 { 457 externalFilename = f; 458 return true; 459 } 460 } 461 return false; 462 } 463 464 void scanModule(Project proj, DModule m) 465 { 466 foreach(importedFile; m.importedFiles) 467 { 468 string subprojectFile = importedFile; 469 470 if (config.get("modules.maindir") != ".") 471 subprojectFile = 472 config.get("modules.maindir") ~ "/" ~ importedFile; 473 474 string externalFile; 475 476 if (exists(importedFile)) 477 scanDependencies(proj, importedFile); 478 else if (exists(subprojectFile)) 479 scanDependencies(proj, subprojectFile); 480 else if (existsInExternals(subprojectFile, externalFile)) 481 scanDependencies(proj, externalFile, true); 482 else 483 { 484 // Treat it as package import (<importedFile>/package.d) 485 string pkgFile = 486 stripExtension(importedFile) ~ "/" 487 ~ moduleToPath("package", config.get("source.ext")); 488 489 string subprojectPkgFile = 490 config.get("modules.maindir") ~ "/" 491 ~ pkgFile; 492 493 if (exists(pkgFile)) 494 scanDependencies(proj, pkgFile); 495 else if (exists(subprojectPkgFile)) 496 scanDependencies(proj, subprojectPkgFile); 497 else if (existsInExternals(pkgFile, externalFile)) 498 scanDependencies(proj, externalFile, true); 499 } 500 } 501 } 502 503 void traceBackwardDependencies(Project proj) 504 { 505 if (!ops.nobacktrace) 506 { 507 foreach(modulei, modulev; proj.modules) 508 { 509 foreach (mName; modulev.importedFiles) 510 { 511 if (mName in proj.modules) 512 { 513 auto imModule = proj.modules[mName]; 514 imModule.backdeps[modulei] = modulev; 515 } 516 } 517 } 518 foreach(mName, m; proj.modules) 519 { 520 foreach(i, v; m.backdeps) 521 { 522 v.forceRebuild = v.forceRebuild || m.forceRebuild; 523 } 524 } 525 } 526 } 527 528 void addForcedModules(Project proj) 529 { 530 foreach(fileName; split(config.get("modules.forced"))) 531 { 532 if (exists(fileName)) 533 { 534 DModule m = proj.addModule(fileName); 535 m.lastModified = timeLastModified(fileName); 536 537 if (!m.buildDependencyList()) 538 quit(1, "cannot build proper dependency list"); 539 540 foreach(importedFile; m.importedFiles) 541 { 542 if (exists(importedFile)) 543 scanDependencies(proj, importedFile); 544 } 545 } 546 } 547 } 548 549 void doPrephase() 550 { 551 uint retcode; 552 string prephase = formatPattern(config.get("prephase"), config, '%'); 553 if (prephase != "") 554 { 555 if (!ops.quiet) 556 writeln(prephase); 557 if (!ops.emulate) 558 { 559 retcode = executeShell(prephase).status; 560 if (retcode) 561 quit(1, "Prephase error"); 562 } 563 } 564 } 565 566 void writeCache(Project proj) 567 { 568 string cache; 569 foreach (i, m; proj.modules) 570 { 571 //TODO: cache module's package also 572 cache ~= i ~ " " ~ m.toString() ~ "\n"; 573 } 574 575 if (!ops.emulate) 576 if (!ops.nocache) 577 std.file.write(config.get("modules.cache"), cache); 578 } 579 580 void writeResponceFile(string rsp) 581 { 582 if (!ops.emulate) 583 if (ops.rsp) 584 std.file.write(config.get("modules.rsp"), rsp); 585 } 586 587 void compileAndLink(Project proj) 588 { 589 string[] pkgList = split(config.get("project.packages")); 590 591 string linkList; 592 593 // Compile modules 594 if (config.get("obj.path") != "") 595 if (!exists(config.get("obj.path"))) 596 mkdir(config.get("obj.path")); 597 bool terminate = false; 598 foreach (i, v; proj.modules) 599 { 600 if (!terminate && exists(i)) 601 { 602 string targetObjectName = i; 603 string tobjext = extension(targetObjectName); 604 targetObjectName = targetObjectName[0..$-tobjext.length] ~ config.get("obj.ext"); 605 string targetObject; 606 if (v.globalFile) 607 { 608 string hash = crc32Of(targetObjectName).crcHexString; 609 targetObject = home(format(".cook/obj/%s/%s%s", 610 config.get("profile"), 611 hash, 612 config.get("obj.ext")), ops); 613 } 614 else 615 targetObject = config.get("obj.path") ~ "/" ~ targetObjectName; 616 617 if ((timeLastModified(i) > timeLastModified(targetObject, SysTime.min)) 618 || v.forceRebuild 619 || ops.rebuild) 620 { 621 if (config.get("obj.path.use") == "false") 622 targetObject = targetObjectName; 623 624 config.set("source", i); 625 config.set("object", targetObject); 626 string command = formatPattern(config.get("project.compile"), config, '%'); 627 if (!ops.quiet) 628 writeln(command); 629 if (!ops.emulate) 630 { 631 immutable auto retcode = executeShell(command).status; 632 if (retcode) 633 terminate = true; 634 } 635 } 636 637 if (config.get("obj.path.use") == "false") 638 targetObject = targetObjectName; 639 640 if (!matches(v.packageName, pkgList)) 641 linkList ~= targetObject ~ " "; 642 } 643 } 644 645 // If compilation error occured 646 if (terminate) 647 { 648 quit(1, "compilation error"); 649 } 650 651 // Compile resource file, if any 652 version(Windows) 653 { 654 if (config.get("rc").length > 0) 655 { 656 string res = stripExtension(config.get("rc")) ~ ".res "; 657 string command = "windres -i " ~ config.get("rc") ~ " -o " ~ res ~ "-O res"; 658 if (!ops.quiet) 659 writeln(command); 660 if (!ops.emulate) 661 { 662 immutable auto retcode = executeShell(command).status; 663 if (retcode) 664 quit(1); 665 } 666 config.append("lflags", res); 667 } 668 } 669 670 // Link packages, if any 671 // WARNING: alpha stage, needs work! 672 // TODO: do not relink a package, if it is unchanged 673 // TODO: do not create a package, if it is empty 674 string pkgLibList; 675 foreach(pkg; pkgList) 676 { 677 string pkgLinkList; 678 foreach(i, m; proj.modules) 679 { 680 if (m.packageName == pkg) 681 { 682 string targetObjectName = i; 683 string tobjext = extension(targetObjectName); 684 targetObjectName = targetObjectName[0..$-tobjext.length] ~ config.get("obj.ext"); 685 686 string targetObject = config.get("obj.path") ~ targetObjectName; 687 pkgLinkList ~= targetObject ~ " "; 688 } 689 } 690 691 //TODO: pkgext should be a configuration key 692 string pkgExt; 693 version(Windows) pkgExt = ".lib"; 694 version(linux) pkgExt = ".a"; 695 696 config.set("objects", pkgLinkList); 697 config.set("package", pkg ~ pkgExt); 698 string command = formatPattern(config.get("project.linkpkg"), config, '%'); 699 700 writeResponceFile(pkgLinkList); 701 702 if (!ops.quiet) 703 writeln(command); 704 if (!ops.emulate) 705 { 706 immutable auto retcode = executeShell(command).status; 707 if (retcode) 708 quit(1, "package linking error"); 709 } 710 711 pkgLibList ~= config.get("package") ~ " "; 712 } 713 714 // Link 715 config.set("objects", linkList); 716 config.set("packages", pkgLibList); 717 if (ops.lib) 718 { 719 version(Windows) config.append("target", ".lib"); 720 version(linux) config.append("target", ".a"); 721 722 writeResponceFile(linkList); 723 724 string command = formatPattern(config.get("project.linklib"), config, '%'); 725 if (!ops.quiet) 726 writeln(command); 727 if (!ops.emulate) 728 { 729 immutable auto retcode = executeShell(command).status; 730 if (retcode) 731 quit(1, "linking error"); 732 } 733 } 734 else 735 { 736 version(Windows) config.append("target", ".exe"); 737 738 writeResponceFile(linkList); 739 740 string command = formatPattern(config.get("project.link"), config, '%'); 741 if (!ops.quiet) 742 writeln(command); 743 if (!ops.emulate) 744 { 745 immutable auto retcode = executeShell(command).status; 746 if (retcode) 747 quit(1, "linking error"); 748 } 749 } 750 } 751 752 void strip() 753 { 754 if (ops.strip) 755 { 756 version(linux) 757 { 758 string command = "strip " ~ config.get("target"); 759 if (!ops.quiet) 760 writeln(command); 761 if (!ops.emulate) 762 std.process.system(command); 763 } 764 } 765 } 766 767 void run() 768 { 769 if (ops.run && !ops.lib) 770 { 771 if (!ops.emulate) 772 { 773 string command = formatPattern(config.get("project.run"), config, '%'); 774 if (!ops.quiet) 775 writeln(command); 776 if (!ops.emulate) 777 executeShell(command); 778 } 779 } 780 } 781 } 782