root/trunk/tools/rdmd.d

Revision 2169, 14.8 kB (checked in by rsinfu, 1 year ago)

Fixed bug 5133: dmd fails to build rdmd (problem with startsWith).

Also replaced invariant -> immutable.

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