Okay, here's an improved version of rdmd with config files.
Added features:
- Config file
- Specify a compiler
- Specify default imports for --eval
- Specify build flags
- Option to show source with --eval
- Changed the definition of "main" with --eval to use 'in char[][]'
rather than 'string[]' (the args probably don't matter much anyway)
- Executables are stored in the temp directory, not the current
directory. This way, they can be reused even if you change directories;
and they don't clutter up your working directory.
Attached is a sample rdmd.conf as well as the modified source (based on
r958 from the phobos repository).
// Written in the D programming language.
/*
* Copyright (C) 2008 by Andrei Alexandrescu
* Written by Andrei Alexandrescu, www.erdani.org
*
* 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.
*/
import std.getopt, std.string, std.process, std.stdio, std.contracts, std.file,
std.algorithm, std.iterator, std.md5, std.path, std.regexp, std.getopt,
std.c.stdlib, std.date, std.process;
private bool chatty, buildOnly, dryRun, force, showSource;
private string exe, compiler = "dmd";
// TODO: should we use these imports as a matter of convenience
// in all scripts, not just --eval scripts?
private string imports;
private string flags;
// For --eval
immutable string importWorld = "
import std.stdio, std.algorithm, std.array, std.atomics, std.base64,
std.bigint, std.bind, /*std.bitarray,*/ std.bitmanip, std.boxer,
std.compiler, std.complex, std.contracts, std.conv, std.cpuid, std.cstream,
std.ctype, std.date, std.dateparse, std.demangle, std.encoding, std.file,
std.format, std.functional, std.getopt, std.intrinsic, std.iterator,
/*std.loader,*/ std.math, std.md5, std.metastrings, std.mmfile,
std.numeric, std.openrj, std.outbuffer, std.path, std.perf, 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 main(string[] args)
{
//writeln("Invoked with: ", map!(q{a ~ ", "})(args));
if (args.length > 1 && std.string.startsWith(args[1], "--shebang "))
{
// multiple options wrapped in one
auto a = args[1]["--shebang ".length .. $];
args = args[0 .. 1] ~ 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
// add a trailing path separator to clarify it's a dir
exe = std.path.join(value[1 .. $], "");
assert(std.string.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()
{
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;
}
}
// set by functions called in getopt if program should exit
bool bailout, loop;
string eval;
getopt(args,
std.getopt.config.caseSensitive,
std.getopt.config.passThrough,
std.getopt.config.stopOnFirstNonOption,
"build-only", &buildOnly,
"chatty", &chatty,
"dry-run", &dryRun,
"force", &force,
"show", (string) { showSource = true; },
"help", (string) { writeln(helpString); bailout = true; },
"man", (string) { man; bailout = true; },
"eval", &eval,
"loop", &loop,
"o", &dashOh,
"compiler", &compiler);
if (bailout) return 0;
if (dryRun) chatty = true; // dry-run implies chatty
// read the config file
findcfg();
if (eval)
{
// Just evaluate this program!
if (loop)
{
return .eval(imports ~ "void main(in char[][] args) { "
~ "foreach (line; stdin.byLine()) { " ~ eval ~ "; } }");
}
else
{
return .eval(imports ~ "void main(in char[][] args) { "
~ eval ~ "; }");
}
}
// Parse the program line - first find the program to run
uint programPos = 1;
for (;; ++programPos)
{
if (programPos == args.length)
{
write(helpString);
return 1;
}
if (args[programPos].length && args[programPos][0] != '-') break;
}
const
root = /*rel2abs*/(chomp(args[programPos], ".d") ~ ".d"),
exeBasename = basename(root, ".d"),
programArgs = args[programPos + 1 .. $];
args = args[0 .. programPos];
const compilerFlags = args[1 .. programPos] ~ flags;
// Compute the object directory and ensure it exists
invariant objDir = getObjPath(root, compilerFlags);
if (!dryRun) // only make a fuss about objDir on a real run
{
exists(objDir)
? enforce(isdir(objDir),
"Entry `"~objDir~"' exists but is not a directory.")
: mkdir(objDir);
}
// Fetch dependencies
const myModules = getDependencies(root, objDir, compilerFlags);
// Compute executable name, check for freshness, rebuild
if (exe)
{
// user-specified exe name
if (std.string.endsWith(exe, std.path.sep))
{
// user specified a directory, complete it to a file
exe = std.path.join(exe, exeBasename);
}
}
else
{
// On some systems, the temp dir will be mounted noexec
// so we should allow a way to specify a temp directory
// but this is better than dumping everything in the
// current directory
exe = std.path.join(tmpDir, exeBasename ~ '.' ~ hash(root, compilerFlags));
}
// Have at it
if (isNewer(root, exe) ||
canFind!((string a) {return isNewer(a, exe);})(myModules.keys))
{
invariant result = rebuild(root, exe, objDir, myModules, compilerFlags);
if (result) return result;
}
// run
return buildOnly ? 0 : execv(exe, [ exe ] ~ programArgs);
}
bool inALibrary(in string source, in string object)
{
// Heuristics: if source starts with "std.", it's in a library
// This is cruddy and I can't stand to look at it. -cw
return std.string.startsWith(source, "std.")
|| std.string.startsWith(source, "core.")
|| std.string.startsWith(source, "tango.")
|| 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 tmpDir()
{
version (linux)
{
enum tmpRoot = "/tmp";
}
else version (Windows)
{
auto tmpRoot = std.process.getenv("TEMP");
if (!tmpRoot)
{
tmpRoot = std.process.getenv("TMP");
if (!tmpRoot) 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 (canFind(irrelevantSwitches, flag)) continue;
context.update(flag);
}
ubyte digest[16];
context.finish(digest);
return digestToString(digest);
}
private string getObjPath(in string root, in string[] compilerFlags)
{
const tmpRoot = tmpDir;
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,
in string[] compilerFlags)
{
auto todo = compiler~" "~join(compilerFlags, " ")
~" -of"~shellQuote(fullExe)
~" -od"~shellQuote(objDir)
~" "~shellQuote(root)~" ";
foreach (k; map!(shellQuote)(myModules.keys)) {
todo ~= k ~ " ";
}
invariant result = run(todo);
if (result)
{
// build failed
return result;
}
// clean up the object file, not needed anymore
//remove(std.path.join(objDir, basename(root, ".d")~".o"));
// clean up the dir containing the object file
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,
in string[] compilerFlags)
{
string d2obj(string dfile) {
return std.path.join(objDir, chomp(basename(dfile), ".d")~".o");
}
// myModules maps module source paths to corresponding .o names
string[string] myModules;// = [ rootModule : d2obj(rootModule) ];
// Must collect dependencies
invariant depsGetter = compiler~" "~join(compilerFlags, " ")
~" -v -o- "~shellQuote(rootModule);
if (chatty) writeln(depsGetter);
FILE* depsReader = popen(depsGetter);
scope(exit) fclose(depsReader);
// 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;
invariant moduleName = pattern[1], moduleSrc = pattern[2];
if (inALibrary(moduleName, moduleSrc)) continue;
invariant moduleObj = d2obj(moduleSrc);
myModules[/*rel2abs*/(moduleSrc)] = moduleObj;
}
return myModules;
}
/*private*/ string shellQuote(string filename)
{
// This may have to change under windows
version (Windows) enum quotechar = '"';
else enum quotechar = '\'';
return quotechar ~ filename ~ quotechar;
}
private bool isNewer(string source, string target)
{
return force || lastModified(source) >= lastModified(target, d_time.min);
}
private string helpString()
{
return
"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
--show if --eval is specified, write out the full source code
--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)
--force force a rebuild even if apparently not necessary
--eval=code evaluate code a la perl -e
--loop assume \"foreach (line; stdin.byLine()) { ... }\" for eval
--help this message
--man open web browser on manual page
--shebang rdmd is in a shebang line (put as first argument)
";
}
int eval(string todo)
{
auto progname = tmpDir~"/eval";
compile(progname, todo, true);
return 0;
}
bool compile(string progname, string program, bool run)
{
if (showSource)
writefln("%s", program);
string file = progname ~ ".d";
std.file.write(file, program);
scope(exit) std.file.remove(file);
string command;
if (run) command = format("%s %s -run %s", compiler, flags, file);
else command = format("%s %s %s -of %s", compiler, file, flags, progname);
return (.run(command) == 0);
}
version (Windows)
{
// Conforming to 4.4BSD -- this should be widely available
extern(C) DWORD GetModuleFileName(void* ptr, char* buf, DWORD size);
string exePath()
{
DWORD length;
char[1024] buf;
auto length = GetModuleFileName(null, buf.ptr, buf.length);
if (length)
return assumeUnique(buf[0..length]);
return null;
}
}
version (Posix)
{
// Conforming to 4.4BSD -- this should be widely available
extern(C) size_t readlink(const char* path, char* buf, size_t buflength);
string exePath()
{
char[1024] exe;
// linux, darwin, solaris support /proc/self/exe
// freebsd is a laggard
auto size = readlink("/proc/self/exe".ptr, &exe[0], exe.length);
if (size >= 0)
{
return assumeUnique(exe[0..size]);
}
return null;
}
}
string exeDirectory()
{
auto exe = exePath();
if (exe)
{
auto dir = dirname(exe);
return dir;
}
return null;
}
void findcfg()
{
version (Windows)
{
// TODO: standard search path on Windows?
string[] search = [];
}
else version (Posix)
{
string[] search = ["/etc/rdmd.conf", "/usr/local/etc/rdmd.conf"];
}
if (exeDirectory())
search ~= std.path.join(exeDirectory(), "rdmd.conf");
foreach (loc; search)
{
if (readcfg(loc)) return;
}
imports = importWorld;
}
bool readcfg(string filename)
{
if (!std.file.exists(filename))
return false;
auto contents = cast(string)std.file.read(filename);
parse(contents);
return true;
}
void parse(string config)
{
foreach (i, line; std.string.splitlines(config))
{
if (line[0] == '#') continue;
auto index = std.string.find(line, '=');
if (index <= 0 || index >= line.length)
{
writefln("Malformed line in config file line %s: %s", i, line);
}
auto parts = [line[0..index], line[index..$]];
auto name = std.string.strip(line[0..index]);
auto value = std.string.strip(line[1 + index..$]);
debug(config) writefln("name=%s value=%s", name, value);
switch (name)
{
case "compiler":
debug(config) writefln("setting compiler (%s)", value);
compiler = value;
break;
case "import":
debug(config) writefln("adding import (%s)", value);
imports ~= value ~ ", ";
break;
case "flags":
debug(config) writefln("setting flags (%s)", value);
flags = value;
break;
default:
}
}
if (imports.length)
imports = "import " ~ imports[0..$-2] ~ ";";
else
imports = importWorld;
}
compiler=dmd
includedir=/usr/local/include/d
import=tango.io.Stdout
#import=tango.text.Regex;
flags=-defaultlib=tango-base-dmd -debuglib=tango-base-dmd
-I/usr/local/include/d -L-ltango-user-dmd