rgdc.rc: ignore whole "/opt" and "/usr"
[rgdc.git] / rgdc.d
blobf2ece3d54f219f8ca897ba590650f9a6be72ea05
1 /*
2 * Copyright (C) 2008 by Andrei Alexandrescu
3 * Written by Andrei Alexandrescu, www.erdani.org
4 * Based on an idea by Georg Wrede
5 * Featuring improvements suggested by Christopher Wright
6 * Windows port using bug fixes and suggestions by Adam Ruppe
8 * Code uglification: Ketmar // Invisible Vector
10 * Distributed under the Boost Software License, Version 1.0.
11 * (See accompanying file LICENSE_1_0.txt or copy at
12 * http://www.boost.org/LICENSE_1_0.txt)
14 // -fversion=no_pragma_lib to disable non-standard -fwrite-pragma-lib arg
16 import
17 std.c.stdlib,
18 std.algorithm,
19 std.array,
20 std.datetime,
21 std.digest.md,
22 std.exception,
23 std.file,
24 std.getopt,
25 std.parallelism,
26 std.path,
27 std.process,
28 std.range,
29 std.regex,
30 std.stdio,
31 std.string,
32 std.typetuple;
35 private enum objExt = ".o";
36 private enum binExt = "";
37 private enum libExt = ".a";
38 private enum altDirSeparator = "";
40 private bool chatty, buildOnly, dryRun, force;
41 private string exe;
44 private string compilerBinary = "gdc";
45 private string[] compilerExtraFlags = ["-pipe"];
47 private bool optDynamic = false;
48 private bool optShowCommands = false;
51 ////////////////////////////////////////////////////////////////////////////////
52 private void yap(size_t line=__LINE__, T...) (auto ref T stuff) {
53 if (!chatty) return;
54 debug stderr.writeln(line, ": ", stuff);
55 else stderr.writeln(stuff);
59 ////////////////////////////////////////////////////////////////////////////////
60 private string[] includeDirs; // -I
61 private string[] importDirs; // -J
62 private bool[string] systemDirs; // to ignore
65 private string expandString (string s) {
66 auto pos = s.indexOf("$(");
67 if (pos >= 0) {
68 string res;
69 while (pos >= 0) {
70 res ~= s[0..pos];
71 s = s[pos+2..$];
72 auto epos = s.indexOf(')');
73 if (epos < 1) throw new Exception("invalid envvar name");
74 auto v = environment.get(s[0..epos], "");
75 res ~= v;
76 s = s[epos+1..$];
77 pos = s.indexOf("$(");
79 res ~= s;
80 return res;
82 return s;
86 private void readRC (string fname) {
87 auto reader = File(fname);
88 scope(exit) collectException(reader.close()); // don't care for errors
89 foreach (string line; lines(reader)) {
90 if (line.startsWith("include=")) {
91 auto val = expandString(line[8..$].strip);
92 val = buildNormalizedPath(val);
93 if (val.length > 0) {
94 val = "-I"~val;
95 if (!includeDirs.canFind(val)) includeDirs ~= val;
97 } else if (line.startsWith("import=")) {
98 auto val = expandString(line[7..$].strip);
99 val = buildNormalizedPath(val);
100 if (val.length > 0) {
101 val = "-J"~val;
102 if (!importDirs.canFind(val)) importDirs ~= val;
104 } else if (line.startsWith("systemdir=")) {
105 auto val = expandString(line[10..$].strip);
106 val = buildNormalizedPath(val);
107 if (val.length > 0) systemDirs[val] = true;
113 ////////////////////////////////////////////////////////////////////////////////
114 private File lockFile;
116 private void lockWorkPath (string workPath) {
117 string lockFileName = buildPath(workPath, "rgdc.lock");
118 if (!dryRun) lockFile.open(lockFileName, "w");
119 yap("lock ", lockFile.name);
120 if (!dryRun) lockFile.lock();
124 private void unlockWorkPath () {
125 yap("unlock ", lockFile.name);
126 if (!dryRun) {
127 lockFile.unlock();
128 lockFile.close();
133 ////////////////////////////////////////////////////////////////////////////////
134 // Run a program optionally writing the command line first
135 // If "replace" is true and the OS supports it, replace the current process.
136 private int run (string[] args, string output=null, bool replace=false) {
137 import std.conv;
138 yap(replace ? "exec " : "spawn ", args.text);
139 if (optShowCommands) stderr.writeln("run: ", args.join(" ")); //TODO: proper quoting
140 if (dryRun) return 0;
141 if (replace && !output) {
142 import std.c.process;
143 auto argv = args.map!toStringz.chain(null.only).array;
144 return execv(argv[0], argv.ptr);
146 File outputFile;
147 if (output) {
148 outputFile = File(output, "wb");
149 } else {
150 outputFile = stdout;
152 auto process = spawnProcess(args, stdin, outputFile);
153 return process.wait();
157 private int runSilent (string[] args) {
158 import std.conv;
159 yap("spawn ", args.text);
160 if (optShowCommands) stderr.writeln("run: ", args.join(" ")); //TODO: proper quoting
161 if (dryRun) return 0;
162 auto process = spawnProcess(args);
163 return process.wait();
167 private int exec (string[] args) {
168 return run(args, null, true);
172 ////////////////////////////////////////////////////////////////////////////////
173 private @property string myOwnTmpDir () {
174 import core.sys.posix.unistd;
175 auto tmpRoot = format("/tmp/.rgdc-%d", getuid());
176 yap("mkdirRecurse ", tmpRoot);
177 if (!dryRun) mkdirRecurse(tmpRoot);
178 return tmpRoot;
182 private string getWorkPath (in string root, in string[] compilerFlags) {
183 static string workPath;
184 if (workPath) return workPath;
185 enum string[] irrelevantSwitches = ["--help", "-ignore", "-quiet", "-v"];
186 MD5 context;
187 context.start();
188 context.put(getcwd().representation);
189 context.put(root.representation);
190 foreach (flag; compilerFlags) {
191 if (irrelevantSwitches.canFind(flag)) continue;
192 context.put(flag.representation);
194 auto digest = context.finish();
195 string hash = toHexString(digest);
196 const tmpRoot = myOwnTmpDir;
197 workPath = buildPath(tmpRoot, "rgdc-"~baseName(root)~'-'~hash);
198 yap("mkdirRecurse ", workPath);
199 if (!dryRun) mkdirRecurse(workPath);
200 return workPath;
204 ////////////////////////////////////////////////////////////////////////////////
205 // Rebuild the executable fullExe starting from modules in myDeps
206 // passing the compiler flags compilerFlags. Generates one large
207 // object file.
208 private int rebuild (string root, string fullExe,
209 string workDir, string objDir, in string[string] myDeps,
210 string[] compilerFlags, bool addStubMain)
212 // Delete the old executable before we start building.
213 yap("stat ", fullExe);
214 if (!dryRun && exists(fullExe)) {
215 yap("rm ", fullExe);
216 try {
217 remove(fullExe);
218 } catch (FileException e) {
219 // This can occur on Windows if the executable is locked.
220 // Although we can't delete the file, we can still rename it.
221 auto oldExe = "%s.%s-%s.old".format(fullExe, Clock.currTime.stdTime, thisProcessID);
222 yap("mv ", fullExe, " ", oldExe);
223 rename(fullExe, oldExe);
226 auto fullExeTemp = fullExe~".tmp";
228 string[] buildTodo () {
229 string outExe = (fullExeTemp[0] != '/' ? objDir~"/"~fullExeTemp : fullExeTemp);
230 auto todo =
231 compilerFlags~
232 ["-o", outExe]~
233 ["-I"~dirName(root)]~
234 ["-J"~dirName(root)]~
235 includeDirs~
236 importDirs/*~
237 [root]*/;
238 foreach (k, objectFile; myDeps) if (objectFile !is null) todo ~= [k];
239 // Need to add void main(){}?
240 if (addStubMain) {
241 auto stubMain = buildPath(myOwnTmpDir, "stubmain.d");
242 std.file.write(stubMain, "void main(){}");
243 todo ~= [stubMain];
245 return todo;
248 auto todo = buildTodo();
249 auto commandLength = escapeShellCommand(todo).length;
250 string[] eflags2;
251 if (optDynamic) {
252 eflags2 ~= "-fPIC";
253 } else {
254 eflags2 ~= "-static-libphobos";
256 // get list of pragma(lib)s
257 string[] prlibs;
258 version(no_pragma_lib) {} else {
259 string prfname = objDir~"/"~fullExeTemp.baseName~".link";
260 //writeln(prfname);
261 collectException(remove(prfname));
262 string[] args =
263 [compilerBinary]~
264 compilerExtraFlags~
265 ["-c", "-o", "/dev/null"]~
266 ["-fwrite-pragma-libs="~prfname]~
267 todo;
268 auto af = args.filter!(a => !a.startsWith("-O") && !a.startsWith("-finline-"));
269 args.length = 0;
270 while (!af.empty) {
271 auto s = af.front;
272 af.popFront();
273 if (s == "-o") {
274 s = af.front;
275 af.popFront();
276 if (s == "/dev/null") args ~= ["-o", "/dev/null"];
277 } else {
278 args ~= s;
281 //args = array(args.filter!(a => !a.startsWith("-O") && !a.startsWith("-finline-")));
282 //writeln(" *** ", args);
283 immutable r1 = run(args);
284 try {
285 auto reader = File(prfname, "r");
286 foreach (string line; lines(reader)) {
287 auto s = line.strip;
288 if (!prlibs.canFind(s)) prlibs ~= s.idup;
290 } catch (Exception) {}
291 if (prlibs.length) {
292 try {
293 auto fo = File(prfname, "w");
294 foreach (s; prlibs) fo.writeln(s);
295 } catch (Exception) {
296 prfname = null;
298 } else {
299 prfname = null;
301 if (prfname.length > 0) {
302 prlibs = ["-Wl,@"~prfname];
303 } else {
304 prlibs.length = 0;
307 if (commandLength+compilerBinary.length+compilerExtraFlags.join(" ").length+eflags2.join(" ").length+
308 prlibs.join(" ").length > 32700) throw new Exception("SHIT!");
309 immutable result = run([compilerBinary]~compilerExtraFlags~eflags2~todo~prlibs);
310 if (result) {
311 // build failed
312 if (exists(fullExeTemp)) remove(fullExeTemp);
313 return result;
315 // clean up the dir containing the object file, just not in dry
316 // run mode because we haven't created any!
317 if (!dryRun) {
318 yap("stat ", objDir);
319 if (objDir.exists && objDir.startsWith(workDir)) {
320 yap("rmdirRecurse ", objDir);
321 // We swallow the exception because of a potential race: two
322 // concurrently-running scripts may attempt to remove this
323 // directory. One will fail.
324 collectException(rmdirRecurse(objDir));
326 yap("mv ", fullExeTemp, " ", fullExe);
327 rename(fullExeTemp, fullExe);
329 return 0;
333 ////////////////////////////////////////////////////////////////////////////////
334 // we don't need std exclusions anymore, we have 'systemdir' key in .rc file for that
335 //private string[] exclusions = ["std", "etc", "core", "tango"]; // packages that are to be excluded
336 private string[] exclusions = []; // packages that are to be excluded
339 // Given module rootModule, returns a mapping of all dependees .d
340 // source filenames to their corresponding .o files sitting in
341 // directory workDir. The mapping is obtained by running dmd -v against
342 // rootModule.
343 private string[string] getDependencies (string rootModule, string workDir, string objDir, string[] compilerFlags) {
344 immutable depsFilename = buildPath(workDir, "rgdc.deps");
346 string[string] readDepsFile (/*string depsFilename, string objDir="."*/) {
348 bool[string] sysDirCache; // cache all modules that are in systemdirs
350 bool inALibrary (string source, string object) {
351 if (object.endsWith(".di") || source == "object" || source == "gcstats") return true;
352 foreach (string exclusion; exclusions) if (source.startsWith(exclusion~'.')) return true;
353 return false;
356 bool isInSystemDir (string path) {
357 path = buildNormalizedPath(path);
358 if (path in sysDirCache) return true;
359 //writeln("isInSystemDir: path=", path, "; dirs=", systemDirs);
360 foreach (auto k; systemDirs.byKey) {
361 if (path.length > k.length && path.startsWith(k) && path[k.length] == '/') {
362 sysDirCache[path.idup] = true;
363 //writeln("in system dir: ", path);
364 return true;
367 return false;
370 string d2obj (string dfile) { return buildPath(objDir, dfile.baseName.chomp(".d")~objExt); }
372 string findLib (string libName) {
373 // This can't be 100% precise without knowing exactly where the linker
374 // will look for libraries (which requires, but is not limited to,
375 // parsing the linker's command line (as specified in dmd.conf/sc.ini).
376 // Go for best-effort instead.
377 string[] dirs = ["."];
378 foreach (envVar; ["LIB", "LIBRARY_PATH", "LD_LIBRARY_PATH"]) dirs ~= environment.get(envVar, "").split(pathSeparator);
379 string[] names = ["lib"~libName~".so", "lib"~libName~".a"];
380 dirs ~= ["/lib", "/usr/lib", "/usr/local/lib"];
381 foreach (dir; dirs) {
382 foreach (name; names) {
383 auto path = buildPath(dir, name);
384 if (path.exists) {
385 if (path.extension == ".so") return "-l"~path.baseName.stripExtension[3..$];
386 return absolutePath(path);
390 return null;
393 yap("read ", depsFilename);
394 auto depsReader = File(depsFilename);
395 scope(exit) collectException(depsReader.close()); // don't care for errors
396 // Fetch all dependencies and append them to myDeps
397 static auto modInfoRE = ctRegex!(r"^(.+?)\s+\(([^)]+)\)");
398 //std.stdio (/opt/gdc/include/d/4.9.1/std/stdio.d) : private : object (/opt/gdc/include/d/4.9.1/object.di)
399 static auto modImportRE = ctRegex!(r"^(.+?)\s+\(([^)]+)\)\s+:\s+string\s+:\s+[^(]+\(([^)]+)\)");
400 //hello (hello.d) : string : zm (/mnt/tigerclaw/D/prj/rgdc/rgdc_native/zm)
401 string[string] result;
402 foreach (string line; lines(depsReader)) {
403 string modName, modPath, filePath;
404 auto modImportMatch = match(line, modImportRE);
405 if (modImportMatch.empty) {
406 auto modInfoMatch = match(line, modInfoRE);
407 if (modInfoMatch.empty) continue;
408 auto modInfoCaps = modInfoMatch.captures;
409 // [1]: module name
410 // [2]: module path
411 modName = modInfoCaps[1];
412 modPath = modInfoCaps[2];
413 } else {
414 auto modImportCaps = modImportMatch.captures;
415 // [1]: module name
416 // [2]: module path
417 // [3]: file path
418 modName = modImportCaps[1];
419 modPath = modImportCaps[2];
420 filePath = modImportCaps[3];
422 if (filePath.length && !isInSystemDir(filePath)) result[filePath] = null;
423 //if (inALibrary(modName, modPath) || isInSystemDir(modPath)) continue;
424 if (isInSystemDir(modPath)) continue;
425 result[modPath] = d2obj(modPath);
427 return result;
430 // Check if the old dependency file is fine
431 if (!force) {
432 yap("stat ", depsFilename);
433 if (exists(depsFilename)) {
434 // See if the deps file is still in good shape
435 auto deps = readDepsFile();
436 auto allDeps = chain(rootModule.only, deps.byKey).array;
437 bool mustRebuildDeps = allDeps.anyNewerThan(timeLastModified(depsFilename));
438 if (!mustRebuildDeps) return deps; // Cool, we're in good shape
439 // Fall through to rebuilding the deps file
443 immutable rootDir = dirName(rootModule);
445 // Collect dependencies
446 auto depsGetter =
447 // "cd "~shellQuote(rootDir)~" && "
448 [compilerBinary]~compilerExtraFlags~compilerFlags~
450 "-c",
451 "-o", "/dev/null",
452 "-fdeps="~depsFilename,
453 rootModule,
454 "-I"~rootDir,
455 "-J"~rootDir
456 ]~includeDirs~importDirs;
458 scope(failure) {
459 // Delete the deps file on failure, we don't want to be fooled
460 // by it next time we try
461 collectException(std.file.remove(depsFilename));
464 immutable depsExitCode = runSilent(depsGetter);
465 if (depsExitCode) {
466 stderr.writefln("Failed: %s", depsGetter);
467 collectException(std.file.remove(depsFilename));
468 exit(depsExitCode);
471 return (dryRun ? null : readDepsFile());
475 ////////////////////////////////////////////////////////////////////////////////
476 // Is any file newer than the given file?
477 private bool anyNewerThan (in string[] files, in string file) {
478 yap("stat ", file);
479 return files.anyNewerThan(file.timeLastModified);
483 // Is any file newer than the given file?
484 private bool anyNewerThan (in string[] files, SysTime t) {
485 // Experimental: running newerThan in separate threads, one per file
486 if (false) {
487 foreach (source; files) if (source.newerThan(t)) return true;
488 return false;
489 } else {
490 bool result;
491 foreach (source; taskPool.parallel(files)) if (!result && source.newerThan(t)) result = true;
492 return result;
498 * If force is true, returns true. Otherwise, if source and target both
499 * exist, returns true iff source's timeLastModified is strictly greater
500 * than target's. Otherwise, returns true.
502 private bool newerThan (string source, string target) {
503 if (force) return true;
504 yap("stat ", target);
505 return source.newerThan(timeLastModified(target, SysTime(0)));
509 private bool newerThan (string source, SysTime target) {
510 if (force) return true;
511 try {
512 yap("stat ", source);
513 return DirEntry(source).timeLastModified > target;
514 } catch (Exception) {
515 // File not there, consider it newer
516 return true;
521 ////////////////////////////////////////////////////////////////////////////////
522 private @property string helpString () {
523 return
524 "rgdc build "~thisVersion~"
525 Usage: rgdc [RDMD AND DMD OPTIONS]... program [PROGRAM OPTIONS]...
526 Builds (with dependents) and runs a D program.
527 Example: rgdc -release myprog --myprogparm 5
529 Any option to be passed to the compiler must occur before the program name. In
530 addition to compiler options, rgdc recognizes the following options:
531 --build-only just build the executable, don't run it
532 --chatty write compiler commands to stdout before executing them
533 --compiler=comp use the specified compiler
534 --dry-run do not compile, just show what commands would be run (implies --chatty)
535 --exclude=package exclude a package from the build (multiple --exclude allowed)
536 --force force a rebuild even if apparently not necessary
537 --help this message
538 --main add a stub main program to the mix (e.g. for unittesting)
539 --shebang rgdc is in a shebang line (put as first argument)
544 ////////////////////////////////////////////////////////////////////////////////
545 private @property string thisVersion () {
546 enum d = __DATE__;
547 enum month = d[0..3],
548 day = d[4] == ' ' ? "0"~d[5] : d[4..6],
549 year = d[7..$];
550 enum monthNum
551 = month == "Jan" ? "01"
552 : month == "Feb" ? "02"
553 : month == "Mar" ? "03"
554 : month == "Apr" ? "04"
555 : month == "May" ? "05"
556 : month == "Jun" ? "06"
557 : month == "Jul" ? "07"
558 : month == "Aug" ? "08"
559 : month == "Sep" ? "09"
560 : month == "Oct" ? "10"
561 : month == "Nov" ? "11"
562 : month == "Dec" ? "12"
563 : "";
564 static assert(month != "", "Unknown month "~month);
565 return year[0]~year[1..$]~monthNum~day;
569 private string which (string path) {
570 yap("which ", path);
571 if (path.canFind(dirSeparator) || altDirSeparator != "" && path.canFind(altDirSeparator)) return path;
572 string[] extensions = [""];
573 foreach (extension; extensions) {
574 foreach (envPath; environment["PATH"].splitter(pathSeparator)) {
575 string absPath = buildPath(envPath, path~extension);
576 yap("stat ", absPath);
577 if (exists(absPath) && isFile(absPath)) return absPath;
580 throw new FileException(path, "File not found in PATH");
584 private size_t indexOfProgram (string[] args) {
585 foreach(i, arg; args[1..$]) {
586 if (!arg.startsWith('-', '@') && !arg.endsWith(".o", ".a")) return i+1;
588 return args.length;
592 int main(string[] args) {
594 string[] convertSomeOptions (string[] args) {
595 string[string] simpleReplace;
596 simpleReplace["-unittest"] = "-funittest";
597 simpleReplace["-main"] = "--main";
598 simpleReplace["-O"] = "-O2";
599 simpleReplace["-boundscheck=off"] = "-fno-bounds-check";
600 simpleReplace["-boundscheck=on"] = "-fbounds-check";
602 debug writeln("before: ", args);
603 string[] res;
604 res ~= args[0];
605 foreach (arg; args[1..$]) {
606 auto sr = arg in simpleReplace;
607 if (sr !is null) { res ~= *sr; continue; }
608 if (arg.startsWith("-debug") ||
609 arg.startsWith("-version")) {
610 res ~= "-f"~arg[1..$];
611 continue;
613 if (arg == "-inline") { res ~= ["-finline-small-functions", "-finline-functions"]; continue; }
614 res ~= arg;
616 if (!res.canFind("-g") && !res.canFind("-s")) res = res~"-s";
617 debug writeln("after: ", res);
618 return res;
621 //writeln("Invoked with: ", args);
622 if (args.length > 1 && args[1].startsWith("--shebang ", "--shebang=")) {
623 // multiple options wrapped in one
624 auto a = args[1]["--shebang ".length..$];
625 args = args[0..1]~std.string.split(a)~args[2..$];
628 // Continue parsing the command line; now get rgdc's own arguments
629 auto programPos = indexOfProgram(args);
630 assert(programPos > 0);
631 auto argsBeforeProgram = convertSomeOptions(args[0..programPos]);
633 bool bailout; // bailout set by functions called in getopt if program should exit
634 bool addStubMain; // set by --main
635 getopt(argsBeforeProgram,
636 std.getopt.config.caseSensitive,
637 std.getopt.config.passThrough,
638 "build-only", &buildOnly,
639 "chatty", &chatty,
640 "compiler", &compilerBinary,
641 "dry-run", &dryRun,
642 "exclude", &exclusions,
643 "force", &force,
644 "help", { writeln(helpString); bailout = true; },
645 "main", &addStubMain,
646 "show-commands", &optShowCommands,
647 "dynamic", { optDynamic = true; },
648 "static", { optDynamic = false; },
650 if (bailout) return 0;
651 if (dryRun) chatty = true; // dry-run implies chatty
653 // no code on command line => require a source file
654 if (programPos == args.length) {
655 write(helpString);
656 return 1;
659 auto
660 root = args[programPos].chomp(".d")~".d",
661 exeBasename = root.baseName(".d"),
662 exeDirname = root.dirName,
663 programArgs = args[programPos+1..$];
665 assert(argsBeforeProgram.length >= 1);
666 auto compilerFlags = argsBeforeProgram[1..$];
668 bool lib = compilerFlags.canFind("-lib");
669 string outExt = (lib ? libExt : binExt);
671 //collectException(readRC(thisExePath.setExtension(".rc")));
672 collectException(readRC("rgdc.rc"));
674 // --build-only implies the user would like a binary in the program's directory
675 if (buildOnly && !exe) exe = exeDirname~dirSeparator;
677 if (exe && exe.endsWith(dirSeparator)) {
678 // user specified a directory, complete it to a file
679 exe = buildPath(exe, exeBasename)~outExt;
682 // Compute the object directory and ensure it exists
683 immutable workDir = getWorkPath(root, compilerFlags);
684 lockWorkPath(workDir); // will be released by the OS on process exit
685 string objDir = buildPath(workDir, "objs");
686 yap("mkdirRecurse ", objDir);
687 if (!dryRun) mkdirRecurse(objDir);
689 if (lib) {
690 // When building libraries, DMD does not generate object files.
691 // Instead, it uses the -od parameter as the location for the library file.
692 // Thus, override objDir (which is normally a temporary directory)
693 // to be the target output directory.
694 objDir = exe.dirName;
697 // Fetch dependencies
698 const myDeps = getDependencies(root, workDir, objDir, compilerFlags);
700 // Compute executable name, check for freshness, rebuild
702 We need to be careful about using -o. Normally the generated
703 executable is hidden in the unique directory workDir. But if the
704 user forces generation in a specific place by using -od or -of,
705 the time of the binary can't be used to check for freshness
706 because the user may change e.g. the compile option from one run
707 to the next, yet the generated binary's datetime stays the
708 same. In those cases, we'll use a dedicated file called ".built"
709 and placed in workDir. Upon a successful build, ".built" will be
710 touched. See also
711 http://d.puremagic.com/issues/show_bug.cgi?id=4814
713 string buildWitness;
714 SysTime lastBuildTime = SysTime.min;
715 if (exe) {
716 // user-specified exe name
717 buildWitness = buildPath(workDir, ".built");
718 if (!exe.newerThan(buildWitness)) {
719 // Both exe and buildWitness exist, and exe is older than
720 // buildWitness. This is the only situation in which we
721 // may NOT need to recompile.
722 lastBuildTime = buildWitness.timeLastModified(SysTime.min);
724 } else {
725 exe = buildPath(workDir, exeBasename)~outExt;
726 buildWitness = exe;
727 lastBuildTime = buildWitness.timeLastModified(SysTime.min);
730 // Have at it
731 if (chain(root.only, myDeps.byKey).array.anyNewerThan(lastBuildTime)) {
732 immutable result = rebuild(root, exe, workDir, objDir, myDeps, compilerFlags, addStubMain);
733 if (result) return result;
734 // Touch the build witness to track the build time
735 if (buildWitness != exe) {
736 yap("touch ", buildWitness);
737 std.file.write(buildWitness, "");
741 if (buildOnly) {
742 // Pretty much done!
743 return 0;
746 // release lock on workDir before launching the user's program
747 unlockWorkPath();
749 // run
750 return exec(exe~programArgs);