Andrei Alexandrescu wrote:
Georg Wrede wrote:
Should I be able to

$ rdmd --eval='printf("Yay, rdmd!")'


Yah. For my money, I can't fathom working in D without rdmd. Now with --eval and passable regexes it's even better because I can easily do tasks (from shell files) that would take longer in Perl.

Since the current source uses the not-yet-released phobos, I wanted to attach a binary so you can try it, but it's too big for the news server.


Andrei

It would be nice to have a config file that lets me specify the default imports and compiler (though this would make it slower). I can send you a patch for this, but I can't seem to build rdmd without modifications; it requires a struct File that has a method popen(string). I can change File to FILE* and make a few minor changes to get it to compile, which is what I did.

Anyway, my modifications look for a config file:
in /etc/
in /usr/local/etc
in the exe directory

Currently it just looks for default imports and the compiler name. Other things to add are compiler flags, and maybe dsss support.

The solution for getting the exe directory isn't included in phobos, as far as I could tell. Perhaps you'd like to change that.
// 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;
private string exe, compiler = "dmd";
string imports;

// 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 .. $];
    }

	// Read the config file
	findcfg();
    
    // 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,
            "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

    if (eval)
    {
        // Just evaluate this program!
        if (loop)
        {
            return .eval(importWorld ~ "void main(string[] args) { "
                ~ "foreach (line; stdin.byLine()) { " ~ eval ~ "; } }");
        }
        else
        {
            return .eval(importWorld ~ "void main(string[] 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];

    // 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
    {
        exe = 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)
{
	// TODO: move this into config file
    // Heuristics: if source starts with "std.", it's in a library
    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);
    auto depsReader = popen(depsGetter);
    scope(exit) collectException(fclose(depsReader)); // we 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;
        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
  --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.d";
    std.file.write(progname, todo);
    scope(exit) std.file.remove(progname);
    run(compiler ~ " -run " ~ progname);
    return 0;
}

bool compile(string progname, string program)
{
	string file = progname ~ ".d";
	std.file.write(file, program);
	string command = format("%s %s -of %s", compiler, file, 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 length = readlink("/proc/self/exe".ptr, &exe[0], exe.length);
		if (length >= 0)
		{
			return assumeUnique(exe[0..length]);
		}
		return null;
	}
}

string exeDirectory()
{
	auto exe = exePath();
	if (exe)
	{
		auto dir = dirname(exe[0..std.string.find(exe, '\0')]);
		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 parts = std.string.split(line, "=");
		if (parts.length <= 1)
		{
			writefln("Malformed line in config file line %s: %s", i, line);
		}
		auto name = std.string.strip(parts[0]);
		auto value = std.string.strip(parts[1]);
		switch (name)
		{
			case "compiler":
				compiler = value;
				break;
			case "import":
				imports ~= value ~ ", ";
				break;
			default:
		}
	}
	if (imports.length)
		imports = "import " ~ imports[0..$-2] ~ ";";
	else
		imports = importWorld;
}

Reply via email to