// Written in the D programming language. import std.algorithm, std.array, std.c.stdlib, std.datetime, std.exception, std.file, std.getopt, std.md5, std.path, std.process, std.regexp, std.stdio, std.string, std.typetuple; version (Posix) { enum objExt = ".o"; enum binExt = ""; } else version (Windows) { import std.c.windows.windows; extern(Windows) HINSTANCE ShellExecuteA(HWND, LPCSTR, LPCSTR, LPCSTR, LPCSTR, INT); enum objExt = ".obj"; enum binExt = ".exe"; } else { static assert(0, "Unsupported operating system."); } private bool chatty, buildOnly, dryRun, force; private string exe, compiler = "dmd"; int main(string[] args) { //writeln("Invoked with: ", map!(q{a ~ ", "})(args)); if (args.length > 1 && std.algorithm.startsWith(args[1], "--shebang ", "--shebang=")) { // multiple options wrapped in one auto a = args[1]["--shebang ".length .. $]; args = args[0 .. 1] ~ std.string.split(a) ~ args[2 .. $]; } // Continue parsing the command line; now get rdmd's own arguments // parse the -o option void dashOh(string key, string value) { if (value[0] == 'f') { // -ofmyfile passed exe = value[1 .. $]; } else if (value[0] == 'd') { // -odmydir passed if(!exe) // Don't let -od override -of { // add a trailing path separator to clarify it's a dir exe = value[1 .. $]; if (!std.algorithm.endsWith(exe, std.path.sep[])) { exe ~= std.path.sep[]; } assert(std.algorithm.endsWith(exe, std.path.sep[])); } } else if (value[0] == '-') { // -o- passed enforce(false, "Option -o- currently not supported by rdmd"); } else { enforce(false, "Unrecognized option: "~key~value); } } // start the web browser on documentation page void man() { version(Windows) { // invoke browser that is associated with the http protocol ShellExecuteA(null, "open", "http://www.digitalmars.com/d/2.0/rdmd.html", null, null, SW_SHOWNORMAL); } else { foreach (b; [ std.process.getenv("BROWSER"), "firefox", "sensible-browser", "x-www-browser" ]) { if (!b.length) continue; if (!system(b~" http://www.digitalmars.com/d/2.0/rdmd.html")) return; } } } auto programPos = indexOfProgram(args); // Insert "--" to tell getopts when to stop args = args[0..programPos] ~ "--" ~ args[programPos .. $]; bool bailout; // bailout set by functions called in getopt if // program should exit string[] loop; // set by --loop bool addStubMain;// set by --main string[] eval; // set by --eval bool makeDepend; getopt(args, std.getopt.config.caseSensitive, std.getopt.config.passThrough, std.getopt.config.stopOnFirstNonOption, "build-only", &buildOnly, "chatty", &chatty, "compiler", &compiler, "dry-run", &dryRun, "eval", &eval, "loop", &loop, "force", &force, "help", (string) { writeln(helpString); bailout = true; }, "main", &addStubMain, "makedepend", &makeDepend, "man", (string) { man; bailout = true; }, "o", &dashOh); if (bailout) return 0; if (dryRun) chatty = true; // dry-run implies chatty // Just evaluate this program! if (loop) { return .eval(importWorld ~ "void main(char[][] args) { " ~ "foreach (line; stdin.byLine()) {\n" ~ std.string.join(loop, "\n") ~ ";\n} }"); } if (eval) { return .eval(importWorld ~ "void main(char[][] args) {\n" ~ std.string.join(eval, "\n") ~ ";\n}"); } // Parse the program line - first find the program to run programPos = indexOfProgram(args); if (programPos == args.length) { write(helpString); return 1; } auto root = /*rel2abs*/(chomp(args[programPos], ".d") ~ ".d"), exeBasename = basename(root, ".d"), exeDirname = dirname(root), programArgs = args[programPos + 1 .. $]; args = args[0 .. programPos]; auto compilerFlags = args[1 .. programPos - 1]; // Compute the object directory and ensure it exists immutable objDir = getObjPath(root, compilerFlags); // Fetch dependencies const myModules = getDependencies(root, objDir, compilerFlags); writeln("dependencies returned"); // --makedepend mode. Just print dependencies and exit. if (makeDepend) { stdout.write(root, " :"); foreach (mod, _; myModules) { stdout.write(' ', mod); } stdout.writeln(); return 0; } if (!dryRun) { exists(objDir) ? enforce(dryRun || isdir(objDir), "Entry `"~objDir~"' exists but is not a directory.") : mkdir(objDir); } // Compute executable name, check for freshness, rebuild if (exe) { // user-specified exe name if (std.algorithm.endsWith(exe, std.path.sep[])) { // user specified a directory, complete it to a file exe = std.path.join(exe, exeBasename); } } else { //exe = exeBasename ~ '.' ~ hash(root, compilerFlags); version (Posix) exe = std.path.join(myOwnTmpDir, rel2abs(root)[1 .. $]) ~ '.' ~ hash(root, compilerFlags); else version (Windows) exe = std.path.join(myOwnTmpDir, replace(root, ".", "-")) ~ '-' ~ hash(root, compilerFlags); else static assert(0); } // Add an ".exe" for Windows exe ~= binExt; // Have at it if (isNewer(root, exe) || std.algorithm.find! ((string a) {return isNewer(a, exe);}) (myModules.keys).length) { immutable result = rebuild(root, exe, objDir, myModules, compilerFlags, addStubMain); if (result) return result; } if (buildOnly) { // Pretty much done! return 0; } // run version (Windows) { foreach(ref arg; programArgs) arg = shellQuote(arg); return system(std.string.join([ exe ] ~ programArgs, " ")); } else { return execv(exe, [ exe ] ~ programArgs); } } size_t indexOfProgram(string[] args) { foreach(i, arg; args) { if (i > 0 && !arg.startsWith('-', '@') && !arg.endsWith(".obj", ".o", ".lib", ".a", ".def")) { return i; } } return args.length; } bool inALibrary(string source, in string object) { // Heuristics: if source starts with "std.", it's in a library return std.string.startsWith(source, "std.") || std.string.startsWith(source, "core.") || source == "object" || source == "gcstats"; // another crude heuristic: if a module's path is absolute, it's // considered to be compiled in a separate library. Otherwise, // it's a source module. //return isabs(mod); } private string myOwnTmpDir() { version (Posix) { enum tmpRoot = "/tmp/.rdmd"; } else version (Windows) { auto tmpRoot = std.process.getenv("TEMP"); if (!tmpRoot) { tmpRoot = std.process.getenv("TMP"); } if (!tmpRoot) tmpRoot = std.path.join(".", ".rdmd"); else tmpRoot ~= sep ~ ".rdmd"; } exists(tmpRoot) && isdir(tmpRoot) || mkdirRecurse(tmpRoot); return tmpRoot; } private string hash(in string root, in string[] compilerFlags) { enum string[] irrelevantSwitches = [ "--help", "-ignore", "-quiet", "-v" ]; MD5_CTX context; context.start(); context.update(getcwd); context.update(root); foreach (flag; compilerFlags) { if (find(irrelevantSwitches, flag).length) continue; context.update(flag); } ubyte digest[16]; context.finish(digest); return digestToString(digest); } private string getObjPath(in string root, in string[] compilerFlags) { const tmpRoot = myOwnTmpDir; return std.path.join(tmpRoot, "rdmd-" ~ basename(root) ~ '-' ~ hash(root, compilerFlags)); } // Rebuild the executable fullExe starting from modules myModules // passing the compiler flags compilerFlags. Generates one large // object file. private int rebuild(string root, string fullExe, string objDir, in string[string] myModules, string[] compilerFlags, bool addStubMain) { auto todo = std.string.join(compilerFlags, " ") ~" -of"~shellQuote(fullExe) ~" -od"~shellQuote(objDir) ~" -I"~shellQuote(dirname(root)) ~" "~shellQuote(root)~" "; foreach (k; map!(shellQuote)(myModules.keys)) { todo ~= k ~ " "; } // Need to add void main(){}? if (addStubMain) { auto stubMain = std.path.join(myOwnTmpDir, "stubmain.d"); std.file.write(stubMain, "void main(){}"); todo ~= stubMain; } // Different shells and OS functions have different limits, // but 1024 seems to be the smallest maximum outside of MS-DOS. enum maxLength = 1024; if (todo.length + compiler.length >= maxLength) { auto rspName = std.path.join(myOwnTmpDir, "rdmd." ~ hash(root, compilerFlags) ~ ".rsp"); // On Posix, DMD can't handle shell quotes in its response files. version(Posix) { todo = std.string.join(compilerFlags.dup, " ") ~" -of"~fullExe ~" -od"~objDir ~" -I"~dirname(root) ~" "~root~" "; foreach (k; myModules.keys) { todo ~= k ~ " "; } } std.file.write(rspName, todo); todo = shellQuote("@"~rspName); } immutable result = run(compiler ~ " " ~ todo); if (result) { // build failed return result; } // clean up the dir containing the object file, just not in dry // run mode because we haven't created any! if (!dryRun) { rmdirRecurse(objDir); } return 0; } // Run a program optionally writing the command line first private int run(string todo) { if (chatty) writeln(todo); if (dryRun) return 0; return system(todo); } // Given module rootModule, returns a mapping of all dependees .d // source filenames to their corresponding .o files sitting in // directory objDir. The mapping is obtained by running dmd -v against // rootModule. private string[string] getDependencies(string rootModule, string objDir, string[] compilerFlags) { string d2obj(string dfile) { return std.path.join(objDir, chomp(basename(dfile), ".d")~objExt); } immutable depsFilename = rootModule~".deps"; immutable rootDir = dirname(rootModule); // myModules maps module source paths to corresponding .o names string[string] myModules;// = [ rootModule : d2obj(rootModule) ]; // Must collect dependencies immutable depsGetter = /*"cd "~shellQuote(rootDir)~" && " ~*/compiler~" "~std.string.join(compilerFlags, " ") ~" -v -o- "~shellQuote(rootModule) ~" -I"~shellQuote(rootDir) ~" >"~depsFilename; if (chatty) writeln(depsGetter); immutable depsExitCode = system(depsGetter); if (depsExitCode) { stderr.writeln("Failed: ", depsGetter); exit(depsExitCode); } auto depsReader = File(depsFilename); // Leave the deps file in place in case of failure, maybe the user // wants to take a look at it scope(success) collectException(std.file.remove(depsFilename)); scope(exit) collectException(depsReader.close); // don't care for errors // Fetch all dependent modules and append them to myModules auto pattern = new RegExp(r"^import\s+(\S+)\s+\((\S+)\)\s*$"); foreach (string line; lines(depsReader)) { if (!pattern.test(line)) continue; immutable moduleName = pattern[1], moduleSrc = pattern[2]; if (inALibrary(moduleName, moduleSrc)) continue; immutable moduleObj = d2obj(moduleSrc); // 2011-06-05: dmd outputs dependencies relative to the path // in which the compiler is run, not to the path of the root // module. Therefore issue deps relative to current dir. // //myModules[/*rel2abs*/std.path.join(rootDir, moduleSrc)] = moduleObj; myModules[moduleSrc] = moduleObj; } writeln("returning dependencies"); return myModules; } /*private*/ string shellQuote(string arg) { // This may have to change under windows version (Windows) enum quotechar = '"'; else enum quotechar = '\''; version (Windows) { // Escape trailing backslash, so it doesn't escape the ending quote. // Backslashes elsewhere should NOT be escaped. if(arg.length > 0 && arg[$-1] == '\\') arg ~= '\\'; arg = std.array.replace(arg, `"`, `\"`); } return quotechar ~ arg ~ quotechar; } private bool isNewer(string source, string target) { return force || timeLastModified(source) >= timeLastModified(target, SysTime(0)); } private string helpString() { return "rdmd build "~thisVersion~" Usage: rdmd [RDMD AND DMD OPTIONS]... program [PROGRAM OPTIONS]... Builds (with dependents) and runs a D program. Example: rdmd -release myprog --myprogparm 5 Any option to be passed to dmd must occur before the program name. In addition to dmd options, rdmd recognizes the following options: --build-only just build the executable, don't run it --chatty write dmd commands to stdout before executing them --compiler=comp use the specified compiler (e.g. gdmd) instead of dmd --dry-run do not compile, just show what commands would be run (implies --chatty) --eval=code evaluate code \u00E0 la perl -e (multiple --eval allowed) --force force a rebuild even if apparently not necessary --help this message --loop assume \"foreach (line; stdin.byLine()) { ... }\" for eval --main add a stub main program to the mix (e.g. for unittesting) --makedepend print dependencies in makefile format and exit --man open web browser on manual page --shebang rdmd is in a shebang line (put as first argument) "; } // For --eval immutable string importWorld = " module temporary; import std.stdio, std.algorithm, std.array, std.base64, std.bigint, std.bitmanip, std.compiler, std.complex, std.conv, std.cpuid, std.cstream, std.ctype, std.datetime, std.demangle, std.encoding, std.exception, std.file, std.format, std.functional, std.getopt, std.math, std.md5, std.metastrings, std.mmfile, std.numeric, std.outbuffer, std.path, std.process, std.random, std.range, std.regex, std.regexp, std.signals, std.socket, std.socketstream, std.stdint, std.stdio, std.stdiobase, std.stream, std.string, std.syserror, std.system, std.traits, std.typecons, std.typetuple, std.uni, std.uri, std.utf, std.variant, std.xml, std.zip, std.zlib; "; int eval(string todo) { MD5_CTX context; context.start(); context.update(todo); ubyte digest[16]; context.finish(digest); auto pathname = myOwnTmpDir; auto progname = std.path.join(pathname, "eval." ~ digestToString(digest)); auto binName = progname ~ binExt; if (exists(binName) || // Compile it (std.file.write(progname~".d", todo), run(compiler ~ " " ~ progname ~ ".d -of" ~ binName) == 0)) { // It's there, just run it run(binName); } // Clean pathname enum lifetimeInHours = 24; auto cutoff = Clock.currTime() - dur!"hours"(lifetimeInHours); foreach (DirEntry d; dirEntries(pathname, SpanMode.shallow)) { if (d.timeLastModified < cutoff) { std.file.remove(d.name); //break; // only one per call so we don't waste time } } return 0; } string thisVersion() { enum d = __DATE__; enum month = d[0 .. 3], day = d[4] == ' ' ? "0"~d[5] : d[4 .. 6], year = d[7 .. $]; enum monthNum = month == "Jan" ? "01" : month == "Feb" ? "02" : month == "Mar" ? "03" : month == "Apr" ? "04" : month == "May" ? "05" : month == "Jun" ? "06" : month == "Jul" ? "07" : month == "Aug" ? "08" : month == "Sep" ? "09" : month == "Oct" ? "10" : month == "Nov" ? "11" : month == "Dec" ? "12" : ""; static assert(month != "", "Unknown month "~month); return year[0]~year[1 .. $]~monthNum~day; } /* * Copyright (C) 2008 by Andrei Alexandrescu * Written by Andrei Alexandrescu, www.erdani.org * Based on an idea by Georg Wrede * Featuring improvements suggested by Christopher Wright * Windows port using bug fixes and suggestions by Adam Ruppe * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * o The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * o Altered source versions must be plainly marked as such, and must not * be misrepresented as being the original software. * o This notice may not be removed or altered from any source * distribution. */