root/trunk/rdmdAlt.d

Revision 230, 17.4 kB (checked in by Abscissa, 10 months ago)

rdmdAlt: Applied fix for DMD #6102: [RDMD] Changing a string import file doesn't trigger a rebuild

Line 
1 // Written in the D programming language.
2
3 // This is a slight modification of RDMD r1400 with patches
4 // applied for these issues:
5 //   #4672, #4683, #4684, #4688, #4928, #4930, #6102
6 // These are necessary for SemiTwist D Tools.
7 // Plus this is fixed to compile on DMD 2.052 and up.
8
9 import std.algorithm, std.c.stdlib, std.exception, std.datetime,
10     std.file, std.getopt,
11     std.md5, std.path, std.process, std.regexp,
12     std.stdio, std.string, std.typetuple;
13 static import std.array;
14
15 version (Posix)
16 {
17     enum objExt = ".o";
18     enum binExt = "";
19 }
20 else version (Windows)
21 {
22     enum objExt = ".obj";
23     enum binExt = ".exe";
24 }
25 else
26 {
27     static assert(0);
28 }
29
30 private bool chatty, buildOnly, dryRun, force, keepObj;
31 private string exe, compiler = "dmd";
32
33 int main(string[] args)
34 {
35     //writeln("Invoked with: ", map!(q{a ~ ", "})(args));
36     if (args.length > 1 && std.algorithm.startsWith(args[1],
37                     "--shebang ", "--shebang="))
38     {
39         // multiple options wrapped in one
40         auto a = args[1]["--shebang ".length .. $];
41         args = args[0 .. 1] ~ std.string.split(a) ~ args[2 .. $];
42     }
43    
44     // Continue parsing the command line; now get rdmd's own arguments
45     // parse the -o option
46     void dashOh(string key, string value)
47     {
48         if (value[0] == 'f')
49         {
50             // -ofmyfile passed
51             exe = value[1 .. $];
52         }
53         else if (value[0] == 'd')
54         {
55             // -odmydir passed
56             if(!exe) // Don't let -od override -of
57             {
58                 // add a trailing path separator to clarify it's a dir
59                 exe = value[1 .. $];
60                 if(!std.algorithm.endsWith(exe, std.path.sep[]))
61                     exe ~= std.path.sep[];
62                 assert(std.algorithm.endsWith(exe, std.path.sep[]));
63             }
64         }
65         else if (value[0] == '-')
66         {
67             // -o- passed
68             enforce(false, "Option -o- currently not supported by rdmd");
69         }
70         else if (value[0] == '+')
71         {
72             // -o+ passed
73             keepObj = true;
74         }
75         else
76         {
77             enforce(false, "Unrecognized option: "~key~value);
78         }
79     }
80    
81     // start the web browser on documentation page
82     void man()
83     {
84         foreach (b; [ std.process.getenv("BROWSER"), "firefox",
85                         "sensible-browser", "x-www-browser" ]) {
86             if (!b.length) continue;
87             if (!system(b~" http://www.digitalmars.com/d/2.0/rdmd.html"))
88                 return;
89         }
90     }
91
92     auto programPos = indexOfProgram(args);
93     // Insert "--" to tell getopts when to stop
94     args = args[0..programPos] ~ "--" ~ ( (programPos<args.length)? args[programPos..$] : [] );
95
96     bool bailout;    // bailout set by functions called in getopt if
97                      // program should exit
98     string[] loop;       // set by --loop
99     bool addStubMain;// set by --main
100     string[] eval;     // set by --eval
101     getopt(args,
102             std.getopt.config.caseSensitive,
103             std.getopt.config.passThrough,
104             "build-only", &buildOnly,
105             "chatty", &chatty,
106             "dry-run", &dryRun,
107             "force", &force,
108             "help", (string) { writeln(helpString); bailout = true; },
109             "main", &addStubMain,
110             "man", (string) { man; bailout = true; },
111             "eval", &eval,
112             "loop", &loop,
113             "o", &dashOh,
114             "compiler", &compiler);
115     if (bailout) return 0;
116     if (dryRun) chatty = true; // dry-run implies chatty
117
118     // Just evaluate this program!
119     if (loop)
120     {
121         return .eval(importWorld ~ "void main(char[][] args) { "
122                 ~ "foreach (line; stdin.byLine()) {\n" ~ std.string.join(loop, "\n")
123                 ~ ";\n} }");
124     }
125     if (eval)
126     {
127         return .eval(importWorld ~ "void main(char[][] args) {\n"
128                 ~ std.string.join(eval, "\n") ~ ";\n}");
129     }
130    
131     // Parse the program line - first find the program to run
132     programPos = indexOfProgram(args);
133     if(programPos == args.length)
134     {
135         write(helpString);
136         return 1;
137     }
138     const
139         root = /*rel2abs*/(chomp(args[programPos], ".d") ~ ".d"),
140         exeBasename = basename(root, ".d"),
141         exeDirname = dirname(root);
142     auto programArgs = args[programPos + 1 .. $];
143     args = args[0 .. programPos];
144     const compilerFlags = args[1 .. programPos-1];
145
146     // Compute the object directory and ensure it exists
147     invariant objDir = getObjPath(root, compilerFlags);
148     if (!dryRun)        // only make a fuss about objDir on a real run
149     {
150         exists(objDir)
151             ? enforce(isdir(objDir),
152                     "Entry `"~objDir~"' exists but is not a directory.")
153             : mkdir(objDir);
154     }
155    
156     // Fetch dependencies
157     const myDeps = getDependencies(root, objDir, compilerFlags);
158
159     // Compute executable name, check for freshness, rebuild
160     if (exe)
161     {
162         // user-specified exe name
163         if (std.algorithm.endsWith(exe, std.path.sep[]))
164         {
165             // user specified a directory, complete it to a file
166             exe = std.path.join(exe, exeBasename);
167         }
168     }
169     else
170     {
171         //exe = exeBasename ~ '.' ~ hash(root, compilerFlags);
172         version (Posix)
173             exe = std.path.join(myOwnTmpDir, rel2abs(root)[1 .. $])
174                 ~ '.' ~ hash(root, compilerFlags);
175         else version (Windows)
176             exe = std.path.join(myOwnTmpDir, std.array.replace(root.dup, ".", "-"))
177                 ~ '-' ~ hash(root, compilerFlags);
178         else
179             assert(0);
180     }
181     // Add an ".exe" for Windows
182     exe ~= binExt;
183
184     // Have at it
185     if (isNewer(root, exe) ||
186             std.algorithm.find!
187                 ((string a) {return isNewer(a, exe);})
188                 (myDeps.keys).length)
189     {
190         invariant result = rebuild(root, exe, objDir, myDeps, compilerFlags,
191                                    addStubMain);
192         if (result) return result;
193     }
194
195     // run
196     version(Windows)
197     {
198         foreach(ref arg; programArgs)
199             arg = shellQuote(arg);
200         return buildOnly ? 0 : system(std.string.join([ exe ] ~ programArgs, " "));
201     }
202     else
203         return buildOnly ? 0 : execv(exe, [ exe ] ~ programArgs);
204 }
205
206 size_t indexOfProgram(ref string[] args)
207 {
208     foreach(i, arg; args)
209     {
210         if(i == 0)
211             continue;
212            
213         if(arg.length > 0 && arg[0] != '-' && arg[0] != '@')
214         {
215             if(
216                 !arg.endsWith(".obj") &&
217                 !arg.endsWith(".o") &&
218                 !arg.endsWith(".lib") &&
219                 !arg.endsWith(".a") &&
220                 !arg.endsWith(".def")
221             )
222             {
223                 return i;
224             }
225         }
226     }
227    
228     return args.length;
229 }
230
231 bool inALibrary(string source, in string object)
232 {
233     // Heuristics: if source starts with "std.", it's in a library
234     return std.string.startsWith(source, "std.")
235         || std.string.startsWith(source, "core.")
236         || source == "object" || source == "gcstats";
237     // another crude heuristic: if a module's path is absolute, it's
238     // considered to be compiled in a separate library. Otherwise,
239     // it's a source module.
240     //return isabs(mod);
241 }
242
243 private string myOwnTmpDir()
244 {
245     version (Posix)
246     {
247         enum tmpRoot = "/tmp/.rdmd";
248     }
249     else version (Windows)
250     {
251         auto tmpRoot = std.process.getenv("TEMP");
252         if (!tmpRoot)
253         {
254             tmpRoot = std.process.getenv("TMP");
255         }
256         if (!tmpRoot) tmpRoot = std.path.join(".", ".rdmd");
257         else tmpRoot ~= sep ~ ".rdmd";
258     }
259     exists(tmpRoot) && isdir(tmpRoot) || mkdirRecurse(tmpRoot);
260     return tmpRoot;
261 }
262
263 private string hash(in string root, in string[] compilerFlags)
264 {
265     enum string[] irrelevantSwitches = [
266         "--help", "-ignore", "-quiet", "-v" ];
267     MD5_CTX context;
268     context.start();
269     context.update(getcwd);
270     context.update(root);
271     foreach (flag; compilerFlags) {
272         if (find(irrelevantSwitches, flag).length) continue;
273         context.update(flag);
274     }
275     ubyte digest[16];
276     context.finish(digest);
277     return digestToString(digest);
278 }
279
280 private string getObjPath(in string root, in string[] compilerFlags)
281 {
282     const tmpRoot = myOwnTmpDir;
283     return std.path.join(tmpRoot,
284             "rdmd-" ~ basename(root) ~ '-' ~ hash(root, compilerFlags));
285 }
286
287 // Rebuild the executable fullExe starting from modules in myDeps
288 // passing the compiler flags compilerFlags. Generates one large
289 // object file.
290
291 private int rebuild(string root, string fullExe,
292         string objDir, in string[string] myDeps,
293         in string[] compilerFlags, bool addStubMain)
294 {
295     //auto todo = `..\SemiTwistDTools\bin\showargs`~" "~join(compilerFlags, " ")
296     //auto todo = "bin\\ddmd"~" "~join(compilerFlags, " ")
297     auto todo = " "~std.string.join(compilerFlags.dup, " ")
298         ~" -of"~shellQuote(fullExe)
299         ~" -od"~shellQuote(objDir)
300         ~" -I"~shellQuote(dirname(root))
301         ~" "~shellQuote(root)~" ";
302     foreach (k, objectFile; myDeps)
303     if(objectFile !is null) {
304         todo ~= k.shellQuote() ~ " ";
305     }
306
307     // Need to add the pesky void main(){}?
308     if (addStubMain)
309     {
310         auto stubMain = std.path.join(myOwnTmpDir, "stubmain.d");
311         std.file.write(stubMain, "void main(){}");
312         todo ~= stubMain;
313     }
314    
315     // Different shells and OS functions have different limits,
316     // but 1024 seems to be the smallest maximum outside of MS-DOS.
317     enum maxLength = 1024;
318     if(todo.length + compiler.length >= maxLength)
319     {
320         auto rspName = std.path.join(myOwnTmpDir,
321                 "rdmd." ~ hash(root, compilerFlags) ~ ".rsp");
322
323         // On Posix, DMD can't handle shell quotes in its response files.
324         version(Posix)
325         {
326             todo = " "~std.string.join(compilerFlags.dup, " ")
327                 ~" -of"~fullExe
328                 ~" -od"~objDir
329                 ~" -I"~dirname(root)
330                 ~" "~root~" ";
331             foreach (k, objectFile; myDeps)
332             if(objectFile !is null) {
333                 todo ~= k ~ " ";
334             }
335         }
336
337         std.file.write(rspName, todo);
338         todo = " " ~ shellQuote("@"~rspName);
339     }
340
341     todo = compiler ~ todo;
342     invariant result = run(todo);
343     if (result)
344     {
345         // build failed
346         return result;
347     }
348     // clean up the dir containing the object file
349     rmdirRecurse(objDir);
350     return 0;
351 }
352
353 // Run a program optionally writing the command line first
354
355 private int run(string todo)
356 {
357     if (chatty) writeln(todo);
358     if (dryRun) return 0;
359     return system(todo);
360 }
361
362 // Given module rootModule, returns a mapping of all dependees .d
363 // source filenames to their corresponding .o files sitting in
364 // directory objDir. The mapping is obtained by running dmd -v against
365 // rootModule.
366
367 private string[string] getDependencies(string rootModule, string objDir,
368         in string[] compilerFlags)
369 {
370     string d2obj(string dfile) {
371         return std.path.join(objDir, chomp(basename(dfile), ".d")~objExt);
372     }
373
374     immutable depsFilename = rootModule~".deps";
375     immutable rootDir = dirname(rootModule);
376    
377     // myDeps maps dependency paths to corresponding .o name (or null, if not a D module)
378     string[string] myDeps;// = [ rootModule : d2obj(rootModule) ];
379     // Must collect dependencies
380     invariant depsGetter = /*"cd "~shellQuote(rootDir)~" && "
381                              ~*/compiler~" "~std.string.join(compilerFlags.dup, " ")
382         ~" -v -o- "~shellQuote(rootModule)
383         ~" -I"~shellQuote(rootDir)
384         ~" >"~depsFilename;
385     if (chatty) writeln(depsGetter);
386     immutable depsExitCode = system(depsGetter);
387     if (depsExitCode)
388     {
389         // if (exists(depsFilename))
390         // {
391         //     stderr.writeln(readText(depsFilename));
392         // }
393         exit(depsExitCode);
394     }
395     auto depsReader = File(depsFilename);
396     scope(exit) collectException(depsReader.close); // we don't care for errors
397
398     // Fetch all dependencies and append them to myDeps
399     auto pattern = new RegExp(r"^(import|file|binary|config)\s+([^\(]+)\(?([^\)]*)\)?\s*$");
400     foreach (string line; lines(depsReader))
401     {
402         if (!pattern.test(line)) continue;
403         switch(pattern[1])
404         {
405         case "import":
406             invariant moduleName = pattern[2].strip(), moduleSrc = pattern[3].strip();
407             if (inALibrary(moduleName, moduleSrc)) continue;
408             invariant moduleObj = d2obj(moduleSrc);
409             myDeps[moduleSrc] = moduleObj;
410             break;
411            
412         case "file":
413             myDeps[pattern[3].strip()] = null;
414             break;
415            
416         case "binary", "config":
417             myDeps[pattern[2].strip()] = null;
418             break;
419            
420         default: assert(0);
421         }
422     }
423
424     return myDeps;
425 }
426
427 /*private*/ string shellQuote(string arg)
428 {
429     // This may have to change under windows
430     version (Windows) enum quotechar = '"';
431     else enum quotechar = '\'';
432     version (Windows)
433     {
434         // Escape trailing backslash, so it doesn't escape the ending quote.
435         // Backslashes elsewhere should NOT be escaped.
436         if(arg.length > 0 && arg[$-1] == '\\')
437             arg ~= '\\';
438         arg = std.array.replace(arg, `"`, `\"`);
439     }
440     return quotechar ~ arg ~ quotechar;
441 }
442
443 private bool isNewer(string source, string target)
444 {
445     return force || timeLastModified(source) >= timeLastModified(target, SysTime.min);
446 }
447
448 private string helpString()
449 {
450     return
451 "rdmd build "~thisVersion~"
452 Usage: rdmd [RDMD AND DMD OPTIONS]... program [PROGRAM OPTIONS]...
453 Builds (with dependents) and runs a D program.
454 Example: rdmd -release myprog --myprogparm 5
455
456 Any option to be passed to dmd must occur before the program name. In addition
457 to dmd options, rdmd recognizes the following options:
458   --build-only      just build the executable, don't run it
459   --chatty          write dmd commands to stdout before executing them
460   --compiler=comp   use the specified compiler (e.g. gdmd) instead of dmd
461   --dry-run         do not compile, just show what commands would be run
462                       (implies --chatty)
463   --eval=code       evaluate code \u00E0 la perl -e (multiple --eval allowed)
464   --force           force a rebuild even if apparently not necessary
465   --help            this message
466   --loop            assume \"foreach (line; stdin.byLine()) { ... }\" for eval
467   --main            add a stub main program to the mix (e.g. for unittesting)
468   --man             open web browser on manual page
469   --shebang         rdmd is in a shebang line (put as first argument)
470   -o+               don't delete object files
471 ";
472 }
473
474 // For --eval
475 immutable string importWorld = "
476 module temporary;
477 import std.stdio, std.algorithm, std.array, std.atomics, std.base64,
478     std.bigint, /*std.bind, std.bitarray,*/ std.bitmanip, /+std.boxer,+/
479     std.compiler, std.complex, std.conv, std.cpuid, std.cstream,
480     std.ctype, std.datetime, std.demangle, std.encoding, std.exception, std.file,
481     std.format, std.functional, std.getopt, std.intrinsic, std.iterator,
482     /*std.loader,*/ std.math, std.md5, std.metastrings, std.mmfile,
483     std.numeric, std.outbuffer, std.path, std.process,
484     std.random, std.range, std.regex, std.regexp, std.signals, std.socket,
485     std.socketstream, std.stdint, std.stdio, std.stdiobase, std.stream,
486     std.string, std.syserror, std.system, std.traits, std.typecons,
487     std.typetuple, std.uni, std.uri, std.utf, std.variant, std.xml, std.zip,
488     std.zlib;
489 ";
490
491 int eval(string todo)
492 {
493     MD5_CTX context;
494     context.start();
495     context.update(todo);
496     ubyte digest[16];
497     context.finish(digest);
498     auto pathname = myOwnTmpDir;
499     auto progname = std.path.join(pathname,
500             "eval." ~ digestToString(digest));
501
502     if (exists(progname) ||
503             // Compile it
504             (std.file.write(progname~".d", todo),
505                     run("dmd " ~ progname ~ ".d -of" ~ progname) == 0))
506     {
507         // It's there, just run it
508         run(progname);
509     }
510
511     // Clean pathname
512     enum lifetimeInHours = 24;
513     auto cutoff = Clock.currTime() + dur!"hours"(lifetimeInHours);
514     foreach (DirEntry d; dirEntries(pathname, SpanMode.shallow))
515     {
516         if (d.timeLastModified < cutoff)
517         {
518             std.file.remove(d.name);
519             //break; // only one per call so we don't waste time
520         }
521     }
522    
523     return 0;
524 }
525
526 string thisVersion()
527 {
528     enum d = __DATE__;
529     enum month = d[0 .. 3],
530         day = d[4] == ' ' ? "0"~d[5] : d[4 .. 6],
531         year = d[7 .. $];
532     enum monthNum
533         = month == "Jan" ? "01"
534         : month == "Feb" ? "02"
535         : month == "Mar" ? "03"
536         : month == "Apr" ? "04"
537         : month == "May" ? "05"
538         : month == "Jun" ? "06"
539         : month == "Jul" ? "07"
540         : month == "Aug" ? "08"
541         : month == "Sep" ? "09"
542         : month == "Oct" ? "10"
543         : month == "Nov" ? "11"
544         : month == "Dec" ? "12"
545         : "";
546     static assert(month != "", "Unknown month "~month);
547     return year[0]~year[1 .. $]~monthNum~day;
548 }
549
550 /*
551  *  Copyright (C) 2008 by Andrei Alexandrescu
552  *  Written by Andrei Alexandrescu, www.erdani.org
553  *  Based on an idea by Georg Wrede
554  *  Featuring improvements suggested by Christopher Wright
555  *  Windows port using bug fixes and suggestions by Adam Ruppe
556  * 
557  *  This software is provided 'as-is', without any express or implied
558  *  warranty. In no event will the authors be held liable for any damages
559  *  arising from the use of this software.
560  *
561  *  Permission is granted to anyone to use this software for any purpose,
562  *  including commercial applications, and to alter it and redistribute it
563  *  freely, subject to the following restrictions:
564  *
565  *  o  The origin of this software must not be misrepresented; you must not
566  *     claim that you wrote the original software. If you use this software
567  *     in a product, an acknowledgment in the product documentation would be
568  *     appreciated but is not required.
569  *  o  Altered source versions must be plainly marked as such, and must not
570  *     be misrepresented as being the original software.
571  *  o  This notice may not be removed or altered from any source
572  *     distribution.
573  */
Note: See TracBrowser for help on using the browser.