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)
33 private enum objExt
= ".o";
34 private enum binExt
= "";
35 private enum libExt
= ".a";
36 private enum altDirSeparator
= "";
38 private bool chatty
, buildOnly
, dryRun
, force
;
40 private string
[] exclusions
= ["std", "etc", "core", "tango"]; // packages that are to be excluded
43 private string compilerBinary
= "gdc";
44 private string
[] compilerExtraFlags
= ["-pipe"];
46 private bool optDynamic
= false;
47 private bool optShowCommands
= false;
50 ////////////////////////////////////////////////////////////////////////////////
51 private void yap(size_t line
=__LINE__
, T
...) (auto ref T stuff
) {
53 debug stderr
.writeln(line
, ": ", stuff
);
54 else stderr
.writeln(stuff
);
58 ////////////////////////////////////////////////////////////////////////////////
59 private string
[] includeDirs
; // -I
60 private string
[] importDirs
; // -J
63 private string
expandString (string s
) {
64 auto pos
= s
.indexOf("$(");
70 auto epos
= s
.indexOf(')');
71 if (epos
< 1) throw new Exception("invalid envvar name");
72 auto v
= environment
.get(s
[0..epos
], "");
75 pos
= s
.indexOf("$(");
84 private void readRC (string fname
) {
85 auto reader
= File(fname
);
86 scope(exit
) collectException(reader
.close()); // don't care for errors
87 foreach (string line
; lines(reader
)) {
88 if (line
.startsWith("include=")) {
89 auto val
= expandString(line
[8..$].strip
);
92 if (!includeDirs
.canFind(val
)) includeDirs
~= val
;
94 } else if (line
.startsWith("import=")) {
95 auto val
= expandString(line
[7..$].strip
);
98 if (!importDirs
.canFind(val
)) importDirs
~= val
;
105 ////////////////////////////////////////////////////////////////////////////////
106 private File lockFile
;
108 private void lockWorkPath (string workPath
) {
109 string lockFileName
= buildPath(workPath
, "rgdc.lock");
110 if (!dryRun
) lockFile
.open(lockFileName
, "w");
111 yap("lock ", lockFile
.name
);
112 if (!dryRun
) lockFile
.lock();
116 private void unlockWorkPath () {
117 yap("unlock ", lockFile
.name
);
125 ////////////////////////////////////////////////////////////////////////////////
126 // Run a program optionally writing the command line first
127 // If "replace" is true and the OS supports it, replace the current process.
128 private int run (string
[] args
, string output
=null, bool replace
=false) {
130 yap(replace ?
"exec " : "spawn ", args
.text
);
131 if (optShowCommands
) stderr
.writeln("run: ", args
.join(" ")); //TODO: proper quoting
132 if (dryRun
) return 0;
133 if (replace
&& !output
) {
134 import std
.c
.process
;
135 auto argv
= args
.map
!toStringz
.chain(null.only
).array
;
136 return execv(argv
[0], argv
.ptr
);
140 outputFile
= File(output
, "wb");
144 auto process
= spawnProcess(args
, stdin
, outputFile
);
145 return process
.wait();
149 private int runSilent (string
[] args
) {
151 yap("spawn ", args
.text
);
152 if (dryRun
) return 0;
153 auto process
= spawnProcess(args
);
154 return process
.wait();
158 private int exec (string
[] args
) {
159 return run(args
, null, true);
163 ////////////////////////////////////////////////////////////////////////////////
164 private @property string
myOwnTmpDir () {
165 import core
.sys
.posix
.unistd
;
166 auto tmpRoot
= format("/tmp/.rgdc-%d", getuid());
167 yap("mkdirRecurse ", tmpRoot
);
168 if (!dryRun
) mkdirRecurse(tmpRoot
);
173 private string
getWorkPath (in string root
, in string
[] compilerFlags
) {
174 static string workPath
;
175 if (workPath
) return workPath
;
176 enum string
[] irrelevantSwitches
= ["--help", "-ignore", "-quiet", "-v"];
179 context
.put(getcwd().representation
);
180 context
.put(root
.representation
);
181 foreach (flag
; compilerFlags
) {
182 if (irrelevantSwitches
.canFind(flag
)) continue;
183 context
.put(flag
.representation
);
185 auto digest
= context
.finish();
186 string hash
= toHexString(digest
);
187 const tmpRoot
= myOwnTmpDir
;
188 workPath
= buildPath(tmpRoot
, "rgdc-"~baseName(root
)~'-'~hash
);
189 yap("mkdirRecurse ", workPath
);
190 if (!dryRun
) mkdirRecurse(workPath
);
195 ////////////////////////////////////////////////////////////////////////////////
196 // Rebuild the executable fullExe starting from modules in myDeps
197 // passing the compiler flags compilerFlags. Generates one large
199 private int rebuild (string root
, string fullExe
,
200 string workDir
, string objDir
, in string
[string
] myDeps
,
201 string
[] compilerFlags
, bool addStubMain
)
203 // Delete the old executable before we start building.
204 yap("stat ", fullExe
);
205 if (!dryRun
&& exists(fullExe
)) {
209 } catch (FileException e
) {
210 // This can occur on Windows if the executable is locked.
211 // Although we can't delete the file, we can still rename it.
212 auto oldExe
= "%s.%s-%s.old".format(fullExe
, Clock
.currTime
.stdTime
, thisProcessID
);
213 yap("mv ", fullExe
, " ", oldExe
);
214 rename(fullExe
, oldExe
);
217 auto fullExeTemp
= fullExe
~".tmp";
219 string
[] buildTodo () {
220 string outExe
= (fullExeTemp
[0] != '/' ? objDir
~"/"~fullExeTemp
: fullExeTemp
);
224 ["-I"~dirName(root
)]~
225 ["-J"~dirName(root
)]~
229 foreach (k
, objectFile
; myDeps
) if (objectFile
!is null) todo
~= [k
];
230 // Need to add void main(){}?
232 auto stubMain
= buildPath(myOwnTmpDir
, "stubmain.d");
233 std
.file
.write(stubMain
, "void main(){}");
238 auto todo
= buildTodo();
239 // Different shells and OS functions have different limits,
240 // but 1024 seems to be the smallest maximum outside of MS-DOS.
241 enum maxLength
= 1024;
242 auto commandLength
= escapeShellCommand(todo
).length
;
247 eflags2
~= "-static-libphobos";
249 if (commandLength
+compilerBinary
.length
+compilerExtraFlags
.join(" ").length
+eflags2
.join(" ").length
> 32760) throw new Exception("SHIT!");
250 immutable result
= run([compilerBinary
]~compilerExtraFlags
~eflags2
~todo
);
253 if (exists(fullExeTemp
)) remove(fullExeTemp
);
256 // clean up the dir containing the object file, just not in dry
257 // run mode because we haven't created any!
259 yap("stat ", objDir
);
260 if (objDir
.exists
&& objDir
.startsWith(workDir
)) {
261 yap("rmdirRecurse ", objDir
);
262 // We swallow the exception because of a potential race: two
263 // concurrently-running scripts may attempt to remove this
264 // directory. One will fail.
265 collectException(rmdirRecurse(objDir
));
267 yap("mv ", fullExeTemp
, " ", fullExe
);
268 rename(fullExeTemp
, fullExe
);
274 ////////////////////////////////////////////////////////////////////////////////
275 private bool inALibrary (string source
, string object
) {
276 if (object
.endsWith(".di") || source
== "object" || source
== "gcstats") return true;
277 foreach (string exclusion
; exclusions
) if (source
.startsWith(exclusion
~'.')) return true;
282 // Given module rootModule, returns a mapping of all dependees .d
283 // source filenames to their corresponding .o files sitting in
284 // directory workDir. The mapping is obtained by running dmd -v against
286 private string
[string
] getDependencies (string rootModule
, string workDir
, string objDir
, string
[] compilerFlags
) {
287 immutable depsFilename
= buildPath(workDir
, "rgdc.deps");
289 string
[string
] readDepsFile (/*string depsFilename, string objDir="."*/) {
290 string
d2obj (string dfile
) { return buildPath(objDir
, dfile
.baseName
.chomp(".d")~objExt
); }
292 string
findLib (string libName
) {
293 // This can't be 100% precise without knowing exactly where the linker
294 // will look for libraries (which requires, but is not limited to,
295 // parsing the linker's command line (as specified in dmd.conf/sc.ini).
296 // Go for best-effort instead.
297 string
[] dirs
= ["."];
298 foreach (envVar
; ["LIB", "LIBRARY_PATH", "LD_LIBRARY_PATH"]) dirs
~= environment
.get(envVar
, "").split(pathSeparator
);
299 string
[] names
= ["lib"~libName
~".so", "lib"~libName
~".a"];
300 dirs
~= ["/lib", "/usr/lib", "/usr/local/lib"];
301 foreach (dir
; dirs
) {
302 foreach (name
; names
) {
303 auto path
= buildPath(dir
, name
);
305 if (path
.extension
== ".so") return "-l"~path
.baseName
.stripExtension
[3..$];
306 return absolutePath(path
);
313 yap("read ", depsFilename
);
314 auto depsReader
= File(depsFilename
);
315 scope(exit
) collectException(depsReader
.close()); // don't care for errors
316 // Fetch all dependencies and append them to myDeps
317 static auto modInfoRE
= ctRegex
!(r
"^(.+?)\s+\(([^)]+)\)");
318 //std.stdio (/opt/gdc/include/d/4.9.1/std/stdio.d) : private : object (/opt/gdc/include/d/4.9.1/object.di)
319 static auto modImportRE
= ctRegex
!(r
"^(.+?)\s+\(([^)]+)\)\s+:\s+string\s+:\s+[^(]+\(([^)]+)\)");
320 //hello (hello.d) : string : zm (/mnt/tigerclaw/D/prj/rgdc/rgdc_native/zm)
321 string
[string
] result
;
322 foreach (string line
; lines(depsReader
)) {
323 string modName
, modPath
, filePath
;
324 auto modImportMatch
= match(line
, modImportRE
);
325 if (modImportMatch
.empty
) {
326 auto modInfoMatch
= match(line
, modInfoRE
);
327 if (modInfoMatch
.empty
) continue;
328 auto modInfoCaps
= modInfoMatch
.captures
;
331 modName
= modInfoCaps
[1];
332 modPath
= modInfoCaps
[2];
334 auto modImportCaps
= modImportMatch
.captures
;
338 modName
= modImportCaps
[1];
339 modPath
= modImportCaps
[2];
340 filePath
= modImportCaps
[3];
342 if (filePath
.length
) result
[filePath
] = null;
343 if (inALibrary(modName
, modPath
)) continue;
344 result
[modPath
] = d2obj(modPath
);
349 // Check if the old dependency file is fine
351 yap("stat ", depsFilename
);
352 if (exists(depsFilename
)) {
353 // See if the deps file is still in good shape
354 auto deps
= readDepsFile();
355 auto allDeps
= chain(rootModule
.only
, deps
.byKey
).array
;
356 bool mustRebuildDeps
= allDeps
.anyNewerThan(timeLastModified(depsFilename
));
357 if (!mustRebuildDeps
) return deps
; // Cool, we're in good shape
358 // Fall through to rebuilding the deps file
362 immutable rootDir
= dirName(rootModule
);
364 // Collect dependencies
366 // "cd "~shellQuote(rootDir)~" && "
367 [compilerBinary
]~compilerExtraFlags
~compilerFlags
~
371 "-fdeps="~depsFilename
,
375 ]~includeDirs
~importDirs
;
378 // Delete the deps file on failure, we don't want to be fooled
379 // by it next time we try
380 collectException(std
.file
.remove(depsFilename
));
383 immutable depsExitCode
= runSilent(depsGetter
);
385 stderr
.writefln("Failed: %s", depsGetter
);
386 collectException(std
.file
.remove(depsFilename
));
390 return (dryRun ?
null : readDepsFile());
394 ////////////////////////////////////////////////////////////////////////////////
395 // Is any file newer than the given file?
396 private bool anyNewerThan (in string
[] files
, in string file
) {
398 return files
.anyNewerThan(file
.timeLastModified
);
402 // Is any file newer than the given file?
403 private bool anyNewerThan (in string
[] files
, SysTime t
) {
404 // Experimental: running newerThan in separate threads, one per file
406 foreach (source
; files
) if (source
.newerThan(t
)) return true;
410 foreach (source
; taskPool
.parallel(files
)) if (!result
&& source
.newerThan(t
)) result
= true;
417 * If force is true, returns true. Otherwise, if source and target both
418 * exist, returns true iff source's timeLastModified is strictly greater
419 * than target's. Otherwise, returns true.
421 private bool newerThan (string source
, string target
) {
422 if (force
) return true;
423 yap("stat ", target
);
424 return source
.newerThan(timeLastModified(target
, SysTime(0)));
428 private bool newerThan (string source
, SysTime target
) {
429 if (force
) return true;
431 yap("stat ", source
);
432 return DirEntry(source
).timeLastModified
> target
;
433 } catch (Exception
) {
434 // File not there, consider it newer
440 ////////////////////////////////////////////////////////////////////////////////
441 private @property string
helpString () {
443 "rgdc build "~thisVersion
~"
444 Usage: rgdc [RDMD AND DMD OPTIONS]... program [PROGRAM OPTIONS]...
445 Builds (with dependents) and runs a D program.
446 Example: rgdc -release myprog --myprogparm 5
448 Any option to be passed to the compiler must occur before the program name. In
449 addition to compiler options, rgdc recognizes the following options:
450 --build-only just build the executable, don't run it
451 --chatty write compiler commands to stdout before executing them
452 --compiler=comp use the specified compiler
453 --dry-run do not compile, just show what commands would be run (implies --chatty)
454 --exclude=package exclude a package from the build (multiple --exclude allowed)
455 --force force a rebuild even if apparently not necessary
457 --main add a stub main program to the mix (e.g. for unittesting)
458 --shebang rgdc is in a shebang line (put as first argument)
463 ////////////////////////////////////////////////////////////////////////////////
464 private @property string
thisVersion () {
466 enum month
= d
[0..3],
467 day
= d
[4] == ' ' ?
"0"~d
[5] : d
[4..6],
470 = month
== "Jan" ?
"01"
471 : month
== "Feb" ?
"02"
472 : month
== "Mar" ?
"03"
473 : month
== "Apr" ?
"04"
474 : month
== "May" ?
"05"
475 : month
== "Jun" ?
"06"
476 : month
== "Jul" ?
"07"
477 : month
== "Aug" ?
"08"
478 : month
== "Sep" ?
"09"
479 : month
== "Oct" ?
"10"
480 : month
== "Nov" ?
"11"
481 : month
== "Dec" ?
"12"
483 static assert(month
!= "", "Unknown month "~month
);
484 return year
[0]~year
[1..$]~monthNum
~day
;
488 private string
which (string path
) {
490 if (path
.canFind(dirSeparator
) || altDirSeparator
!= "" && path
.canFind(altDirSeparator
)) return path
;
491 string
[] extensions
= [""];
492 foreach (extension
; extensions
) {
493 foreach (envPath
; environment
["PATH"].splitter(pathSeparator
)) {
494 string absPath
= buildPath(envPath
, path
~extension
);
495 yap("stat ", absPath
);
496 if (exists(absPath
) && isFile(absPath
)) return absPath
;
499 throw new FileException(path
, "File not found in PATH");
503 private size_t
indexOfProgram (string
[] args
) {
504 foreach(i
, arg
; args
[1..$]) {
505 if (!arg
.startsWith('-', '@') && !arg
.endsWith(".o", ".a")) return i
+1;
511 int main(string
[] args
) {
513 for (size_t f
= 1; f
< args
.length
; ++f
) {
514 if (args
[f
] == "-dynamic" || args
[f
] == "--dynamic") {
516 args
= args
[0..f
]~args
[f
+1..$];
519 if (args
[f
] == "-static" || args
[f
] == "--static") {
521 args
= args
[0..f
]~args
[f
+1..$];
526 //writeln("Invoked with: ", args);
527 if (args
.length
> 1 && args
[1].startsWith("--shebang ", "--shebang=")) {
528 // multiple options wrapped in one
529 auto a
= args
[1]["--shebang ".length
..$];
530 args
= args
[0..1]~std
.string
.split(a
)~args
[2..$];
533 // Continue parsing the command line; now get rgdc's own arguments
534 auto programPos
= indexOfProgram(args
);
535 assert(programPos
> 0);
536 auto argsBeforeProgram
= args
[0..programPos
];
538 bool bailout
; // bailout set by functions called in getopt if program should exit
539 bool addStubMain
; // set by --main
540 getopt(argsBeforeProgram
,
541 std
.getopt
.config
.caseSensitive
,
542 std
.getopt
.config
.passThrough
,
543 "build-only", &buildOnly
,
545 "compiler", &compilerBinary
,
547 "exclude", &exclusions
,
549 "help", { writeln(helpString
); bailout
= true; },
550 "main", &addStubMain
,
551 "show-commands", &optShowCommands
553 if (bailout
) return 0;
554 if (dryRun
) chatty
= true; // dry-run implies chatty
556 // no code on command line => require a source file
557 if (programPos
== args
.length
) {
563 root
= args
[programPos
].chomp(".d")~".d",
564 exeBasename
= root
.baseName(".d"),
565 exeDirname
= root
.dirName
,
566 programArgs
= args
[programPos
+1..$];
568 assert(argsBeforeProgram
.length
>= 1);
569 auto compilerFlags
= argsBeforeProgram
[1..$];
571 bool lib
= compilerFlags
.canFind("-lib");
572 string outExt
= (lib ? libExt
: binExt
);
574 //collectException(readRC(thisExePath.setExtension(".rc")));
575 collectException(readRC("rgdc.rc"));
577 // --build-only implies the user would like a binary in the program's directory
578 if (buildOnly
&& !exe
) exe
= exeDirname
~dirSeparator
;
580 if (exe
&& exe
.endsWith(dirSeparator
)) {
581 // user specified a directory, complete it to a file
582 exe
= buildPath(exe
, exeBasename
)~outExt
;
585 // Compute the object directory and ensure it exists
586 immutable workDir
= getWorkPath(root
, compilerFlags
);
587 lockWorkPath(workDir
); // will be released by the OS on process exit
588 string objDir
= buildPath(workDir
, "objs");
589 yap("mkdirRecurse ", objDir
);
590 if (!dryRun
) mkdirRecurse(objDir
);
593 // When building libraries, DMD does not generate object files.
594 // Instead, it uses the -od parameter as the location for the library file.
595 // Thus, override objDir (which is normally a temporary directory)
596 // to be the target output directory.
597 objDir
= exe
.dirName
;
600 // Fetch dependencies
601 const myDeps
= getDependencies(root
, workDir
, objDir
, compilerFlags
);
603 // Compute executable name, check for freshness, rebuild
605 We need to be careful about using -o. Normally the generated
606 executable is hidden in the unique directory workDir. But if the
607 user forces generation in a specific place by using -od or -of,
608 the time of the binary can't be used to check for freshness
609 because the user may change e.g. the compile option from one run
610 to the next, yet the generated binary's datetime stays the
611 same. In those cases, we'll use a dedicated file called ".built"
612 and placed in workDir. Upon a successful build, ".built" will be
614 http://d.puremagic.com/issues/show_bug.cgi?id=4814
617 SysTime lastBuildTime
= SysTime
.min
;
619 // user-specified exe name
620 buildWitness
= buildPath(workDir
, ".built");
621 if (!exe
.newerThan(buildWitness
)) {
622 // Both exe and buildWitness exist, and exe is older than
623 // buildWitness. This is the only situation in which we
624 // may NOT need to recompile.
625 lastBuildTime
= buildWitness
.timeLastModified(SysTime
.min
);
628 exe
= buildPath(workDir
, exeBasename
)~outExt
;
630 lastBuildTime
= buildWitness
.timeLastModified(SysTime
.min
);
634 if (chain(root
.only
, myDeps
.byKey
).array
.anyNewerThan(lastBuildTime
)) {
635 immutable result
= rebuild(root
, exe
, workDir
, objDir
, myDeps
, compilerFlags
, addStubMain
);
636 if (result
) return result
;
637 // Touch the build witness to track the build time
638 if (buildWitness
!= exe
) {
639 yap("touch ", buildWitness
);
640 std
.file
.write(buildWitness
, "");
649 // release lock on workDir before launching the user's program
653 return exec(exe
~programArgs
);