view build-tool/configure_functions.d @ 135:be50d20643a1

Depend on only the things we need.
author David Bryant <bagnose@gmail.com>
date Mon, 17 Sep 2012 11:49:45 +0930
parents 89e8b0d92f36
children
line wrap: on
line source

// Functions used by various configure scripts to set up a build environment
// suitable for building with bob and running/deploying.
//
// Expected to be called from a configure.d in a project directory with code like
// the following abbreviated example:
/*
void main(string args[]) {
    auto data = initialise(args, "project-name");

    usePackage(data, "libssh2", Constraint.AtLeast, "1.2");
    useHeader( data, "gcrypt.h");
    useLibrary(data, "libgcrypt.so");
    useExecutable(data, "IMAGE_MAGICK_CONVERT", ["convert"]);

    appendRunVar(data, "GST_PLUGIN_PATH", ["${DIST_PATH}/lib/plugins"]);
    appendBobVar(data, "CCFLAGS", ["-DUSE_BATHY_CHARTING_RASTER_SOURCE"]);

    finalise(data, ["open", "reuse"]); // all packages in this and specified other repos
}
*/


module configure_functions;

import std.string;
import std.getopt;
import std.path;
import std.file;
import std.process;
import std.stdio;
import std.conv;

import core.stdc.stdlib;
import core.sys.posix.sys.stat;




private void setMode(string path, uint mode) {
    chmod(toStringz(path), mode);
}


//
// Config - a data structure to accumulate configure information.
//

enum Priority { User, Env, Project, System } // highest priority first
enum Use      { Inc, Bin, Lib, Pkg }

struct Config {
    int                     verboseConfigure;
    string                  buildLevel;
    string                  productVersion;
    string                  backgroundCopyright;
    string                  foregroundCopyright;
    string                  buildDir;
    bool[string]            architectures;
    string[][Priority][Use] dirs;
    string[][Use]           prevDirs;
    string[][string]        bobVars;
    string[][string]        runVars;
    string[][string]        buildVars;
    string                  reason;
    string[]                configureOptions;
    string                  srcDir;
}

bool[string] barred;


//
// Return the paths that the linker (ld) searches for libraries.
//
string[] linkerSearchPaths() {
    // Note 1: The method used is quite hacky and probably very non-portable.
    // Note 2: An alternative method (no less hacky) would parse the linker script
    // for SEARCH_DIR commands.
    string improbable_name = "this_is_an_extremely_improbable_library_name";
    string command_base = "LIBRARY_PATH= LD_LIBRARY_PATH= ";
    command_base ~= "ld --verbose -lthis_is_an_extremely_improbable_library_name < /dev/null";
    // First command is to check that ld is working and giving the expected error message
    // Something about the ld command means the output must be piped through something (e.g. "cat -")
    // otherwise std.process.shell() will throw 'Could not close pipe'.
    string expected_message = "ld: cannot find -l" ~ improbable_name;
    string command_1 = command_base ~ " 2>&1 > /dev/null | cat -";

    // Second command is to get the output containing the paths that ld searches for libraries.
    string good_prefix = "attempt to open ";
    string good_suffix = "lib" ~ improbable_name ~ ".so failed";
    string command_2 = command_base ~ " 2> /dev/null | grep '^" ~ good_prefix ~ ".*" ~ good_suffix ~ "$'";

    string[] result;
    try {
        //writefln("command_1=%s\n", command_1);
        string[] ld_lines = std.string.splitLines(std.process.shell(command_1));
        //if (expected_message in ld_lines) {
        bool success = false;
        foreach (line; ld_lines) {
            if (line == expected_message) { success = true; break; }
        }
        if (success) {
            //writefln("command_2=%s\n", command_2);
            ld_lines = std.string.splitLines(std.process.shell(command_2));
            foreach (line; ld_lines) {
                if (line.length > good_prefix.length + good_suffix.length + 1) {
                    if (line[0 .. good_prefix.length] == good_prefix &&
                        line[$-good_suffix.length .. $] == good_suffix) {
                        //writefln("Match: \"%s\"", line);
                        result ~= line[good_prefix.length .. $-good_suffix.length-1];
                    }
                }
            }
            return result;
        }
        else {
            writefln("Did not get expected output from ld...\ncommand=%s\noutput:", command_1);
            foreach (line; ld_lines) { writefln(line); }
            exit(1);
        }
    }
    catch (Exception ex) {
        writefln("Error running ld: %s", ex);
        exit(1);
    }
    assert(0); // TODO Why aren't the above exit() calls adequate to satisfy the compiler?
}


//
// Append some tokens to the end of a bob, run or build variable,
// appending only if not already present and preserving order.
//
private void appendVar(ref string[] strings, string[] extra) {
    foreach (string item; extra) {
        if (item in barred) continue;
        bool got = false;
        foreach (string have; strings) {
            if (item == have) {
                got = true;
                break;
            }
        }
        if (!got) {
            strings ~= item;
        }
    }
}
void appendBobVar(ref Config data, string var, string[] tokens) {
    if (data.verboseConfigure >= 2) { writefln("appendBobVar: %s %s", var, tokens); }
    if (var !in data.bobVars) {
        data.bobVars[var] = null;
    }
    appendVar(data.bobVars[var], tokens);
}
void appendRunVar(ref Config data, string var, string[] tokens) {
    if (data.verboseConfigure >= 2) { writefln("appendRunVar: %s %s", var, tokens); }
    if (var !in data.runVars) {
        data.runVars[var] = null;
    }
    appendVar(data.runVars[var], tokens);
}
void appendBuildVar(ref Config data, string var, string[] tokens) {
    if (data.verboseConfigure >= 2) { writefln("appendBuildVar: %s %s", var, tokens); }
    if (var !in data.buildVars) {
        data.buildVars[var] = null;
    }
    appendVar(data.buildVars[var], tokens);
}


//
// Return the join of paths with extra
//
string[] joins(string[] paths, string extra) {
    string[] result;
    foreach (path; paths) {
        result ~= buildPath(path, extra);
    }
    return result;
}


//
// Return a string array of tokens parsed from a number of environment variables, using ':' as delimiter.
// Duplicated are discarded.
//
string[] fromEnv(string[] variables) {
    string[] result;
    bool[string] present;
    foreach (variable; variables) {
        foreach (token; split(std.process.getenv(variable), ":")) {
            if (token !in present) {
                present[token] = true;
                result ~= token;
            }
        }
    }
    return result;
}



//
// Return a string representing the given tokens as an environment variable declaration
//
string toEnv(string[][Priority] tokens, string name) {
    string result;
    foreach (string[] strings; tokens) {
        foreach (string token; strings) {
            result ~= ":" ~ token;
        }
    }
    if (result && result[0] == ':') {
        result = result[1..$];
    }
    if (result) {
        result = name ~ "=\"" ~ result ~ "\"";
    }
    return result;
}


//
// Output (to console) the current search paths being used to locate dependencies.
//
void printSearchDirs(ref string[][Priority][Use] dirs) {
    foreach (Use use, string[][Priority] v; dirs) {
        writefln("%s", use);
        foreach (Priority p, string[] a; v) {
            writefln("  %s: %s", std.string.rightJustify(std.conv.to!string(p), 7), a);
        }
    }
}


//
// Set project-specific directories to look in for required files
//
void setProjectDirs(ref Config data, string[][Use] projectDirs) {
    if (data.verboseConfigure >= 3) { writefln("Updating project search paths:"); }
    foreach (Use use, string[] dirs; projectDirs) {
        data.prevDirs[use] = dirs;
        data.dirs[use][Priority.Project] = dirs;
        if (data.verboseConfigure >= 3) { writefln("Project %s: %s", use, dirs); }
    }
}

//
// Restore project-specific dirs - only one level of restoration available
//
void restoreProjectDirs(ref Config data) {
    if (data.verboseConfigure >= 3) { writefln("Restoring project search paths:"); }
    foreach (Use use, string[] dirs; data.prevDirs) {
        data.prevDirs[use] = dirs;
        data.dirs[use][Priority.Project] = dirs;
        if (data.verboseConfigure >= 3) { writefln("Project %s: %s", use, dirs); }
    }
}


//
// Locate an executable (which can have any of the specified names)
// in any of the dirs listed in data.dirs[Use.Bin], and:
// * set the executable name to bobVar id so bob can run it efficiently.
// * add the dir to runVar PATH, providing acces to other exes in the same dir.
//
string useExecutable(ref Config data, string id, string[] names) {
    foreach (string[] dirs; data.dirs[Use.Bin]) {
        foreach (string dir; dirs) {
            foreach (name; names) {
                if (exists(buildPath(dir, name))) {
                    appendBobVar(data, id, [buildPath(dir, name)]);
                    appendRunVar(data, "PATH", [dir]);
                    if (data.verboseConfigure >= 1) { writefln("Found exe %s as %s in %s", id, name, dir); }
                    return dir;
                }
            }
        }
    }
    data.reason ~= format("Could not find executable %s by names %s\n", id, names);
    return "";
}


//
// Locate an include file in any of data.dirs[Use.Inc], and:
// * add the header dir to bobVar HEADERS
//
string useHeader(ref Config data, string name) {
    foreach (string[] dirs; data.dirs[Use.Inc]) {
        foreach (string dir; dirs) {
            if (exists(buildPath(dir, name))) {
                appendBobVar(data, "HEADERS", [dir]);
                if (data.verboseConfigure >= 1) { writefln("Found header <%s> in %s", name, dir); }
                return dir;
            }
        }
    }
    data.reason ~= "Could not find header " ~ name ~ "\n";
    return "";
}


//
// Locate a library file in any of data.dirs[Use.Lib], and:
// * add library dir to bobVar LINKFLAGS (with '-L' prefix)
// * Not add library dir to buildVar LIBRARY_PATH (Not necessary if adding -L dir to LINKFLAGS)
// * add library dir to runVar LD_LIBRARY_PATH
//
string useLibrary(ref Config data, string name) {
    foreach (string[] dirs; data.dirs[Use.Lib]) {
        foreach (string dir; dirs) {
            if (exists(buildPath(dir, name))) {
                appendBobVar(data, "LINKFLAGS", ["-L" ~ dir]);
                appendRunVar(data, "LD_LIBRARY_PATH", [dir]);
                if (data.verboseConfigure >= 1) { writefln("Found library %s in %s", name, dir); }
                return dir;
            }
        }
    }
    data.reason ~= "Could not find library " ~ name ~ "\n";
    return "";
}


//
// Locate a package .pc file in any of data.dirs[Use.Pkg], and
// use pkg-config to:
// * add library dir to bobVar LINKFLAGS
// * Not add library dir to buildVar LIBRARY_PATH (Not necessary if adding -L dir to LINKFLAGS)
// * add library dir to runVar LD_LIBRARY_PATH
//
enum Constraint { Exists, AtLeast, Exact, Max }
void usePackage(ref Config data,
                string     name,
                Constraint constraint = Constraint.Exists,
                string     ver        = "") {
    string[] constraints = ["--exists", "--atleast-version=", "--exact-version=", "--max-version="];
    try {
        string prefix = toEnv(data.dirs[Use.Pkg], "PKG_CONFIG_PATH");

        // disable use of "uninstalled" packages (which would otherwise silently be used in preference!)
        if (prefix && prefix.length != 0) { prefix ~= " "; }
        prefix ~= "PKG_CONFIG_DISABLE_UNINSTALLED=1";

        //writefln("prefix=%s", prefix);

        string command;

        command = prefix ~ " pkg-config " ~ constraints[constraint] ~ ver ~ " " ~ name;
        //writefln("command=%s", command);
        shell(command);

        command = prefix ~ " pkg-config --cflags " ~ name;
        //writefln("command=%s", command);
        string ccflags = shell(command);
        foreach (flag; split(ccflags)) {
            if (flag.length > 2 && flag[0..2] == "-I") {
                appendBobVar(data, "HEADERS", [flag[2..$]]);
            }
            else {
                appendBobVar(data, "CCFLAGS", [flag]);
            }
        }

        command = prefix ~ " pkg-config --libs-only-L " ~ name;
        //writefln("command=%s", command);
        string linkflags = shell(command);
        appendBobVar(data, "LINKFLAGS", split(linkflags));
        foreach (flag; split(linkflags)) {
            if (flag.length > 2 && flag[0..2] == "-L") {
                //appendBuildVar(data, "LIBRARY_PATH", [flag[2..$]]);  not necessary
                appendRunVar(data, "LD_LIBRARY_PATH", [flag[2..$]]);
            }
        }

        if (data.verboseConfigure >= 1) {
            command = prefix ~ " pkg-config --modversion " ~ name;
            //writefln("command=%s", command);
            string modVersion = shell(command);
            writefln("Found package %s (v%s)", name, chomp(modVersion));
        }
    }
    catch (Exception ex) {
        data.reason ~= "Could not find package " ~ constraints[constraint] ~ " " ~ ver ~ " " ~ name ~ "\n";
    }
}


//
// Locate a corba-tao library in any of data.dirs[Use.Lib], provide access to
// required TAO utilities, and set required variables.
//
void useTao(ref Config data) {
    string incdir = useHeader(data, "tao/corba.h");

    useLibrary(data, "libTAO.so");

    useExecutable(data, "TAO_IDL",                    ["tao_idl"]);
    useExecutable(data, "TAO_NAMING_SERVICE",         ["Naming_Service"]);
    useExecutable(data, "TAO_EVENT_SERVICE",          ["CosEvent_Service"]);
    useExecutable(data, "TAO_INTERFACE_REPO_SERVICE", ["IFR_Service"]);
    useExecutable(data, "TAO_INTERFACE_COMPILER",     ["tao_ifr"]);

    appendBobVar(data, "IDL_HEADERS",            [incdir]);
    appendBobVar(data, "GENERATE_EMPTY_SERVANT", ["false"]);
    appendBobVar(data, "CCFLAGS",                ["-DTAO_HAS_TYPED_EVENT_CHANNEL"]);
    appendRunVar(data, "ACE_ROOT",               [dirName(incdir)]);
    appendRunVar(data, "TAO_ROOT",               [buildPath(dirName(incdir), "TAO")]);
}


//
// Parse command-line arguments and return resultant Config data
//
Config initialise(string[] args, string projectPackage) {

    // check that we are in the project directory
    if (!exists("configure.d")) {
        writefln("Configure must be run from the project directory, which contains configure.d");
        exit(1);
    }

    // parse arguments

    bool     help;
    int      verboseConfigure;
    string   buildLevel = "release";
    string   productVersion = "development from " ~ getcwd;
    string[] architectures;
    string[] packagePrefixes;

    immutable bool[string] validArchitectures = ["Ubuntu":true, "CentOS-4":true, "CentOS-5":true];

    Config data;
    auto argsCopy = args.dup; // remember the arguments

    try {
        getopt(args, std.getopt.config.caseSensitive,
               "help|h",         &help,
               "verbose+",       &verboseConfigure,
               "build",          &buildLevel,
               "product|p",      &productVersion,
               "architecture|a", &architectures,
               "package-prefix", &packagePrefixes);
    }
    catch (Exception ex) {
        writefln("Invalid argument(s): %s", ex.msg);
        help = true;
    }

    if (help || args.length < 2) {
        writefln("Usage: configure [options] build-dir-path\n"
                 "  --help                display this message\n"
                 "  --verbose             display more configure messages (multiple)\n"
                 "  --build=level         build level: debug, integrate, release (default) or profile\n"
                 "  --product=version     sets product version\n"
                 "  --architecture=arch   sets architecture for conditional Bobfile rules (multiple)\n"
                 "  --package-prefix=path looks for locally installed packages at path (multiple)\n");
        exit(1);
    }
    foreach (arch; architectures) {
        if (arch !in validArchitectures) {
            writefln("%s is not one of these valid architectures: %s",
                     arch, validArchitectures.keys());
            exit(1);
        }
        data.architectures[arch] = true;
    }

    string buildDir = args[1];


    //
    // populate and return config data
    //

    data.srcDir = std.file.getcwd();

    foreach (arg; argsCopy[1..$]) {
        if (arg != buildDir) {
            data.configureOptions ~= arg;
        }
    }

    //
    // populate and return config data
    //

    // Populate data.dirs using packagePrefixes, environment variables, hard-coding, etc.
    // The Pritority.Project elements are (re)populated via a call to setProjectDirs.

    // add some "standard" user-specific prefixes to packagePrefixes to make life easier for users
    packagePrefixes ~= ["/opt/acacia/tao", "/opt/acacia/ecw"];

    // System - lowest priority
    data.dirs[Use.Inc][Priority.System] = ["/include", "/usr/include"];
    data.dirs[Use.Bin][Priority.System] = ["/bin", "/sbin", "/usr/bin", "/usr/sbin"];
    data.dirs[Use.Lib][Priority.System] = linkerSearchPaths(); // ["/lib", "/usr/lib"];
    data.dirs[Use.Pkg][Priority.System] = null; // /usr/lib/pkgconfig is automatically used

    // Prevent System paths from being added to the output
    foreach (Use use, string[][Priority] v; data.dirs) {
        foreach (Priority p, string[] a; v) {
            foreach (string b; a) {
                barred[b] = true;
            }
        }
    }
    // Extra protection required for library paths
    foreach (string v; data.dirs[Use.Lib][Priority.System]) {
        barred["-L" ~ v] = true;
    }

    // Project - medium priority, set by call to setProjectDirs
    data.dirs[Use.Inc][Priority.Project] = null;
    data.dirs[Use.Bin][Priority.Project] = null;
    data.dirs[Use.Lib][Priority.Project] = null;
    data.dirs[Use.Pkg][Priority.Project] = null;

    // Env - high priority
    data.dirs[Use.Inc][Priority.Env] = fromEnv(["CPATH"]);
    data.dirs[Use.Bin][Priority.Env] = fromEnv(["PATH"]);
    data.dirs[Use.Lib][Priority.Env] = fromEnv(["LD_LIBRARY_PATH"/*, "LIBRARY_PATH"*/]);
    data.dirs[Use.Pkg][Priority.Env] = fromEnv(["PKG_CONFIG_PATH"]);

    // User - highest priority
    data.dirs[Use.Inc][Priority.User] = joins(packagePrefixes, "include");
    data.dirs[Use.Bin][Priority.User] = joins(packagePrefixes, "bin");
    data.dirs[Use.Lib][Priority.User] = joins(packagePrefixes, "lib");
    data.dirs[Use.Pkg][Priority.User] = joins(packagePrefixes, "lib/pkgconfig");

    // Print the search paths (better to do this elsewhere/when)
    if (verboseConfigure >= 3) { writefln("Initial search paths:"); printSearchDirs(data.dirs); }

    // assorted variables

    data.verboseConfigure = verboseConfigure;
    data.buildLevel       = buildLevel;
    data.productVersion   = productVersion;
    // TODO? Automatically insert the current year?
    data.backgroundCopyright =
        "Part or all of this software is © Copyright 1995-2011 Acacia Research Pty Ltd. "
        "All rights reserved.\\n"
        "This software may utilise third party libraries from various sources.\\n"
        "These libraries are copyrighted by their respective owners.";
    data.foregroundCopyright =
        "Parts of this software are foreground intellectual property. ";

    data.buildDir = buildDir;

    appendBobVar(data, "PROJECT-PACKAGE", [projectPackage]);

    appendBobVar(data, "LINKFLAGS", ["-lstdc++", "-rdynamic"]);

    appendBobVar(data, "DEXTERNALS", ["std", "core"]);

    appendBobVar(data, "DFLAGS", ["-w", "-wi"]);

    appendBobVar(data, "CCFLAGS",
                 ["-fPIC",
                 "-pedantic",
                 "-Werror",
                 "-Wall",
                 "-Wno-long-long",
                 "-Wundef",
                 "-Wredundant-decls"]);

    if (data.buildLevel == "debug") {
        appendBobVar(data, "DFLAGS", ["-gc"]);
        appendBobVar(data, "CCFLAGS",
                     ["-O1",
                     "-DACRES_DEBUG=1",
                     "-DACRES_INTEGRATE=1",
                     "-fno-omit-frame-pointer",
                     "-ggdb3"]);
    }
    else if (data.buildLevel == "integrate") {
        appendBobVar(data, "DFLAGS", ["-O"]);
        appendBobVar(data, "CCFLAGS",
                     ["-O1",
                     "-DACRES_DEBUG=0",
                     "-DACRES_INTEGRATE=1",
                     "-DNDEBUG",
                     "-fno-omit-frame-pointer",
                     "-Wno-unused-variable"]);
    }
    else if (data.buildLevel == "profile") {
        appendBobVar(data, "DFLAGS", ["-O"]);
        appendBobVar(data, "CCFLAGS",
                     ["-O2",
                     "-DACRES_DEBUG=0",
                     "-DACRES_INTEGRATE=0",
                     "-DNDEBUG",
                     "-fno-omit-frame-pointer",
                     "-Wno-unused-variable",
                     "-ggdb3"]);
    }
    else if (data.buildLevel == "release") {
        appendBobVar(data, "DFLAGS", ["-O", "-release"]);
        appendBobVar(data, "CCFLAGS",
                     ["-O2",
                     "-DACRES_DEBUG=0",
                     "-DACRES_INTEGRATE=0",
                     "-DNDEBUG",
                     "-fno-omit-frame-pointer",
                     "-Wno-unused-variable"]);
    }
    else {
        writefln("unsupported build level '%s'", data.buildLevel);
        exit(1);
    }

    appendBobVar(data, "C++FLAGS",
                 ["-Woverloaded-virtual",
                 "-Wsign-promo",
                 "-Wctor-dtor-privacy",
                 "-Wnon-virtual-dtor"]);

    appendBobVar(data, "VALID_ARCHITECTURES", validArchitectures.keys);
    appendBobVar(data, "ARCHITECTURES",       architectures);

    /*
    useExecutable(data, "RST2HTML", ["rst2html.py", "rst2html", "docutils-rst2html.py"]);
    */

    appendRunVar(data, "LD_LIBRARY_PATH",  [`${DIST_PATH}/lib`]);
    appendRunVar(data, "LD_LIBRARY_PATH",  [`${DIST_PATH}/lib/plugins`]);
    appendRunVar(data, "SYSTEM_DATA_PATH", [`${DIST_PATH}/data`]);
    appendRunVar(data, "PATH", [`${DIST_PATH}/bin`]);

    return data;
}


// Write the content to the file if file doesn't already match (and optionally set executable).
// File is created if it doesn't exist.
void update(ref Config data, string name, string content, bool executable) {
    string path = buildPath(data.buildDir, name);
    bool clean = false;
    if (exists(path)) {
        string current = cast(string) std.file.read(path);
        clean = (current == content);
    }
    if (!clean) {
        if (data.verboseConfigure >= 2) { writefln("Setting content of %s", name); }
        std.file.write(path, content);
    }
    if (executable) {
        uint exeMode = octal!700;
        uint attr = getAttributes(path);
        if ((attr & exeMode) != exeMode) {
            if (data.verboseConfigure >= 4) { writefln("Setting exe mode on %s", name); }
            setMode(path, exeMode | attr);
        }
    }
}


//
// Set up build environment as specified by data, or issue error messages and bail
//
// repos are repository names in sibling directories to the directory containing
// the configure script.
//
void finalise(ref Config data, string[] otherRepos) {

    // check that all is well, and bail with an explanation if not
    if (data.reason.length) {
        writefln("Configure FAILED because:\n%s\n", data.reason);
        exit(1);
    }

    writefln("Configure checks completed ok - establishing build directory...");

    // create build directory
    if (!exists(data.buildDir)) {
        mkdirRecurse(data.buildDir);
    }
    else if (!isDir(data.buildDir)) {
        writefln("Configure FAILED because: %s is not a directory", data.buildDir);
        exit(1);
    }

    // create Boboptions file from bobVars
    string bobText;
    foreach (string key, string[] tokens; data.bobVars) {
        bobText ~= key ~ " = ";
        if (key == "C++FLAGS") {
            // C++FLAGS has all of CCFLAGS too
            foreach (token; data.bobVars["CCFLAGS"]) {
                bobText ~= token ~ " ";
            }
        }
        foreach (token; tokens) {
            bobText ~= token ~ " ";
        }
        bobText ~= ";\n";
    }
    update(data, "Boboptions", bobText, false);

    // create version_info.h file
    string versionText;
    versionText ~= "#ifndef VERSION_INFO__H\n";
    versionText ~= "#define VERSION_INFO__H\n";
    versionText ~= "\n";
    versionText ~= "#define PRODUCT_VERSION \"" ~ data.productVersion ~ "\"\n";
    versionText ~= "#define FOREGROUND_IP_COPYRIGHT_NOTICE \"" ~ data.foregroundCopyright ~ "\"\n";
    versionText ~= "#define BACKGROUND_IP_COPYRIGHT_NOTICE \"" ~ data.backgroundCopyright ~ "\"\n";
    versionText ~= "\n";
    versionText ~= "#endif /* VERSION_INFO__H */\n";
    update(data, "version_info.h", versionText, false);

    // set up string for a fix_env bash function
    string fixText =
`# Remove duplicates and empty tokens from a string containing
# colon-separated tokens, preserving order.
function fix_env () {
    local original="${1}"
    local IFS=':'
    local result=""
    for item in ${original}; do
        if [ -z "${item}" ]; then
            continue
        fi
        #echo "item: \"${item}\"" >&2
        local -i found_existing=0
        for existing in ${result}; do
            if [ "${item}" == "${existing}" ]; then
                found_existing=1
                break 1
            fi
        done
        if [ ${found_existing} -eq 0 ]; then
            result="${result:+${result}:}${item}"
        fi
    done
    echo "${result}"
}
`;

    // create environment-run file
    string runEnvText;
    runEnvText ~= "# set up the run environment variables\n\n";
    runEnvText ~= fixText;
    runEnvText ~= `if [ -z "${DIST_PATH}" ]; then` ~ "\n";
    runEnvText ~= `    echo "DIST_PATH not set"` ~ "\n";
    runEnvText ~= "    return 1\n";
    runEnvText ~= "fi\n";
    runEnvText ~= "\n";
    foreach (string key, string[] tokens; data.runVars) {
        runEnvText ~= "export " ~ key ~ `="$(fix_env "`;
        foreach (token; tokens) {
            runEnvText ~= token ~ ":";
        }
        runEnvText ~= `${` ~ key ~ `}")"` ~ "\n";
    }
    runEnvText ~= "unset fix_env\n";
    update(data, "environment-run", runEnvText, false);


    // create environment-build file
    string buildEnvText;
    buildEnvText ~= "# set up the build environment variables\n\n";
    buildEnvText ~= fixText;
    buildEnvText ~=
`if [ ! -z "${DIST_PATH}" ]; then
    echo "ERROR: DIST_PATH set when building"
    return 1
fi
export DIST_PATH="${PWD}/dist"
`;
    foreach (string key, string[] tokens; data.buildVars) {
        buildEnvText ~= "export " ~ key ~ `="$(fix_env "`;
        foreach (token; tokens) {
            buildEnvText ~= token ~ ":";
        }
        buildEnvText ~= `${` ~ key ~ `}")"` ~ "\n";
    }
    buildEnvText ~= "unset fix_env\n";
    buildEnvText ~= "# also pull in the run environment\n";
    buildEnvText ~= "source ./environment-run\n";
    update(data, "environment-build", buildEnvText, false);


    // create build script
    string buildText =
`#!/bin/bash

source ./environment-build

# Rebuild the bob executable if necessary
BOB_SRC="./src/build-tool/bob.d"
BOB_EXE="./.bob/bob"
if [ ! -e ${BOB_EXE} -o ${BOB_SRC} -nt ${BOB_EXE} ]; then
    echo "Compiling build tool."
    dmd -O -gc -w -wi ${BOB_SRC} -of${BOB_EXE}
    if [ $? -ne 0 ]; then
        echo "Failed to compile the build tool..."
        exit 1
    else
        echo "Build tool compiled successfully."
    fi
fi

# Test if we are running under eclipse
# Cause bob to echo commands passed to compiler to support eclipse auto discovery.
# Also change the include directives to those recognised by eclipse CDT.
if [ "$1" = "--eclipse" ] ; then
    shift
    echo "NOTE: What is displayed here on the console is not exactly what is executed by g++"

    ${BOB_EXE} --actions "$@" 2>&1 | sed -re "s/-iquote|-isystem/-I/g"
else
    ${BOB_EXE} "$@"
fi
`;
    update(data, "build", buildText, true);


    // create clean script
    string cleanText =
`#!/bin/bash

if [ $# -eq 0 ]; then
    rm -rf ./dist ./priv ./obj
else
    echo "Failed: $(basename ${0}) does not accept arguments - it cleans everything."
    exit 2
fi
`;
    update(data, "clean", cleanText, true);


    // strings containing common parts of run-like scripts
    string runPrologText =
`#!/bin/bash

export DIST_PATH="${PWD}/dist"
source ./environment-run
exe=$(which "$1" 2> /dev/null)

if [ -z "${exe}" ]; then
    echo "Couldn't find \"$1\"" >&2
    exit 1
fi
export TMP_PATH="$(dirname ${exe})/tmp-$(basename ${exe})"
`;


    // create (exuberant) ctags config file
    string dotCtagsText =
`--langdef=IDL
--langmap=IDL:+.idl
--regex-IDL=/^[ \t]*module[ \t]+([a-zA-Z0-9_]+)/\1/n,module,Namespace/e
--regex-IDL=/^[ \t]*enum[ \t]+([a-zA-Z0-9_]+)/\1/g,enum/e
--regex-IDL=/^[ \t]*struct[ \t]+([a-zA-Z0-9_]+)/\1/c,struct/e
--regex-IDL=/^[ \t]*exception[ \t]+([a-zA-Z0-9_]+)/\1/c,exception/e
--regex-IDL=/^[ \t]*interface[ \t]+([a-zA-Z0-9_]+)/\1/c,interface/e
--regex-IDL=/^[ \t]*typedef[ \t]+[a-zA-Z0-9_:\*<> \t]+[ \t]+([a-zA-Z0-9_]+)[ \t]*;/\1/t,typedef/e
--regex-IDL=/^[ \t]*[a-zA-Z0-9_:]+[ \t]+([a-zA-Z0-9_]+)[ \t]*[;]/\1/v,variable/e
`;
    update(data, ".ctags", dotCtagsText, false);


    // create make-tags script
    string makeCtagsText =
`#!/bin/bash

SOURCE_DIR="src"
TAGS_FILE="tags"

find -H "${SOURCE_DIR}"/* -xdev \( \( -type d -name \.svn \) -prune \
            -o -name \*.cc -o -name \*.h -o -name \*.ccg -o -name \*.hg -o -name \*.hpp -o -name \*.cpp \
            -o -name \*.inl -o -name \*.i \
            -o -name \*.idl \) |
grep -v ".svn" |
# maybe add other grep commands here
ctags -f "${TAGS_FILE}" -h default --langmap="c++:+.hg.ccg.inl.i" --extra=+f+q --c++-kinds=+p --tag-relative=yes --totals=yes --fields=+i -L -
`;
    update(data, "make-tags", makeCtagsText, true);


    // create make-cooked-tags script
    string makeCookedCtagsText =
`#!/bin/bash

SOURCE_DIR="obj"
TAGS_FILE="cooked-tags"

find -H "${SOURCE_DIR}"/* -xdev \( \( -type d -name \.svn \) -prune \
            -o -name \*.cc -o -name \*.h -o -name \*.ccg -o -name \*.hg -o -name \*.hpp -o -name \*.cpp \
            -o -name \*.idl \) |
grep -v ".svn" |
# maybe add other grep commands here
ctags -f "${TAGS_FILE}" -h default --langmap="c++:+.hg.ccg" --extra=+f+q --c++-kinds=+p --tag-relative=yes --totals=yes --fields=+i -L -
`;
    update(data, "make-cooked-ctags", makeCookedCtagsText, true);


    // create test script
    string testText;
    testText ~= runPrologText;
    testText ~=
`
if [ $# -ne 1 ]; then
    echo "The test script doesn't support arguments to test executable." >&2
    echo "Given: ${@}" >&2
    exit 2
fi
declare -i return_value=1

run_test() {
    # remove results and run the test to make some more
    set -o pipefail
    rm -f ${exe}-*

    # Ensure the result file is not zero-length (Bob depends on this)
    echo ${exe} > ${exe}-result
    ${exe}     >> ${exe}-result 2>&1

    # generate passed or failed file
    if [ "$?" != "0" ]; then
        mv ${exe}-result ${exe}-failed
        echo "${exe}-failed:1: error: test failed"
        cat ${exe}-failed
        exit 1
    else
        mv ${exe}-result ${exe}-passed    
        rm -rf ${TMP_PATH}
    fi
}

rm -rf ${TMP_PATH} && mkdir ${TMP_PATH} && run_test
`;
    update(data, "test", testText, true);


    // create run script
    string runText;
    runText ~= runPrologText;
    runText ~= "rm -rf ${TMP_PATH} && mkdir ${TMP_PATH} && exec \"$@\"";
    update(data, "run", runText, true);

    if (data.buildLevel == "profile") {
        // create perf script
        string perfText;
        perfText ~= runPrologText;
        perfText ~= "echo after exiting, run 'perf report' to see the result\n";
        perfText ~= "rm -rf ${TMP_PATH} && mkdir ${TMP_PATH} && exec perf record -g -f $@\n";
        update(data, "perf", perfText, true);
    }

    if (data.buildLevel != "release") {
        // create gdb script
        string gdbText;
        gdbText ~= runPrologText;
        gdbText ~= "rm -rf ${TMP_PATH} && mkdir ${TMP_PATH} && exec gdb --args $@\n";
        update(data, "gdb", gdbText, true);

        // create nemiver script
        string nemiverText;
        nemiverText ~= runPrologText;
        nemiverText ~= "rm -rf ${TMP_PATH} && mkdir ${TMP_PATH} && exec nemiver $@\n";
        update(data, "nemiver", nemiverText, true);
    }

    // create valgrind script
    string valgrindText;
    valgrindText ~= runPrologText;
    valgrindText ~= "rm -rf ${TMP_PATH} && mkdir ${TMP_PATH} && exec valgrind $@\n";
    update(data, "valgrind", valgrindText, true);


    //
    // create src directory with symbolic links to all top-level packages in all
    // specified repositories
    //

    // make src dir
    string srcPath = buildPath(data.buildDir, "src");
    if (!exists(srcPath)) {
        mkdir(srcPath);
    }

    // make a symbolic link to each top-level package in this and other specified repos
    string[string] pkgPaths;  // package paths keyed on package name
    string project = dirName(getcwd);
    foreach (string repoName; otherRepos ~ baseName(getcwd)) {
        string repoPath = buildPath(project, repoName);
        if (isDir(repoPath)) {
            //writefln("adding source links for packages in repo %s", repoName);
            foreach (string path; dirEntries(repoPath, SpanMode.shallow)) {
                string pkgName = baseName(path);
                if (isDir(path) && pkgName[0] != '.') {
                    //writefln("  found top-level package %s", pkgName);
                    assert(pkgName !in pkgPaths,
                           format("Package %s found at %s and %s",
                                  pkgName, pkgPaths[pkgName], path));
                    pkgPaths[pkgName] = path;
                }
            }
        }
    }
    foreach (name, path; pkgPaths) {
        string linkPath = buildPath(srcPath, name);
        system(format("rm -f %s; ln -sn %s %s", linkPath, path, linkPath));
    }

    // print success
    writefln("Build environment in %s is ready to roll", data.buildDir);
}