diff build-tool/configure_functions.d @ 134:89e8b0d92f36

Ported to bob2 !!!
author David Bryant <bagnose@gmail.com>
date Thu, 02 Aug 2012 17:20:52 +0930
parents
children be50d20643a1
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/build-tool/configure_functions.d	Thu Aug 02 17:20:52 2012 +0930
@@ -0,0 +1,1012 @@
+// 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);
+}
+