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 * Distributed under the Boost Software License, Version 1.0.
9 * (See accompanying file LICENSE_1_0.txt or copy at
10 * http://www.boost.org/LICENSE_1_0.txt)
31 private enum objExt
= ".o";
32 private enum binExt
= "";
33 private enum libExt
= ".a";
34 private enum altDirSeparator
= "";
36 private bool chatty
, buildOnly
, dryRun
, force
;
38 private string
[] exclusions
= ["std", "etc", "core", "tango"]; // packages that are to be excluded
41 private string compilerBinary
= "gdc";
42 private string compilerExtraFlags
= "-pipe";
45 private void yap(size_t line
=__LINE__
, T
...) (auto ref T stuff
) {
47 debug stderr
.writeln(line
, ": ", stuff
);
48 else stderr
.writeln(stuff
);
52 private string
[] includeDirs
; // -I
53 private string
[] importDirs
; // -J
56 private string
expandString (string s
) {
57 auto pos
= s
.indexOf("$(");
63 auto epos
= s
.indexOf(')');
64 if (epos
< 1) throw new Exception("invalid envvar name");
65 auto v
= environment
.get(s
[0..epos
], "");
68 pos
= s
.indexOf("$(");
77 private void readRC (string fname
) {
78 auto reader
= File(fname
);
79 scope(exit
) collectException(reader
.close()); // don't care for errors
80 foreach (string line
; lines(reader
)) {
81 if (line
.startsWith("include=")) {
82 auto val
= expandString(line
[8..$].strip
);
85 if (!includeDirs
.canFind(val
)) includeDirs
~= val
;
87 } else if (line
.startsWith("import=")) {
88 auto val
= expandString(line
[7..$].strip
);
91 if (!importDirs
.canFind(val
)) importDirs
~= val
;
98 private File lockFile
;
100 private void lockWorkPath (string workPath
) {
101 string lockFileName
= buildPath(workPath
, "rgdc.lock");
102 if (!dryRun
) lockFile
.open(lockFileName
, "w");
103 yap("lock ", lockFile
.name
);
104 if (!dryRun
) lockFile
.lock();
108 private void unlockWorkPath () {
109 yap("unlock ", lockFile
.name
);
117 private bool inALibrary (string source
, string object
) {
118 if (object
.endsWith(".di") || source
== "object" || source
== "gcstats") return true;
119 foreach (string exclusion
; exclusions
) if (source
.startsWith(exclusion
~'.')) return true;
124 private @property string
myOwnTmpDir () {
125 import core
.sys
.posix
.unistd
;
126 auto tmpRoot
= format("/tmp/.rgdc-%d", getuid());
127 yap("mkdirRecurse ", tmpRoot
);
128 if (!dryRun
) mkdirRecurse(tmpRoot
);
133 private string
getWorkPath (in string root
, in string
[] compilerFlags
) {
134 static string workPath
;
135 if (workPath
) return workPath
;
136 enum string
[] irrelevantSwitches
= ["--help", "-ignore", "-quiet", "-v"];
139 context
.put(getcwd().representation
);
140 context
.put(root
.representation
);
141 foreach (flag
; compilerFlags
) {
142 if (irrelevantSwitches
.canFind(flag
)) continue;
143 context
.put(flag
.representation
);
145 auto digest
= context
.finish();
146 string hash
= toHexString(digest
);
147 const tmpRoot
= myOwnTmpDir
;
148 workPath
= buildPath(tmpRoot
, "rgdc-"~baseName(root
)~'-'~hash
);
149 yap("mkdirRecurse ", workPath
);
150 if (!dryRun
) mkdirRecurse(workPath
);
155 // Rebuild the executable fullExe starting from modules in myDeps
156 // passing the compiler flags compilerFlags. Generates one large
158 private int rebuild (string root
, string fullExe
,
159 string workDir
, string objDir
, in string
[string
] myDeps
,
160 string
[] compilerFlags
, bool addStubMain
)
162 // Delete the old executable before we start building.
163 yap("stat ", fullExe
);
164 if (!dryRun
&& exists(fullExe
)) {
168 } catch (FileException e
) {
169 // This can occur on Windows if the executable is locked.
170 // Although we can't delete the file, we can still rename it.
171 auto oldExe
= "%s.%s-%s.old".format(fullExe
, Clock
.currTime
.stdTime
, thisProcessID
);
172 yap("mv ", fullExe
, " ", oldExe
);
173 rename(fullExe
, oldExe
);
176 auto fullExeTemp
= fullExe
~".tmp";
178 string
[] buildTodo () {
179 string outExe
= (fullExeTemp
[0] != '/' ? objDir
~"/"~fullExeTemp
: fullExeTemp
);
183 ["-I"~dirName(root
)]~
184 ["-J"~dirName(root
)]~
188 foreach (k
, objectFile
; myDeps
) if (objectFile
!is null) todo
~= [k
];
189 // Need to add void main(){}?
191 auto stubMain
= buildPath(myOwnTmpDir
, "stubmain.d");
192 std
.file
.write(stubMain
, "void main(){}");
197 auto todo
= buildTodo();
198 // Different shells and OS functions have different limits,
199 // but 1024 seems to be the smallest maximum outside of MS-DOS.
200 enum maxLength
= 1024;
201 auto commandLength
= escapeShellCommand(todo
).length
;
202 if (commandLength
+compilerBinary
.length
+compilerExtraFlags
.length
> 32760) throw new Exception("SHIT!");
203 immutable result
= run([compilerBinary
, compilerExtraFlags
]~todo
);
206 if (exists(fullExeTemp
)) remove(fullExeTemp
);
209 // clean up the dir containing the object file, just not in dry
210 // run mode because we haven't created any!
212 yap("stat ", objDir
);
213 if (objDir
.exists
&& objDir
.startsWith(workDir
)) {
214 yap("rmdirRecurse ", objDir
);
215 // We swallow the exception because of a potential race: two
216 // concurrently-running scripts may attempt to remove this
217 // directory. One will fail.
218 collectException(rmdirRecurse(objDir
));
220 yap("mv ", fullExeTemp
, " ", fullExe
);
221 rename(fullExeTemp
, fullExe
);
227 // Run a program optionally writing the command line first
228 // If "replace" is true and the OS supports it, replace the current process.
229 private int run (string
[] args
, string output
=null, bool replace
=false) {
231 yap(replace ?
"exec " : "spawn ", args
.text
);
232 if (dryRun
) return 0;
233 if (replace
&& !output
) {
234 import std
.c
.process
;
235 auto argv
= args
.map
!toStringz
.chain(null.only
).array
;
236 return execv(argv
[0], argv
.ptr
);
240 outputFile
= File(output
, "wb");
244 auto process
= spawnProcess(args
, stdin
, outputFile
);
245 return process
.wait();
249 private int runSilent (string
[] args
) {
251 yap("spawn ", args
.text
);
252 if (dryRun
) return 0;
253 auto process
= spawnProcess(args
);
254 return process
.wait();
258 private int exec (string
[] args
) {
259 return run(args
, null, true);
263 // Given module rootModule, returns a mapping of all dependees .d
264 // source filenames to their corresponding .o files sitting in
265 // directory workDir. The mapping is obtained by running dmd -v against
267 private string
[string
] getDependencies (string rootModule
, string workDir
, string objDir
, string
[] compilerFlags
) {
268 immutable depsFilename
= buildPath(workDir
, "rgdc.deps");
270 string
[string
] readDepsFile (/*string depsFilename, string objDir="."*/) {
271 string
d2obj (string dfile
) { return buildPath(objDir
, dfile
.baseName
.chomp(".d")~objExt
); }
273 string
findLib (string libName
) {
274 // This can't be 100% precise without knowing exactly where the linker
275 // will look for libraries (which requires, but is not limited to,
276 // parsing the linker's command line (as specified in dmd.conf/sc.ini).
277 // Go for best-effort instead.
278 string
[] dirs
= ["."];
279 foreach (envVar
; ["LIB", "LIBRARY_PATH", "LD_LIBRARY_PATH"]) dirs
~= environment
.get(envVar
, "").split(pathSeparator
);
280 string
[] names
= ["lib"~libName
~".so", "lib"~libName
~".a"];
281 dirs
~= ["/lib", "/usr/lib", "/usr/local/lib"];
282 foreach (dir
; dirs
) {
283 foreach (name
; names
) {
284 auto path
= buildPath(dir
, name
);
286 if (path
.extension
== ".so") return "-l"~path
.baseName
.stripExtension
[3..$];
287 return absolutePath(path
);
294 yap("read ", depsFilename
);
295 auto depsReader
= File(depsFilename
);
296 scope(exit
) collectException(depsReader
.close()); // don't care for errors
297 // Fetch all dependencies and append them to myDeps
298 static auto modInfoRE
= ctRegex
!(r
"^(.+?)\s+\(([^)]+)\)");
299 //std.stdio (/opt/gdc/include/d/4.9.1/std/stdio.d) : private : object (/opt/gdc/include/d/4.9.1/object.di)
300 static auto modImportRE
= ctRegex
!(r
"^(.+?)\s+\(([^)]+)\)\s+:\s+string\s+:\s+[^(]+\(([^)]+)\)");
301 //hello (hello.d) : string : zm (/mnt/tigerclaw/D/prj/rgdc/rgdc_native/zm)
302 string
[string
] result
;
303 foreach (string line
; lines(depsReader
)) {
304 string modName
, modPath
, filePath
;
305 auto modImportMatch
= match(line
, modImportRE
);
306 if (modImportMatch
.empty
) {
307 auto modInfoMatch
= match(line
, modInfoRE
);
308 if (modInfoMatch
.empty
) continue;
309 auto modInfoCaps
= modInfoMatch
.captures
;
312 modName
= modInfoCaps
[1];
313 modPath
= modInfoCaps
[2];
315 auto modImportCaps
= modImportMatch
.captures
;
319 modName
= modImportCaps
[1];
320 modPath
= modImportCaps
[2];
321 filePath
= modImportCaps
[3];
323 if (filePath
.length
) result
[filePath
] = null;
324 if (inALibrary(modName
, modPath
)) continue;
325 result
[modPath
] = d2obj(modPath
);
330 // Check if the old dependency file is fine
332 yap("stat ", depsFilename
);
333 if (exists(depsFilename
)) {
334 // See if the deps file is still in good shape
335 auto deps
= readDepsFile();
336 auto allDeps
= chain(rootModule
.only
, deps
.byKey
).array
;
337 bool mustRebuildDeps
= allDeps
.anyNewerThan(timeLastModified(depsFilename
));
338 if (!mustRebuildDeps
) return deps
; // Cool, we're in good shape
339 // Fall through to rebuilding the deps file
343 immutable rootDir
= dirName(rootModule
);
345 // Collect dependencies
347 // "cd "~shellQuote(rootDir)~" && "
348 [compilerBinary
, compilerExtraFlags
]~compilerFlags
~
352 "-fdeps="~depsFilename
,
356 ]~includeDirs
~importDirs
;
359 // Delete the deps file on failure, we don't want to be fooled
360 // by it next time we try
361 collectException(std
.file
.remove(depsFilename
));
364 immutable depsExitCode
= runSilent(depsGetter
);
366 stderr
.writefln("Failed: %s", depsGetter
);
367 collectException(std
.file
.remove(depsFilename
));
371 return (dryRun ?
null : readDepsFile());
375 // Is any file newer than the given file?
376 private bool anyNewerThan (in string
[] files
, in string file
) {
378 return files
.anyNewerThan(file
.timeLastModified
);
382 // Is any file newer than the given file?
383 private bool anyNewerThan (in string
[] files
, SysTime t
) {
384 // Experimental: running newerThan in separate threads, one per file
386 foreach (source
; files
) if (source
.newerThan(t
)) return true;
390 foreach (source
; taskPool
.parallel(files
)) if (!result
&& source
.newerThan(t
)) result
= true;
397 * If force is true, returns true. Otherwise, if source and target both
398 * exist, returns true iff source's timeLastModified is strictly greater
399 * than target's. Otherwise, returns true.
401 private bool newerThan (string source
, string target
) {
402 if (force
) return true;
403 yap("stat ", target
);
404 return source
.newerThan(timeLastModified(target
, SysTime(0)));
408 private bool newerThan (string source
, SysTime target
) {
409 if (force
) return true;
411 yap("stat ", source
);
412 return DirEntry(source
).timeLastModified
> target
;
413 } catch (Exception
) {
414 // File not there, consider it newer
420 private @property string
helpString () {
422 "rgdc build "~thisVersion
~"
423 Usage: rgdc [RDMD AND DMD OPTIONS]... program [PROGRAM OPTIONS]...
424 Builds (with dependents) and runs a D program.
425 Example: rgdc -release myprog --myprogparm 5
427 Any option to be passed to the compiler must occur before the program name. In
428 addition to compiler options, rgdc recognizes the following options:
429 --build-only just build the executable, don't run it
430 --chatty write compiler commands to stdout before executing them
431 --compiler=comp use the specified compiler
432 --dry-run do not compile, just show what commands would be run (implies --chatty)
433 --exclude=package exclude a package from the build (multiple --exclude allowed)
434 --force force a rebuild even if apparently not necessary
436 --main add a stub main program to the mix (e.g. for unittesting)
437 --shebang rgdc is in a shebang line (put as first argument)
442 private @property string
thisVersion () {
444 enum month
= d
[0..3],
445 day
= d
[4] == ' ' ?
"0"~d
[5] : d
[4..6],
448 = month
== "Jan" ?
"01"
449 : month
== "Feb" ?
"02"
450 : month
== "Mar" ?
"03"
451 : month
== "Apr" ?
"04"
452 : month
== "May" ?
"05"
453 : month
== "Jun" ?
"06"
454 : month
== "Jul" ?
"07"
455 : month
== "Aug" ?
"08"
456 : month
== "Sep" ?
"09"
457 : month
== "Oct" ?
"10"
458 : month
== "Nov" ?
"11"
459 : month
== "Dec" ?
"12"
461 static assert(month
!= "", "Unknown month "~month
);
462 return year
[0]~year
[1..$]~monthNum
~day
;
466 private string
which (string path
) {
468 if (path
.canFind(dirSeparator
) || altDirSeparator
!= "" && path
.canFind(altDirSeparator
)) return path
;
469 string
[] extensions
= [""];
470 foreach (extension
; extensions
) {
471 foreach (envPath
; environment
["PATH"].splitter(pathSeparator
)) {
472 string absPath
= buildPath(envPath
, path
~extension
);
473 yap("stat ", absPath
);
474 if (exists(absPath
) && isFile(absPath
)) return absPath
;
477 throw new FileException(path
, "File not found in PATH");
481 private size_t
indexOfProgram (string
[] args
) {
482 foreach(i
, arg
; args
[1..$]) {
483 if (!arg
.startsWith('-', '@') && !arg
.endsWith(".o", ".a")) return i
+1;
489 int main(string
[] args
) {
490 //writeln("Invoked with: ", args);
491 if (args
.length
> 1 && args
[1].startsWith("--shebang ", "--shebang=")) {
492 // multiple options wrapped in one
493 auto a
= args
[1]["--shebang ".length
..$];
494 args
= args
[0..1]~std
.string
.split(a
)~args
[2..$];
497 // Continue parsing the command line; now get rgdc's own arguments
498 auto programPos
= indexOfProgram(args
);
499 assert(programPos
> 0);
500 auto argsBeforeProgram
= args
[0..programPos
];
502 bool bailout
; // bailout set by functions called in getopt if program should exit
503 bool addStubMain
; // set by --main
504 getopt(argsBeforeProgram
,
505 std
.getopt
.config
.caseSensitive
,
506 std
.getopt
.config
.passThrough
,
507 "build-only", &buildOnly
,
509 "compiler", &compilerBinary
,
511 "exclude", &exclusions
,
513 "help", { writeln(helpString
); bailout
= true; },
516 if (bailout
) return 0;
517 if (dryRun
) chatty
= true; // dry-run implies chatty
519 // no code on command line => require a source file
520 if (programPos
== args
.length
) {
526 root
= args
[programPos
].chomp(".d")~".d",
527 exeBasename
= root
.baseName(".d"),
528 exeDirname
= root
.dirName
,
529 programArgs
= args
[programPos
+1..$];
531 assert(argsBeforeProgram
.length
>= 1);
532 auto compilerFlags
= argsBeforeProgram
[1..$];
534 bool lib
= compilerFlags
.canFind("-lib");
535 string outExt
= (lib ? libExt
: binExt
);
537 //collectException(readRC(thisExePath.setExtension(".rc")));
538 collectException(readRC("rgdc.rc"));
540 // --build-only implies the user would like a binary in the program's directory
541 if (buildOnly
&& !exe
) exe
= exeDirname
~dirSeparator
;
543 if (exe
&& exe
.endsWith(dirSeparator
)) {
544 // user specified a directory, complete it to a file
545 exe
= buildPath(exe
, exeBasename
)~outExt
;
548 // Compute the object directory and ensure it exists
549 immutable workDir
= getWorkPath(root
, compilerFlags
);
550 lockWorkPath(workDir
); // will be released by the OS on process exit
551 string objDir
= buildPath(workDir
, "objs");
552 yap("mkdirRecurse ", objDir
);
553 if (!dryRun
) mkdirRecurse(objDir
);
556 // When building libraries, DMD does not generate object files.
557 // Instead, it uses the -od parameter as the location for the library file.
558 // Thus, override objDir (which is normally a temporary directory)
559 // to be the target output directory.
560 objDir
= exe
.dirName
;
563 // Fetch dependencies
564 const myDeps
= getDependencies(root
, workDir
, objDir
, compilerFlags
);
566 // Compute executable name, check for freshness, rebuild
568 We need to be careful about using -o. Normally the generated
569 executable is hidden in the unique directory workDir. But if the
570 user forces generation in a specific place by using -od or -of,
571 the time of the binary can't be used to check for freshness
572 because the user may change e.g. the compile option from one run
573 to the next, yet the generated binary's datetime stays the
574 same. In those cases, we'll use a dedicated file called ".built"
575 and placed in workDir. Upon a successful build, ".built" will be
577 http://d.puremagic.com/issues/show_bug.cgi?id=4814
580 SysTime lastBuildTime
= SysTime
.min
;
582 // user-specified exe name
583 buildWitness
= buildPath(workDir
, ".built");
584 if (!exe
.newerThan(buildWitness
)) {
585 // Both exe and buildWitness exist, and exe is older than
586 // buildWitness. This is the only situation in which we
587 // may NOT need to recompile.
588 lastBuildTime
= buildWitness
.timeLastModified(SysTime
.min
);
591 exe
= buildPath(workDir
, exeBasename
)~outExt
;
593 lastBuildTime
= buildWitness
.timeLastModified(SysTime
.min
);
597 if (chain(root
.only
, myDeps
.byKey
).array
.anyNewerThan(lastBuildTime
)) {
598 immutable result
= rebuild(root
, exe
, workDir
, objDir
, myDeps
, compilerFlags
, addStubMain
);
599 if (result
) return result
;
600 // Touch the build witness to track the build time
601 if (buildWitness
!= exe
) {
602 yap("touch ", buildWitness
);
603 std
.file
.write(buildWitness
, "");
612 // release lock on workDir before launching the user's program
616 return exec(exe
~programArgs
);