Mercurial > projects > doodle
view build-tool/bob.d @ 134:89e8b0d92f36
Ported to bob2 !!!
author | David Bryant <bagnose@gmail.com> |
---|---|
date | Thu, 02 Aug 2012 17:20:52 +0930 |
parents | |
children |
line wrap: on
line source
// Copyright Graham St Jack module bob; import std.stdio; import std.ascii; import std.string; import std.format; import std.process; import std.algorithm; import std.range; import std.file; import std.path; import std.conv; import std.datetime; import std.getopt; import std.concurrency; import std.functional; import std.exception; import core.sys.posix.sys.wait; import core.sys.posix.signal; /*======================================================================== A build tool suitable for C/C++ and D code, written in D. Objectives of this build tool are: * Easy to write and maintain build scripts (Bobfiles): - Simple syntax. - Automatic determination of which in-project libraries to link. * Auto execution and evaluation of unit tests. * Enforcement of dependency rules. * Support for building source code from multiple repositories. * Support for C/C++ and D. * Support for code generation: - A source file isn't scanned for imports/includes until after it is up to date. - Dependencies inferred from these imports are automatically applied. Directory Structure ------------------- The source code is arranged into repositories, and within repositories into packages which correspond to the directory structure. Packages should contain not only library source code, but also tests, utilities, documentation and data. The intention is to make packages easily reusable by putting all their pieces in one location. A typical directory structure is: repo repository | +--package-name package's Bobfile and library source | +--doc the package's documentation +--data data files needed by the package +--test source code for automated regression tests +--util source code for non-test executables | +--child-package(s) Dependency rules ---------------- Files and their owning packages are arranged in a tree with cross-linking dependencies. Each node in the tree can be public or protected. The root of the tree contains its children publicly. The dependency rules are: * A protected node can only be referred to by sibling nodes or nodes contained by those siblings. * A node can only refer to another node if its parent transitively refers to or transitively contains that other node. * Circular dependencies are not allowed. An object file can only be used once - either in a library or an executable. Dynamic libraries don't count as a use - they are just a repackaging. A dynamic library cannot contain the same static library as another dynamic library. Configure --------- Bundled with bob is a configure procedure that is mainly implemented in configure_functions.d. Each project contains a configure.d program that is invoked via a configure.sh script. Configure establishes a build directory complete with scripts necessary to build and run a project. Refer to configure_functions.d for details. Bobfiles -------- Each package has a Bobfile. On start, bob (the builder) reads the Bobfile specified in the Boboptions file and follows its directives. Bobfiles must refer to things in dependency order, so that anything referred to is defined earlier. General rule format in Bobfiles is: # This line is a comment. # General form of a rule. Arguments are space-separated tokens. Args are optional. rulename target : sources : arg1 : arg2 : arg3; # Resolves to 0 or 1 rule, choosing the first that matches an architecture specified # during configure. [] is a default that matches regardless of architecture. ARCHITECTURE{ [arch-1] rulename target : arg1 : arg2 : arg3; [arch-2] rulename target : arg1 : arg2 : arg3; [] rulename target : arg1 : arg2 : arg3; } Specific rules are: # This package contains packages. eg: package math ipc; package names [: protected]; # defaults to public # This package refers to others. eg: refer math ipc; # Referenced packages are specified by path relative to root. eg: icp/client refer packages; # Library: for D/C/C++, list both header and body files, all of which must be local. # eg: static-lib math : matrix.h : matrix.cc : m; static-lib name : public-sources : protected-sources : required-system-libs; # dynamic-libs can only contain static-libs within their package or its # children. The contained static-libs are specified as a relative name trail, # with the last element optionally omitted if it is the same as the package name. # eg: dynamic-lib ipc : common client server; dynamic-lib name : static-libs; dynamic-lib name : static-libs : plugin; # Executables: makes an exe from a single source file and any libs deduced from # imports and includes. Also links with specified system libs, plus any specified # by deduced libraries. # Tests are auto-executed and build fails on test error. test-util name : source : required-system-libs; # source file in test dir or generated priv-util name : source : required-system-libs; # source file in util dir or generated dist-util name : source : required-system-libs; # source file in util dir or generated # Shell priv-shell name; # source file in util dir or generated dist-shell name; # source file in util dir or generated # Data - any dirs named specify whole contained tree, not including symlinks test-data name; # source in data dir or generated. dist-data name; # source in data dir or generated. # Docco - rst files converted to html doc names; # source in doc dir or generated. doc-data names; # source in doc dir or generated. Build directory structure ------------------------- Bob assumes that a build directory has been established by an earlier configure operation. The configure sets up a Boboptions file containing definitions of variables used by rules. The format of Boboptions is: key = value; Usually bob is invoked from a build script established during configure. Under the build directory we have: obj static-libs packages(s) obj-files generated-sources packages(s) priv package(s) test-exes util-exes test test-exe-specific-test-dir(s) test-results test-working-files doc docs package(s) dist data All dist-data lib All dynamic-libs except plugins plugin Dynalic-libs designated as plugins bin All dist-util Search paths ------------ Compilers are told to look in 'src' and 'obj' directories for input files. The src directory contains links to each top-level package in all the repositories that comprise the project. Therefore, include directives have to include the path starting from the top-level package names, which must be unique. This namespacing avoids problems of duplicate filenames at the cost of the compiler being able to find everything, even files that should not be accessible. Bob therefore enforces all visibility rules before invoking the compiler. The build process ----------------- Bob reads the project Bobfile, transiting into other-package Bobfiles as packages are mentioned. Bob assumes that new packages, libraries, etc are mentioned in dependency order. That is, when each thing is mentioned, everything it depends on, including dependencies inferred by include/import statements in source code, has already been mentioned. The planner scans the Bobfiles, binding files to specific locations in the filesystem as it goes, and builds the dependency graph. The file state sequence is: initial dependencies_clean skipped if no dependencies building skipped if no build action up-to-date scanning_for_includes includes_known clean As files become buildable, actions are passed to workers. Results cause the dependency graph to be updated, allowing more actions to be issued. Specifically, generated source files are scanned for import/include after they are up to date, and the dependency graph and action commands are adjusted accordingly. //==========================================================================*/ /+ // signal handler for otherwise-fatal thread-specific signals extern (C) void threadSpecificSignalHandler(int signum) { string name() { switch (signum) { case SIGSEGV: return "SIGSEGV"; case SIGFPE: return "SIGFPE"; case SIGILL: return "SIGILL"; case SIGABRT: return "SIGABRT"; default: return ""; } } writefln("called!!!"); stdout.flush; writefln("got signal %s %s", signum, name); stdout.flush; throw new Error(format("Got signal %s %s", signum, name)); } // install a signal handler sigaction_t setHandler(int signum, sigfn_t handler) { sigset_t empty_mask; sigemptyset(&empty_mask); sigaction_t new_action; new_action.sa_handler = handler; new_action.sa_mask = empty_mask; new_action.sa_flags = SA_RESETHAND; sigaction_t old_action; sigaction(signum, &new_action, &old_action); return old_action; } shared static this() { writefln("setting up thread-specific signal handlers"); // set up shared signal handlers for fatal thread-specific signals setHandler(SIGFPE, &threadSpecificSignalHandler); setHandler(SIGILL, &threadSpecificSignalHandler); setHandler(SIGSEGV, &threadSpecificSignalHandler); // unblock the signals sigset_t unblock_set, prev_set; sigemptyset(&unblock_set); sigaddset(&unblock_set, SIGILL); sigaddset(&unblock_set, SIGSEGV); sigaddset(&unblock_set, SIGFPE); pthread_sigmask(SIG_UNBLOCK, &unblock_set, &prev_set); } +/ //----------------------------------------------------------------------------------------- // PriorityQueue - insert items in any order, and remove largest-first // (or smallest-first if "a > b" is passed for less). // // A simplified adaptation of std.container.BinaryHeap. The original doesn't // have the behaviour needed here. // // It is a simple input range (empty(), front() and popFront()). // // Notes from Wikipedia article on Binary Heap: // * Tree is concocted using index arithetic on underlying array, as follows: // First layer is 0. Second is 1,2. Third is 3,4,5,6, etc. // Therefore parent of index i is (i-1)/2 and children of index i are 2*i+1 and 2*i+2 // * Tree is balanced, with incomplete population to right of bottom layer. // * A parent is !less all its children. // * Insert: // - Append to array. // - Swap new element with parent until parent !less child. // * Remove: // - Replace root with the last element and reduce the length of the array. // - If moved element is less than a child, swap with largest child. //----------------------------------------------------------------------------------------- struct PriorityQueue(T, alias less = "a < b") { private: T[] _store; // underlying store, whose length is the queue's capacity size_t _used; // the used length of _store alias binaryFun!(less) comp; public: @property size_t length() const nothrow { return _used; } @property size_t capacity() const nothrow { return _store.length; } @property bool empty() const nothrow { return !length; } @property const(T) front() const { enforce(!empty); return _store[0]; } // Insert a value into the queue size_t insert(T value) { // put the new element at the back of the store if ( length == capacity) { _store.length = (capacity + 1) * 2; } _store[_used] = value; // percolate-up the new element for (size_t n = _used; n; ) { auto parent = (n - 1) / 2; if (!comp(_store[parent], _store[n])) break; swap(_store[parent], _store[n]); n = parent; } ++_used; return 1; } void popFront() { enforce(!empty); // replace the front element with the back one if (_used > 1) { _store[0] = _store[_used-1]; } --_used; // percolate-down the front element (which used to be at the back) size_t parent = 0; for (;;) { auto left = parent * 2 + 1, right = left + 1; if (right > _used) { // no children - done break; } if (right == _used) { // no right child - possibly swap parent with left, then done if (comp(_store[parent], _store[left])) swap(_store[parent], _store[left]); break; } // both left and right children - swap parent with largest of itself and left or right auto largest = comp(_store[parent], _store[left]) ? (comp(_store[left], _store[right]) ? right : left) : (comp(_store[parent], _store[right]) ? right : parent); if (largest == parent) break; swap(_store[parent], _store[largest]); parent = largest; } } } //------------------------------------------------------------------------------ // Synchronized object that launches external processes in the background // and keeps track of their PIDs. A one-shot bail() method kills all those // launched processes and prevents any more from being launched. // // bail() is called by the error() functions, which then throw an exception. // // We also install a signal handler to bail on receipt of various signals. //------------------------------------------------------------------------------ class BailException : Exception { this() { super("Bail"); } } synchronized class Launcher { private { bool bailed; bool[int] children; } // launch a process if we haven't bailed pid_t launch(string command) { if (bailed) { say("Aborting launch because we have bailed"); throw new BailException(); } int child = spawnvp(P_NOWAIT, "/bin/bash", ["bash", "-c", command]); //say("spawned child, pid=%s", child); children[child] = true; return child; } // a child has been finished with void completed(pid_t child) { children.remove(child); //say("completed child, pid=%s", child); } // bail, doing nothing if we had already bailed bool bail() { if (!bailed) { bailed = true; foreach (child; children.byKey) { //say("killing child, pid=%s", child); kill(child, SIGTERM); } return false; } else { return true; } } } shared Launcher launcher; void doBail() { launcher.bail; } extern (C) void mySignalHandler(int sig) { // launch a thread to initiate a bail say("got signal %s", sig); spawn(&doBail); } shared static this() { // set up shared Launcher and signal handling launcher = new shared(Launcher)(); /* signal(SIGTERM, &mySignalHandler); signal(SIGINT, &mySignalHandler); signal(SIGHUP, &mySignalHandler); */ } //------------------------------------------------------------------------------ // printing utility functions //------------------------------------------------------------------------------ // where something originated from struct Origin { string path; uint line; } private void sayNoNewline(A...)(string fmt, A a) { auto w = appender!(char[])(); formattedWrite(w, fmt, a); stderr.write(w.data); } void say(A...)(string fmt, A a) { auto w = appender!(char[])(); formattedWrite(w, fmt, a); stderr.writeln(w.data); stderr.flush; } void fatal(A...)(string fmt, A a) { say(fmt, a); launcher.bail; throw new BailException(); } void error(A...)(ref Origin origin, string fmt, A a) { sayNoNewline("%s|%s| ERROR: ", origin.path, origin.line); fatal(fmt, a); } void errorUnless(A ...)(bool condition, Origin origin, lazy string fmt, lazy A a) { if (!condition) { error(origin, fmt, a); } } //------------------------------------------------------------------------- // path/filesystem utility functions //------------------------------------------------------------------------- // // Return the given path with the top level directory removed // string clip(string path) { for (uint i = 0; i < path.length; ++i) { if (isDirSeparator(path[i])) return path[i+1..$]; } return ""; } // // Ensure that the parent dir of path exists // void ensureParent(string path) { static bool[string] doesExist; string dir = dirName(path); if (dir !in doesExist) { if (!exists(dir)) { ensureParent(dir); say("%-15s %s", "Mkdir", dir); mkdir(dir); } else if (!isDir(dir)) { error(Origin(), "%s is not a directory!", dir); } doesExist[path] = true; } } // // return the modification time of the file at path // Note: A zero-length target file is treated as if it doesn't exist. // long modifiedTime(string path, bool isTarget) { if (!exists(path) || (isTarget && getSize(path) == 0)) { return 0; } SysTime fileAccessTime, fileModificationTime; getTimes(path, fileAccessTime, fileModificationTime); return fileModificationTime.stdTime; } // return the privacy implied by args Privacy privacyOf(ref Origin origin, string[] args) { if (!args.length ) return Privacy.PUBLIC; else if (args[0] == "protected") return Privacy.PROTECTED; else if (args[0] == "semi-protected") return Privacy.SEMI_PROTECTED; else if (args[0] == "private") return Privacy.PRIVATE; else if (args[0] == "public") return Privacy.PUBLIC; else error(origin, "privacy must be one of public, semi-protected, protected or private"); assert(0); } //------------------------------------------------------------------------ // File parsing //------------------------------------------------------------------------ // options read from Boboptions file string[string] options; bool[string] architectures; bool[string] validArchitectures; // // Read an options file, populating options // Format is: key=value;\n // value can contain '=' // void readOptions() { string path = "Boboptions"; Origin origin = Origin(path, 1); errorUnless(exists(path) && isFile(path), origin, "can't read Boboptions %s", path); string content = readText(path); string key; int anchor = 0; foreach (int pos, char ch ; content) { if (ch == '\n') ++origin.line; if (key is null && ch == '=') { key = strip(content[anchor..pos]); anchor = pos + 1; } else if (ch == ';') { if (key is null) error(origin, "option terminated without a key"); string str = strip(content[anchor..pos]); if (key == "VALID_ARCHITECTURES") { foreach (arch; split(str)) { validArchitectures[arch] = true; } } else if (key == "ARCHITECTURES") { foreach (arch; split(str)) { architectures[arch] = true; } } options[key] = str; key = null; } else if (ch == '\n') { errorUnless(key is null, origin, "option %s not terminated with ';'", key); anchor = pos + 1; } } errorUnless(key is null, origin, "%s ends in unterminated option", path); } string getOption(string key) { auto value = key in options; if (value) { return *value; } else { return ""; } } // // Scan file for includes, returning an array of included trails // # include "trail" // // All of the files found should have trails relative to "src" (if source) // or "obj" (if generated). All system includes must use angle-brackets, // and are not returned from a scan. // struct Include { string trail; uint line; } Include[] scanForIncludes(string path) { Include[] result; Origin origin = Origin(path, 1); enum Phase { START, HASH, WORD, INCLUDE, QUOTE, NEXT } if (exists(path) && isFile(path)) { string content = readText(path); int anchor = 0; Phase phase = Phase.START; foreach (int i, char ch; content) { if (ch == '\n') { phase = Phase.START; ++origin.line; } else { switch (phase) { case Phase.START: if (ch == '#') { phase = Phase.HASH; } else if (!isWhite(ch)) { phase = Phase.NEXT; } break; case Phase.HASH: if (!isWhite(ch)) { phase = Phase.WORD; anchor = i; } break; case Phase.WORD: if (isWhite(ch)) { if (content[anchor..i] == "include") { phase = Phase.INCLUDE; } else { phase = Phase.NEXT; } } break; case Phase.INCLUDE: if (ch == '"') { phase = Phase.QUOTE; anchor = i+1; } else if (!isWhite(ch)) { phase = Phase.NEXT; } break; case Phase.QUOTE: if (ch == '"') { result ~= Include(content[anchor..i].idup, origin.line); phase = Phase.NEXT; } break; case Phase.NEXT: break; default: error(origin, "invalid phase"); } } } } return result; } // // Scan a D source file for imports. // // The parser is simple and fast, but can't deal with version // statements or mixins. This is ok for now because it only needs // to work for source we have control over. // // The approach is: // * Scan for a line starting with "static", "public", "private" or "" // followed by "import". // * Then look for: // ':' - module is previous word, and then skip to next ';'. // ',' - module is previous word. // ';' - module is previous word. // The import is terminated by a ';'. // Include[] scanForImports(string path) { Include[] result; string content = readText(path); string word; int anchor, line=1; bool inWord, inImport, ignoring; string[] externals = split(getOption("DEXTERNALS")); foreach (int pos, char ch; content) { if (ch == '\n') { line++; } if (ignoring) { if (ch == ';' || ch == '\n') { // resume looking for imports ignoring = false; inWord = false; inImport = false; } else { // ignore } } else { // we are not ignoring if (inWord && (isWhite(ch) || ch == ':' || ch == ',' || ch == ';')) { inWord = false; word = content[anchor..pos]; if (!inImport) { if (isWhite(ch)) { if (word == "import") { inImport = true; } else if (word != "public" && word != "private" && word != "static") { ignoring = true; } } else { ignoring = true; } } } if (inImport && word && (ch == ':' || ch == ',' || ch == ';')) { // previous word is a module name string trail = std.array.replace(word, ".", dirSeparator) ~ ".d"; bool ignored = false; foreach (external; externals) { string ignoreStr = external ~ dirSeparator; if (trail.length >= ignoreStr.length && trail[0..ignoreStr.length] == ignoreStr) { ignored = true; break; } } if (!ignored) { result ~= Include(trail, line); } word = null; if (ch == ':') ignoring = true; else if (ch == ';') inImport = false; } if (!inWord && !(isWhite(ch) || ch == ':' || ch == ',' || ch == ';')) { inWord = true; anchor = pos; } } } return result; } // // read a Bobfile, returning all its statements // // // a simple statement // rulename targets... : arg1... : arg2... : arg3...; // can expand Boboptions variable with ${var-name} // // // a conditional statement resolving to 0 or one of its constituents, based on ARCHITECTURE Boboption. // ARCHITECTURE{ // [arch-1] rulename targets... : arg1... : arg2... ; // [arch-2] rulename targets... : arg1... : arg2... ; // [] rulename targets... : arg1... : arg2... ; // optional default // } // // struct Statement { Origin origin; int phase; // 0==>empty, 1==>rule populated, 2==rule,targets populated, etc string rule; string[] targets; string[] arg1; string[] arg2; string[] arg3; string toString() const { string result; if (phase >= 1) result ~= rule; if (phase >= 2) result ~= format(" : %s", targets); if (phase >= 3) result ~= format(" : %s", arg1); if (phase >= 4) result ~= format(" : %s", arg2); if (phase >= 5) result ~= format(" : %s", arg3); return result; } } Statement[] readBobfile(string path) { Statement[] statements; Origin origin = Origin(path, 1); errorUnless(exists(path) && isFile(path), origin, "can't read Bobfile %s", path); string content = readText(path); Statement statement; int anchor = 0; bool inWord = false; bool inComment = false; bool inCondition = false; bool conditionMatch = false; bool conditionSatisfied = false; foreach (int pos, char ch ; content) { if (ch == '\n') { ++origin.line; } if (ch == '#') { inComment = true; inWord = false; } if (inComment) { if (ch == '\n') { inComment = false; anchor = pos; } } else if ((isWhite(ch) || ch == ':' || ch == ';')) { if (inWord) { inWord = false; string word = content[anchor..pos]; if (word == "ARCHITECTURE{") { // start an architecture condition errorUnless(!inCondition, origin, "nested ARCHITECTURE condition in %s", path); errorUnless(statement.phase == 0, origin, "ARCHITECTURE condition inside a statement in %s", path); inCondition = true; conditionMatch = false; conditionSatisfied = false; } else if (word.length >= 2 && word[0] == '[' && word[$-1] == ']') { // architecture specifier preceeds statement string architecture = word[1..$-1]; errorUnless(inCondition, origin, "architecture '%s' specifier when not in condition in %s", architecture, path); errorUnless(statement.phase == 0, origin, "nested statements in %s near %s", path, word); errorUnless(architecture == "" || architecture in validArchitectures, origin, "invalid architecture '%s' in %s", architecture, path); if (!conditionSatisfied && (architecture == "" || architecture in architectures)) { conditionMatch = true; } } else if (word == "}") { inCondition = false; conditionMatch = false; conditionSatisfied = false; } else { // should be a word in a statement string[] words = [word]; if (word.length > 3 && word[0..2] == "${" && word[$-1] == '}') { // macro substitution words = split(getOption(word[2..$-1])); } if (word.length > 0) { if (statement.phase == 0) { statement.origin = origin; statement.rule = words[0]; ++statement.phase; } else if (statement.phase == 1) { statement.targets ~= words; } else if (statement.phase == 2) { statement.arg1 ~= words; } else if (statement.phase == 3) { statement.arg2 ~= words; } else if (statement.phase == 4) { statement.arg3 ~= words; } else { error(origin, "Too many arguments in %s", path); } } } } if (ch == ':' || ch == ';') { ++statement.phase; if (ch == ';') { if (statement.phase > 1 && (!inCondition || conditionMatch)) { statements ~= statement; if (inCondition) { conditionSatisfied = true; conditionMatch = false; //say("conditional resolves to statement '%s'", statement.toString); } } statement = statement.init; } } } else if (!inWord) { inWord = true; anchor = pos; } } errorUnless(statement.phase == 0, origin, "%s ends in unterminated rule", path); return statements; } //------------------------------------------------------------------------- // Planner // // Planner reads Bobfiles, understands what they mean, builds // a tree of packages, etc, understands what it all means, enforces rules, // binds everything to filenames, discovers modification times, scans for // includes, and schedules actions for processing by the worker. // // Also receives results of successful actions from the Worker, // does additional scanning for includes, updates modification // times and schedules more work. // // A critical feature is that scans for includes are deferred until // a file is up-to-date. //------------------------------------------------------------------------- // some thread-local "globals" to make things easier bool g_print_rules; bool g_print_deps; bool g_print_details; // // Action - specifies how to build some files, and what they depend on // final class Action { static Action[string] byName; static int nextNumber; static PriorityQueue!Action queue; string name; // the name of the action string command; // the action command-string int number; // influences build order File[] builds; // files that this action builds File[] depends; // files that the action's targets depend on bool issued; // true if the action has been issued to a worker this(ref Origin origin, string name_, string command_, File[] builds_, File[] depends_) { name = name_; command = command_; number = nextNumber++; builds = builds_; depends = depends_; errorUnless(!(name in byName), origin, "Duplicate command name=%s", name); byName[name] = this; // add the bobfile responsible for all the files built by this action File bobfile; foreach (build; builds) { File b = build.bobfile; assert(b !is null); if (bobfile is null) bobfile = b; assert(b is bobfile); } assert(bobfile !is null); depends ~= bobfile; // set up reverse dependencies between builds and depends foreach (depend; depends) { foreach (built; builds) { depend.dependedBy[built] = true; if (g_print_deps) say("%s depends on %s", built.path, depend.path); } } } // add an extra depend to this action void addDependency(File depend) { if (issued) fatal("Cannot add a dependancy to issued action %s", this); if (builds.length != 1) { fatal("cannot add a dependency to an action that builds more than one file: %s", name); } depends ~= depend; // set up references and reverse dependencies between builds and depend foreach (built; builds) { depend.dependedBy[built] = true; if (g_print_deps) say("%s depends on %s", built.path, depend.path); } } // augment this action's command string with some text void augment(string augmentation) { assert(!issued); command ~= augmentation; } // issue this action void issue() { assert(!issued); issued = true; queue.insert(this); } override string toString() { return name; } override int opCmp(Object o) const { // reverse order if (this is o) return 0; Action a = cast(Action)o; if (a is null) return -1; return a.number - number; } } // // SysLib - represents a library outside the project. // final class SysLib { static SysLib[string] byName; string name; // assorted Object overrides for printing and use as an associative-array key override string toString() const { return name; } this(string name_) { name = name_; byName[name] = this; } } // // Node - abstract base class for things in an ownership tree // with cross-linked dependencies. Used to manage allowed references. // // additional constraint on allowed references enum Privacy { PUBLIC, // no additional constraint SEMI_PROTECTED, // only accessable to descendents of grandparent PROTECTED, // only accessible to children of parent PRIVATE } // not accessible class Node { static Node[string] byTrail; string name; // simple name this node adds to parent string trail; // slash-separated name components from root to this Node parent; Privacy privacy; Node[] children; Node[] refers; // assorted Object overrides for printing and use as an associative-array key override string toString() const { return trail; } // create the root of the tree this() { trail = "root"; assert(trail !in byTrail, "already have root node"); byTrail[trail] = this; } // create a node and place it into the tree this(ref Origin origin, Node parent_, string name_, Privacy privacy_) { assert(parent_); errorUnless(dirName(name_) == ".", origin, "Cannot define node with multi-part name '%s'", name_); parent = parent_; name = name_; privacy = privacy_; if (parent.parent) { // child of non-root trail = buildPath(parent.trail, name); } else { // child of the root trail = name; } parent.children ~= this; errorUnless(trail !in byTrail, origin, "%s already known", trail); byTrail[trail] = this; } // return true if this is a descendant of other private bool isDescendantOf(Node other) { for (auto node = this; node !is null; node = node.parent) { if (node is other) return true; } return false; } // return true if this is a visible descendant of other private bool isVisibleDescendantOf(Node other, Privacy allowed) { for (auto node = this; node !is null; node = node.parent) { if (node is other) return true; if (node.privacy > allowed) break; if (allowed > Privacy.PUBLIC) allowed--; } return false; } // return true if other is a visible-child or reference of this, // or is a visible-descendant of them private bool allowsRefTo(ref Origin origin, Node other, size_t depth = 0, Privacy allowPrivacy = Privacy.PROTECTED, bool[Node] checked = null) { errorUnless(depth < 100, origin, "circular reference involving %s referring to %s", this, other); //say("for %s: checking if %s allowsReferenceTo %s", origin.path, this, other); if (other is this || other.isVisibleDescendantOf(this, allowPrivacy)) { if (g_print_details) say("%s allows reference to %s via containment", this, other); return true; } foreach (node; refers) { // referred-to nodes grant access to their public children, and referred-to // suiblings grant access to their semi-protected children if (node !in checked) { checked[node] = true; if (node.allowsRefTo(origin, other, depth+1, node.parent is this.parent ? Privacy.SEMI_PROTECTED : Privacy.PUBLIC, checked)) { if (g_print_details) say("%s allows reference to %s via explicit reference", this, other); return true; } } } return false; } // Add a reference to another node. Cannot refer to: // * Nodes that aren't defined yet. // * Self. // * Ancestors. // * Nodes whose selves or ancestors have not been referred to by our parent. // Also can't explicitly refer to children - you get that implicitly. final void addReference(ref Origin origin, Node other, string cause = null) { errorUnless(other !is null, origin, "%s cannot refer to NULL node", this); errorUnless(other != this, origin, "%s cannot refer to self", this); errorUnless(!this.isDescendantOf(other), origin, "%s cannot refer to ancestor %s", this, other); errorUnless(!other.isDescendantOf(this), origin, "%s cannnot explicitly refer to descendant %s", this, other); errorUnless(this.parent.allowsRefTo(origin, other), origin, "Parent %s does not allow %s to refer to %s", parent, this, other); errorUnless(!other.allowsRefTo(origin, this), origin, "%s cannot refer to %s because of a circularity", this, other); if (g_print_deps) say("%s refers to %s%s", this, other, cause); refers ~= other; } } // // Pkg - a package. Has a Bobfile, assorted source and built files, and sub-packages // Used to group files together for dependency control, and to house a Bobfile. // final class Pkg : Node { File bobfile; this(ref Origin origin, Node parent_, string name_, Privacy privacy_) { super(origin, parent_, name_, privacy_); bobfile = File.addSource(origin, this, "Bobfile", Privacy.PRIVATE, false); } } // // A file // class File : Node { static File[string] byPath; // Files by their path static bool[File] allActivated; // all activated files static bool[File] outstanding; // outstanding buildable files static int nextNumber; // Statistics static uint numBuilt; // number of files targeted static uint numUpdated; // number of files successfully updated by actions string path; // the file's path int number; // order of file creation bool scannable; // true if the file and its includes should be scanned for includes bool built; // true if this file will be built by an action Action action; // the action used to build this file (null if non-built) long modTime; // the modification time of the file bool[File] dependedBy; // Files that depend on this bool used; // true if this file has been used already // state-machine stuff bool activated; // considered by touch() for files with an action bool scanned; // true if this has already been scanned for includes File[] includes; // the Files this includes bool[File] includedBy; // Files that include this bool clean; // true if usable by higher-level files long includeModTime; // transitive max of mod_time and includes include_mod_time // analysis stuff File youngestDepend; File youngestInclude; // return a prospective path to a potential file. static string prospectivePath(string start, Node parent, string extra) { Node node = parent; while (node !is null) { Pkg pkg = cast(Pkg) node; if (pkg) { return buildPath(start, pkg.trail, extra); } node = node.parent; } fatal("prospective file %s's parent %s has no package in its ancestry", extra, parent); assert(0); } // return the bobfile that this file is declared in final File bobfile() { Node node = parent; while (node) { Pkg pkg = cast(Pkg) node; if (pkg) { return pkg.bobfile; } node = node.parent; } fatal("file %s has no package in its ancestry", this); assert(0); } this(ref Origin origin, Node parent_, string name_, Privacy privacy_, string path_, bool scannable_, bool built_) { super(origin, parent_, name_, privacy_); path = path_; scannable = scannable_; built = built_; number = nextNumber++; modTime = modifiedTime(path, built); errorUnless(path !in byPath, origin, "%s already defined", path); byPath[path] = this; if (built) { //say("built file %s", path); ++numBuilt; } } // Add a source file specifying its trail within its package static File addSource(ref Origin origin, Node parent, string extra, Privacy privacy, bool scannable) { // three possible paths to the file string path1 = prospectivePath("obj", parent, extra); // a built file in obj directory tree string path2 = prospectivePath("src", parent, extra); // a source file in src directory tree string path3 = baseName(extra); // a configure-generated source file in build directory string name = baseName(extra); File * file = path1 in byPath; if (file) { // this is a built source file we already know about errorUnless(!file.used, origin, "%s has already been used", path1); return *file; } else if (exists(path2)) { // a source file under src return new File(origin, parent, name, privacy, path2, scannable, false); } else if (exists(path3)) { // a source file in build dir return new File(origin, parent, name, privacy, path3, scannable, false); } else { error(origin, "Could not find source file %s in %s, %s or %s", name, path1, path2, path3); assert(0); } } // This file has been updated final void updated() { ++numUpdated; modTime = modifiedTime(path, true); if (g_print_details) say("Updated %s, mod_time %s", this, modTime); if (action !is null) { action = null; outstanding.remove(this); } touch; } // Scan this file for includes, returning them after making sure those files // themselves exist and have already been scanned for includes private void scan() { errorUnless(!scanned, Origin(path, 1), "%s has been scanned for includes twice!", this); scanned = true; if (scannable) { // scan for includes that are part of the project, and thus must already be known Include[] entries; string ext = extension(path); if (ext == ".c" || ext == ".cc" || ext == ".h") { entries = scanForIncludes(path); } else if (ext == ".d") { entries = scanForImports(path); } else { fatal("Don't know how to scan %s for includes/imports", path); } foreach (entry; entries) { // verify that we know the included file File * file; // under src using full trail? File * include = cast(File *) (buildPath("src", entry.trail) in byPath); if (include is null && baseName(entry.trail) == entry.trail) { // in src dir of the including file's package? include = cast(File *) (prospectivePath("src", parent, entry.trail) in byPath); } if (include is null) { // under obj? include = cast(File *) (buildPath("obj", entry.trail) in byPath); } if (include is null) { // build dir? include = entry.trail in byPath; } Origin origin = Origin(this.path, entry.line); errorUnless(include !is null, origin, "included/imported unknown file %s", entry.trail); // add the included file to this file's includes includes ~= *include; include.includedBy[this] = true; // tell all files that depend on this one that the include has been added includeAdded(origin, this, *include); // now (after includeAdded) we can add a reference between this file and the included one //say("adding include-reference from %s to %s", this, *include); addReference(origin, *include); } if (g_print_deps && includes) say("%s includes=%s", this, includes); // totally important to touch includes AFTER we know what all of them are foreach (include; includes) { include.touch; } } } // An include has been added from includer (which we depend on) to included. // Specialisations of File override this to infer additional depends. void includeAdded(ref Origin origin, File includer, File included) { //say("File.includeAdded called on %s with %s including %s", this, includer, included); foreach (depend; dependedBy.keys()) { //say(" passing on to %s", depend); depend.includeAdded(origin, includer, included); } } // This file's action is about to be issued, and this is the last chance to // augment its action. Specialisation should override this method if augmentation // is required. Return true if dependencies were added. bool augmentAction() { return false; } // This file has been touched - work out if its action should be issued // or if it is now clean, transiting to affected items if this becomes clean. // NOTE - nothing can become clean until AFTER all activation has been done by the planner. final void touch() { if (clean) return; if (g_print_details) say("touching %s", path); long newest; if (activated && action && !action.issued) { // this items action may need to be issued //say("activated file %s touched", this); foreach (depend; action.depends) { if (!depend.clean) { if (g_print_details) say("%s waiting for %s to become clean", path, depend.path); return; } if (newest < depend.includeModTime) { newest = depend.includeModTime; youngestDepend = depend; } } // all files this one depends on are clean // give this file a chance to augment its action if (augmentAction()) { // dependency added - touch this file again to re-check dependencies touch; return; } else { // no dependencies were added, so we can issue the action now if (modTime < newest) { // buildable and out of date - issue action to worker if (g_print_details) { say("%s is out of date with mod_time ", this, modTime); File other = youngestDepend; File prevOther = this; while (other && other.includeModTime > prevOther.modTime) { say(" %s mod_time %s (younger by %s)", other, other.includeModTime, other.includeModTime - modTime); other = other.youngestDepend; } } action.issue; return; } else { // already up to date - no need for building if (g_print_details) say("%s is up to date", path); action = null; outstanding.remove(this); } } } if (action) return; errorUnless(modTime > 0, Origin(path, 1), "%s (%s) is up to date with zero mod_time!", path, trail); // This file is up to date // Scan for includes, possibly becoming clean in the process if (!scanned) scan; if (clean) return; // Find out if includes are clean and what our effective mod_time is newest = modTime; foreach (include; includes) { if (!include.clean) { return; } if (newest < include.includeModTime) { newest = include.includeModTime; youngestInclude = include; } } includeModTime = newest; if (g_print_details) { say("%s is clean with effective mod_time %s", this, includeModTime); File other = youngestInclude; File prevOther = this; while (other && other.includeModTime > prevOther.modTime) { say(" %s mod_time %s (younger by %s)", other, other.includeModTime, other.includeModTime - prevOther.modTime); other = other.youngestInclude; } } // All includes are clean, so we are too clean = true; // touch everything that includes or depends on this foreach (other; includedBy.byKey()) { other.touch; } foreach (other; dependedBy.byKey()) { if (other.activated) other.touch; } } } // // Obj - an object file built from a source file // final class Obj : File { static Obj[File] bySource; // object files by the source file they are built from this(ref Origin origin, File source) { string name_ = setExtension(source.name, "o"); string path_ = prospectivePath("obj", source.parent, name_); super(origin, source.parent, name_, Privacy.PUBLIC, path_, false, true); errorUnless(source !in bySource, origin, "source file %s already used to build an object file", source); bySource[source] = this; string actionName; string actionCommand; switch (extension(source.name)) { case ".cc": case ".cpp": actionName = format("%-15s %s", "C++", path); actionCommand = format("g++ -c %s -DTRACE_DOMAIN=\\\"%s\\\"", getOption("C++FLAGS"), source.path); foreach (dir; split(getOption("HEADERS"))) { actionCommand ~= format(" -isystem %s", dir); } actionCommand ~= " -iquote src -iquote obj -iquote ."; actionCommand ~= format(" -o %s %s", path, source.path); break; case ".c": actionName = format("%-15s %s", "C", path); actionCommand = format("gcc -c %s -DTRACE_DOMAIN=\\\"%s\\\"", getOption("CCFLAGS"), source.path); foreach (dir; split(getOption("HEADERS"))) { actionCommand ~= format(" -isystem %s", dir); } actionCommand ~= " -iquote src -iquote obj -iquote ."; actionCommand ~= format(" -o %s %s", path, source.path); break; case ".d": actionName = format("%-15s %s", "D", path); actionCommand = format("dmd -c %s", getOption("DFLAGS")); foreach (dir; split(getOption("IMPORTS"))) { actionCommand ~= format(" -I%s", dir); } actionCommand ~= " -Isrc -Iobj"; actionCommand ~= format(" -of%s %s", path, source.path); break; default: error(origin, "Unsupported source file extension %s", extension(source.name)); } action = new Action(origin, actionName, actionCommand, [this], [source]); addReference(origin, source); } } // // Binary - a binary file incorporating object files and 'owning' source files. // abstract class Binary : File { static Binary[File] byContent; // binaries by the header and body files they 'contain' struct Source { string name; Privacy privacy; } bool isD; File[] objs; File[] headers; bool[SysLib] reqSysLibs; bool[Binary] reqBinaries; // create a binary using files from this package. // All the sources themselves may be already-known built files, // but can't already be used by another Binary. this(ref Origin origin, Pkg pkg, string name_, string path_, string[] requires) { super(origin, pkg, name_, Privacy.PUBLIC, path_, false, true); // required system libraries foreach (req; requires) { if (req !in SysLib.byName) { new SysLib(req); } SysLib lib = SysLib.byName[req]; if (lib !in reqSysLibs) { reqSysLibs[lib] = true; } } } // Add sources to this Binary. Adding them after construction allows generated source // files to be children of the Binary. void addMySources(ref Origin origin, Source[] sources) { bool isScannable(string extension) { return extension == ".c" || extension == ".cc" || extension == ".h" || extension == ".d"; } errorUnless(sources.length > 0, origin, "binary must have at least one source file"); foreach (source; sources) { string ext = extension(source.name); File sourceFile = File.addSource(origin, this, source.name, source.privacy, isScannable(ext)); byContent[sourceFile] = this; if (ext == ".d" || ext == ".cc" || ext == ".c") { // a source file that we generate an Obj from objs ~= new Obj(origin, sourceFile); if (ext == ".d") { isD = true; } } else if (ext == ".ipc") { // an inter-process-communication file from which we generate a header file // make sure the ipc-compiler has already been defined string compilerPath = buildPath("dist", "bin", "ipc-compiler"); File* compiler = compilerPath in File.byPath; errorUnless(compiler !is null, origin, "Cannot cook an ipc files before creating the compiler (%s)", compilerPath); // build a header file from this ipc file string destPath = buildPath("obj", parent.trail, stripExtension(source.name) ~ ".h"); File dest = new File(origin, this, baseName(destPath), source.privacy, destPath, true, true); dest.action = new Action(origin, format("%-15s %s", "cook-ipc", sourceFile.path), format("ipc-compiler %s %s", sourceFile.path, dest.path), [dest], [sourceFile, *compiler]); byContent[dest] = this; headers ~= dest; } else { headers ~= sourceFile; } } } override void includeAdded(ref Origin origin, File includer, File included) { // A file we depend on (includer) has included another file (included). // If this means that this 'needs' another Binary, remember the fact and // also add a dependency on that other Binary. Note that the dependency // is often not 'real' (a StaticLib doesn't actually depend on other StaticLibs), // but it is a very useful simplification when working out which libraries an // Exe depends on. //say("%s: %s includes %s", this.path, includer.path, included.path); if (includer in byContent && byContent[includer] is this) { Binary * container = included in byContent; errorUnless(container !is null, origin, "included file is not contained in a library"); if (*container !is this && *container !in reqBinaries) { // we require the container of the included file reqBinaries[*container] = true; // add a dependancy and a reference addReference(origin, *container, format(" because %s includes %s", includer.path, included.path)); action.addDependency(*container); //say("%s requires %s", this.path, container.path); } } } } // // StaticLib - a static library. // final class StaticLib : Binary { string uniqueName; this(ref Origin origin, Pkg pkg, string name_, string[] requires) { uniqueName = std.array.replace(buildPath(pkg.trail, name_), dirSeparator, "-") ~ "-s"; if (name_ == pkg.name) uniqueName = std.array.replace(pkg.trail, dirSeparator, "-") ~ "-s"; string _path = buildPath("obj", format("lib%s.a", uniqueName)); super(origin, pkg, name_, _path, requires); } void addSources(ref Origin origin, string[] publicSources, string[] protectedSources) { Source[] sources; foreach (name; protectedSources) { sources ~= Source(name, Privacy.SEMI_PROTECTED); } foreach (name; publicSources) { sources ~= Source(name, Privacy.PUBLIC); } addMySources(origin, sources); // action string command; if (objs.length) { command = format("rm -f %s; ar csr %s", path, path); foreach (obj; objs) { command ~= format(" %s", obj.path); } } else { command = format("rm -f %s; echo dummy > %s", path, path); } action = new Action(origin, format("%-15s %s", "StaticLib", path), command, [this], objs ~ headers); } } // // DynamicLib - a dynamic library. Contains all of the object files // from a number of specified StaticLibs. If defined prior to an Exe, the Exe will // link with the DynamicLib instead of those StaticLibs. // // Any StaticLibs required by the incorporated StaticLibs must also be incorporated // into DynamicLibs. // // The static lib names are relative to pkg, and therefore only descendants of the DynamicLib's // parent can be incorporated. // final class DynamicLib : File { static DynamicLib[StaticLib] byContent; // dynamic libs by the static libs they 'contain' Origin origin; bool augmented; string uniqueName; StaticLib[] staticLibs; this(ref Origin origin_, Pkg pkg, string name_, string[] staticTrails, bool isPlugin) { origin = origin_; uniqueName = std.array.replace(buildPath(pkg.trail, name_), "/", "-"); if (name_ == pkg.name) uniqueName = std.array.replace(pkg.trail, dirSeparator, "-"); string _path = buildPath("dist", "lib", format("lib%s.so", uniqueName)); if (isPlugin) _path = buildPath("dist", "lib", "plugins", format("lib%s.so", uniqueName)); super(origin, pkg, name_ ~ "-dynamic", Privacy.PUBLIC, _path, false, true); foreach (trail; staticTrails) { string trail1 = buildPath(pkg.trail, trail, baseName(trail)); string trail2 = buildPath(pkg.trail, trail); Node* node = trail1 in Node.byTrail; if (node is null || cast(StaticLib*) node is null) { node = trail2 in Node.byTrail; if (node is null || cast(StaticLib*) node is null) { error(origin, "Unknown static-lib %s, looked for with trails %s and %s", trail, trail1, trail2); } } StaticLib* staticLib = cast(StaticLib*) node; errorUnless(*staticLib !in byContent, origin, "static lib %s already used by dynamic lib %s", *staticLib, byContent[*staticLib]); addReference(origin, *staticLib); staticLibs ~= *staticLib; byContent[*staticLib] = this; } errorUnless(staticLibs.length > 0, origin, "dynamic-lib must have at least one static-lib"); // action bool[SysLib] gotSysLibs; string actionName = format("%-15s %s", "DynamicLib", path); string command = format("g++ -shared %s -o %s", getOption("LINKFLAGS"), path); File[] depLibs; foreach (staticLib; staticLibs) { depLibs ~= staticLib; foreach (obj; staticLib.objs) { command ~= format(" %s", obj.path); } } action = new Action(origin, actionName, command, [cast(File)this], depLibs); } // Called just before our action is issued. // Verify that all the StaticLibs we now know that we depend on are contained by this or // another earlier-defined-than-this DynamicLib. // Add any required SysLibs to our action. override bool augmentAction() { if (augmented) return false; augmented = true; bool[StaticLib] doneStaticLibs; bool[SysLib] gotSysLibs; SysLib[] sysLibs; bool added; //say("augmenting action for DynamicLib %s", name); void accumulate(StaticLib lib) { //say(" accumulating %s", lib.name); foreach (other; lib.reqBinaries.keys) { StaticLib next = cast(StaticLib) other; assert(next !is null); if (next !in doneStaticLibs) { accumulate(next); } } if (lib !in doneStaticLibs) { doneStaticLibs[lib] = true; errorUnless(lib.objs.length == 0 || (lib in byContent && byContent[lib].number <= number), origin, "dynamic-lib %s requires static-lib %s (%s) which " "is not contained in a pre-defined dynamic-lib", name, lib.trail, lib.path); foreach (sys; lib.reqSysLibs.keys) { if (sys !in gotSysLibs) { //say(" new required SysLib %s", sys.name); gotSysLibs[sys] = true; sysLibs ~= sys; added = true; } } } } foreach (lib; staticLibs) { accumulate(lib); } string augmentation; foreach (sys; sysLibs) { augmentation ~= format(" -l%s", sys.name); } //say("augmentation is %s", augmentation); action.augment(augmentation); return added; } } // // Exe - An executable file // final class Exe : Binary { bool augmented; string desc; string src; string dest; // create an executable using files from this package, linking to libraries // that contain any included header files, and any required system libraries. // Note that any system libraries required by inferred local libraries are // automatically linked to. this(ref Origin origin, Pkg pkg, string kind, string name_, string[] requires) { // interpret kind switch (kind) { case "dist-util": desc = "DistUtil"; src = "util"; dest = buildPath("dist", "bin", name_); break; case "priv-util": desc = "PrivUtil"; src = "util"; dest = buildPath("priv", pkg.trail, name_); break; case "test-util": desc = "TestExe"; src = "test"; dest = buildPath("priv", pkg.trail, name_); break; default: assert(0, "invalid Exe kind " ~ kind); } super(origin, pkg, name_ ~ "-exe", dest, requires); if (kind == "test-util") { File test = new File(origin, pkg, name ~ "-result", Privacy.PRIVATE, dest ~ "-passed", false, true); test.action = new Action(origin, format("%-15s %s", "TestResult", test.path), format("./test %s", dest), [test], [this]); } } void addSources(ref Origin origin, string[] sources) { errorUnless(sources.length > 0, origin, "An exe must have at least one source file"); Source[] _sources; foreach (source; sources) { _sources ~= Source(buildPath(src, source), Privacy.PROTECTED); } addMySources(origin, _sources); // exe action string command; string ext = extension(sources[0]); switch (ext) { case ".cc": case ".c": command = format("%s %s -o %s", ext == "c" ? "gcc" : "g++", getOption("LINKFLAGS"), dest); foreach (dir; split(getOption("HEADERS"))) { command ~= format(" -isystem %s", dir); } foreach (obj; objs) { command ~= format(" %s", obj.path); } command ~= format(" -L%s", buildPath("dist", "lib")); command ~= format(" -L%s", buildPath("dist", "lib", "plugins")); command ~= format(" -Lobj"); break; case ".d": command = format("dmd %s -of%s ", getOption("DLINKFLAGS"), path); foreach (obj; objs) { command ~= format(" %s", obj.path); } command ~= format(" -L-Lobj"); break; default: error(origin, "Unsupported source file extension %s in %s", extension(sources[0]), path); } action = new Action(origin, format("%-15s %s", desc, dest), command, [this], objs ~ headers); } // Called just before our action is issued - augment the action's command string // with the library dependencies that we should now know about via includeAdded(). // Return true if dependencies were added. override bool augmentAction() { if (augmented) return false; augmented = true; bool added = false; //say("augmenting %s's action command", this); // binaries we require, with most fundamental first bool[DynamicLib] gotDynamicLibs; bool[StaticLib] gotStaticLibs; bool[SysLib] gotSysLibs; DynamicLib[] dynamicLibs; StaticLib[] staticLibs; SysLib[] sysLibs; // accumulate the libraries needed void accumulate(Binary binary) { //say("accumulating binary %s", binary.path); foreach (other; binary.reqBinaries.keys) { StaticLib lib = cast(StaticLib) other; assert(lib !is null); if (lib !in gotStaticLibs) { accumulate(other); } } if (binary is this) { foreach (sys; reqSysLibs.keys) { if (sys !in gotSysLibs) { //say(" using sys-lib %s", sys.name); gotSysLibs[sys] = true; sysLibs ~= sys; } } } else { StaticLib lib = cast(StaticLib) binary; if (lib !in gotStaticLibs) { //say(" require static-lib %s", lib.uniqueName); gotStaticLibs[lib] = true; foreach (sys; lib.reqSysLibs.keys) { if (sys !in gotSysLibs) { //say(" using sys-lib %s", sys.name); gotSysLibs[sys] = true; sysLibs ~= sys; } } DynamicLib* dynamic = lib in DynamicLib.byContent; if (dynamic !is null && dynamic.number < this.number) { // use the dynamic lib that contains the static lib if (*dynamic !in gotDynamicLibs) { //say(" using dynamic-lib %s to cover %s", dynamic.name, lib.uniqueName); gotDynamicLibs[*dynamic] = true; dynamicLibs ~= *dynamic; action.addDependency(*dynamic); added = true; // we have to also accumulate everything this dynamic lib needs foreach (contained; dynamic.staticLibs) { accumulate(contained); } } } else { // use the static lib //say(" using static-lib %s", lib.uniqueName); staticLibs ~= lib; } } } } //say("accumulating required libraries for exe %s", path); accumulate(this); string extra; if (isD) extra = "-L"; string augmentation; foreach (lib; retro(staticLibs)) { if (lib.objs.length) { augmentation ~= format(" %s-l%s", extra, lib.uniqueName); } } foreach (lib; retro(dynamicLibs)) { augmentation ~= format(" %s-l%s", extra, lib.uniqueName); } foreach (lib; retro(sysLibs)) { augmentation ~= format(" %s-l%s", extra, lib.name); } //say("%s augmentation is '%s'", this, augmentation); action.augment(augmentation); return added; } } // // Mark all built files in child or referred packages as needed // int markNeeded(Node given) { static bool[Node] done; int qty = 0; if (given in done) return qty; done[given] = true; File file = cast(File) given; if (file && file.built) { // activate this file if (file.action is null) { fatal("file %s activated before its action was set", file); } //say("activating %s", file.path); file.activated = true; File.allActivated[file] = true; File.outstanding[file] = true; qty++; } // recurse into child and referred nodes foreach (child; chain(given.children, given.refers)) { qty += markNeeded(child); } if (file) { // touch this file to trigger building file.touch; } return qty; } // // Process a Bobfile // void processBobfile(string indent, Pkg pkg) { static bool[Pkg] processed; if (pkg in processed) return; processed[pkg] = true; if (g_print_rules) say("%sprocessing %s", indent, pkg.bobfile); indent ~= " "; foreach (statement; readBobfile(pkg.bobfile.path)) { if (g_print_rules) say("%s%s", indent, statement.toString); switch (statement.rule) { // packages case "contain": foreach (name; statement.targets) { errorUnless(dirName(name) == ".", statement.origin, "Contained packages have to be top-level"); Privacy privacy = privacyOf(statement.origin, statement.arg1); Pkg newPkg = new Pkg(statement.origin, pkg, name, privacy); processBobfile(indent, newPkg); } break; case "refer": foreach (trail; statement.targets) { Pkg* other = cast(Pkg*) (trail in Node.byTrail); if (other is null) { // create the referenced package which must be top-level, then refer to it errorUnless(dirName(trail) == ".", statement.origin, "Previously-unknown referenced package %s has to be top-level", trail); Pkg newPkg = new Pkg(statement.origin, Node.byTrail["root"], trail, Privacy.PUBLIC); processBobfile(indent, newPkg); pkg.addReference(statement.origin, newPkg); } else { // refer to the existing package errorUnless(other !is null, statement.origin, "Cannot refer to unknown pkg %s", trail); pkg.addReference(statement.origin, *other); } } break; // libraries case "static-lib": { errorUnless(statement.targets.length == 1, statement.origin, "Can only have one static-lib name per rule"); StaticLib lib = new StaticLib(statement.origin, pkg, statement.targets[0], statement.arg3); lib.addSources(statement.origin, statement.arg1, statement.arg2); } break; case "dynamic-lib": { errorUnless(statement.targets.length == 1, statement.origin, "Can only have one dynamic-lib name per rule"); new DynamicLib(statement.origin, pkg, statement.targets[0], statement.arg1, statement.arg2.length == 1 && statement.arg2[0] == "plugin"); } break; case "dist-util": case "priv-util": case "test-util": { errorUnless(statement.targets.length == 1, statement.origin, "Can only have one util/exe name per rule"); Exe exe = new Exe(statement.origin, pkg, statement.rule, statement.targets[0], statement.arg2); exe.addSources(statement.origin, statement.arg1); } break; case "tao-lib": { // // tao-lib lib-name : idl-sources; # idl files and gen src in package dir // errorUnless(statement.targets.length == 1, statement.origin, "Can only have one tao static-lib name per rule"); // // create the static-lib // auto requires = [ "TAO_CosNaming", "TAO_PortableServer", "TAO_DynamicAny", "TAO", "ACE", "rt", "dl"]; auto lib = new StaticLib(statement.origin, pkg, statement.targets[0], requires); // // create the idl files and the source files generated from them // string[] publicNames; string[] protectedNames; foreach (idlName; statement.arg1) { // idl file File idl = File.addSource(statement.origin, pkg, idlName, Privacy.PROTECTED, false); // paths of all generated files string base = buildPath("obj", pkg.trail, stripExtension(idlName)); string[] junk = [ base ~ "S_T.cc"]; string[] publicPaths = [ base ~ "C.h", base ~ "S.h" ]; string[] protectedPaths = [ base ~ "S_T.inl", base ~ "C.inl", base ~ "S.inl", base ~ "S_T.h", base ~ "C.cc", base ~ "S.cc" ]; // generated files File[] files; foreach (path; junk) { files ~= new File(statement.origin, lib, baseName(path), Privacy.PRIVATE, path, false, true); } foreach (path; publicPaths) { files ~= new File(statement.origin, lib, baseName(path), Privacy.PUBLIC, path, !(extension(path) == ".inl"), true); publicNames ~= baseName(path); } foreach (path; protectedPaths) { files ~= new File(statement.origin, lib, baseName(path), Privacy.PROTECTED, path, !(extension(path) == ".inl"), true); protectedNames ~= baseName(path); } // action to generate the files and edit them to be correct string command = format("%s -in -Ce -cs C.cc -ss S.cc -sT S_T.cc", getOption("TAO_IDL")); if (toLower(getOption("GENERATE_EMPTY_SERVANT")) == "true") { command ~= " -GI -GIh \"_impl-rename.h\" -GIs \"_impl-rename.cc\" -GIe \"Impl\" -GIc -GIa"; } foreach (d; split(getOption("IDL_HEADERS"))) { command ~= format(" -I%s", d); } command ~= format(" -I%s", dirName(idl.path)); command ~= format(" -I%s", dirName(base)); command ~= " -o " ~ buildPath("obj", pkg.trail) ~ " " ~ idl.path ~ " &&"; command ~= ` sed --in-place --separate` ~ ` -e '/#include/s/\"orbsvcs\/\([^\"]*\)\"/\<orbsvcs\/\1\>/'` ~ ` -e '/#include/s/\"tao\/\([^\"]*\)\"/\<tao\/\1\>/'` ~ ` -e '/#include/s/\"ace\/\([^\"]*\)\"/\<ace\/\1\>/'` ~ ` -e '/#include \"/s|#include \"|#include \"` ~ pkg.trail ~ `/|'` ~ // XXX should not do this if included path contains a slash ` -e '/#include .*.cc/d'` ~ ` -e 's/[[:space:]]\+$//'`; foreach (file; files) { command ~= " " ~ file.path; } auto action = new Action(statement.origin, format("%-15s %s", statement.rule, idl.path), command, files, [idl]); foreach (file; files) { file.action = action; } } // add the generated source files as the sources of the library, completing its definition lib.addSources(statement.origin, publicNames, protectedNames); } break; case "dist-data": case "test-data": case "util-data": case "doc-data": { void dataRule(ref Origin origin, Pkg pkg, string kind, string extra) { string fromPath; string destPath; if (kind == "doc-data") { fromPath = buildPath("src", pkg.trail, "doc", extra); destPath = buildPath("priv", pkg.trail, "doc", extra); } else if (kind == "test-data") { fromPath = buildPath("src", pkg.trail, "test", extra); destPath = buildPath("priv", pkg.trail, extra); } else if (kind == "util-data") { fromPath = buildPath("src", pkg.trail, "util", extra); destPath = buildPath("priv", pkg.trail, extra); } else if (kind == "dist-data") { fromPath = buildPath("src", pkg.trail, "data", extra); destPath = buildPath("dist", "data", extra); } if (isDir(fromPath)) { // recurse into directory foreach (string path; dirEntries(fromPath, SpanMode.shallow)) { dataRule(origin, pkg, kind, buildPath(extra, path.baseName)); } } else { // set up from and dest files, and an action to create the dest. string name = std.array.replace(extra, dirSeparator, "-"); File from = new File(origin, pkg, name ~ "-src", Privacy.PUBLIC, fromPath, false, false); File dest = new File(origin, pkg, name ~ "-dest", Privacy.PUBLIC, destPath, false, true); dest.action = new Action(origin, format("%-15s %s", "Data", dest.path), format("cp %s %s", from.path, dest.path), [dest], [from]); } } foreach (name; statement.targets) { dataRule(statement.origin, pkg, statement.rule, name); } } break; case "doc": { foreach (name; statement.targets) { string fromPath = buildPath("src", pkg.trail, "doc", name ~ ".rst"); string destPath = buildPath("priv", pkg.trail, "doc", name ~ ".html"); errorUnless(exists(fromPath) && !isDir(fromPath), statement.origin, "%s not found", fromPath); File from = new File(statement.origin, pkg, name ~ "-rst", Privacy.PUBLIC, fromPath, false, false); File dest = new File(statement.origin, pkg, name ~ "-html", Privacy.PUBLIC, destPath, false, true); dest.action = new Action(statement.origin, format("%-15s %s", "Doc", dest.path), format("%s --exit-status=2 %s %s", getOption("RST2HTML"), from.path, dest.path), [dest], [from]); } } break; case "dist-shell": case "priv-shell": { foreach (name; statement.targets) { string fromPath = buildPath("src", pkg.trail, "util", name); string destPath; if (statement.rule == "dist-shell") { destPath = buildPath("dist", "bin", name); } else { destPath = buildPath("priv", pkg.trail, name); } errorUnless(exists(fromPath) && !isDir(fromPath), statement.origin, "%s not found", fromPath); File from = new File(statement.origin, pkg, name ~ "-sh", Privacy.PUBLIC, fromPath, false, false); File dest = new File(statement.origin, pkg, name, Privacy.PUBLIC, destPath, false, true); dest.action = new Action(statement.origin, format("%-15s %s", "Shell", dest.path), format("cp -f %s %s && chmod +x %s", from.path, dest.path, dest.path), [dest], [from]); } } break; default: error(statement.origin, "Unsupported rule '%s'", statement.rule); } } } // remove any files in obj, priv and dist that aren't marked as needed void cleandirs() { void cleanDir(string name) { //say("cleaning dir %s, cdw=%s", name, getcwd); if (exists(name) && isDir(name)) { bool[string] dirs; foreach (DirEntry entry; dirEntries(name, SpanMode.depth, false)) { //say(" considering %s", entry.name); bool isDir = attrIsDir(entry.linkAttributes); if (!isDir) { File* file = entry.name in File.byPath; if (file is null || (*file) !in File.allActivated) { say("Removing unwanted file %s", entry.name); std.file.remove(entry.name); } else { // leaving a file in place //say(" keeping activated file %s", file.path); dirs[entry.name.dirName] = true; } } else { if (entry.name !in dirs) { //say("removing empty dir %s", entry.name); rmdir(entry.name); } else { //say(" keeping non-empty dir %s", entry.name); dirs[entry.name.dirName] = true; } } } } } cleanDir("obj"); cleanDir("priv"); cleanDir("dist"); } // // Planner function // bool doPlanning(int numJobs, bool printRules, bool printDeps, bool printDetails) { // state variables size_t inflight; bool[string] workers; bool[string] idlers; bool exiting; bool success = true; // receive registration message from each worker and remember its name while (workers.length < numJobs) { receive( (string worker) { workers[worker] = true; idlers[worker] = true; } ); } // local function: an action has completed successfully - update all files built by it void actionCompleted(string worker, string action) { //say("%s %s succeeded", action, worker); --inflight; idlers[worker] = true; try { foreach (file; Action.byName[action].builds) { file.updated; } } catch (BailException ex) { exiting = true; success = false; } } // local function: a worker has terminated - remove it from workers and remember we are exiting void workerTerminated(string worker) { exiting = true; workers.remove(worker); //say("%s has terminated - %s workers remaining", worker, workers.length); } // set up some globals readOptions; g_print_rules = printRules; g_print_deps = printDeps; g_print_details = printDetails; string projectPackage = getOption("PROJECT-PACKAGE"); int needed; try { // read the project Bobfile and descend into all those it refers to auto root = new Node(); auto project = new Pkg(Origin(), root, projectPackage, Privacy.PRIVATE); processBobfile("", project); // mark all files needed by the project Bobfile as needed, priming the action queue needed = markNeeded(project); cleandirs(); } catch (BailException ex) { exiting = true; success = false; } while (workers.length) { // give any idle workers something to do //say("%s idle workers and %s actions in priority queue", idlers.length, Action.queue.length); string[] toilers; foreach (idler, dummy; idlers) { if (!exiting && !File.outstanding.length) exiting = true; Tid tid = std.concurrency.locate(idler); if (exiting) { // tell idle worker to terminate //say("telling %s to terminate", idler); send(tid, true); toilers ~= idler; } else if (!Action.queue.empty) { // give idle worker an action to perform //say("giving %s an action", idler); const Action next = Action.queue.front(); Action.queue.popFront(); string targets; foreach (target; next.builds) { ensureParent(target.path); targets ~= "|" ~ target.path; } //say("issuing action %s", next.name); send(tid, next.name.idup, next.command.idup, targets.idup); toilers ~= idler; ++inflight; } else if (!inflight) { fatal("nothing to do and no inflight actions"); } else { // nothing to do //say("nothing to do - waiting for results"); break; } } foreach (toiler; toilers) idlers.remove(toiler); // receive a completion or failure receive( (string worker, string action) { actionCompleted(worker, action); }, (string worker) { workerTerminated(worker); } ); } if (!File.outstanding.length && success) { say("\n" "Total number of files: %s\n" "Number of target files: %s\n" "Number of activated target files: %s\n" "Number of files updated: %s\n", File.byPath.length, File.numBuilt, needed, File.numUpdated); return true; } return false; } //----------------------------------------------------- // Worker //----------------------------------------------------- void doWork(bool printActions, uint index, Tid plannerTid) { bool success; string myName = format("worker-%d", index); std.concurrency.register(myName, thisTid); void perform(string action, string command, string targets) { if (printActions) { say("\n%s\n", command); } say("%s", action); success = false; string results = buildPath(".bob", myName); // launch child process to do the action string str = command ~ " 2>" ~ results; pid_t child = launcher.launch(str); // wait for it to complete for (;;) { int status; pid_t wpid = core.sys.posix.sys.wait.waitpid(child, &status, 0); if (wpid == -1) { // error, possibly a signal - treat as failure break; } else if (wpid == 0) { // non-blocking return, which should not happen - treat as failure break; } else if ((status & 0x7f) == 0) { // child has terminated - might be success success = ((status & 0xff00) >> 8) == 0; break; } else { // child state has changed in some way other than termination - treat as failure break; } } launcher.completed(child); if (!success) { bool bailed = launcher.bail; // delete built files foreach (target; split(targets, "|")) { if (exists(target)) { say(" Deleting %s", target); std.file.remove(target); } } // print error message if (!bailed) { say("\n%s", readText(results)); say("%s: FAILED\n%s", action, command); fatal("Aborting build due to action failure"); } else { // just quietly throw throw new BailException(); } } else { // tell planner the action succeeded send(plannerTid, myName, action); } } try { send(plannerTid, myName); bool done; while (!done) { receive( (string action, string command, string targets) { perform(action, command, targets); }, (bool dummy) { done = true; }); } } catch (BailException) {} catch (Exception ex) { say("Unexpected exception %s", ex); } // tell planner we are terminating send(plannerTid, myName); //say("%s terminating", myName); } //-------------------------------------------------------------------------------------- // main // // Assumes that the top-level source packages are all located in a src subdirectory, // and places build outputs in obj, priv and dist subdirectories. // The local source paths are necessary to minimise the length of actions, // and is usually achieved by a configure step setting up sym-links to the // actual source locations. //-------------------------------------------------------------------------------------- int main(string[] args) { try { bool printRules = false; bool printDeps = false; bool printDetails = false; bool printActions = false; bool help = false; uint numJobs = 1; int returnValue = 0; try { getopt(args, std.getopt.config.caseSensitive, "rules", &printRules, "deps", &printDeps, "details", &printDetails, "actions", &printActions, "jobs|j", &numJobs, "help", &help); } catch (std.conv.ConvException ex) { returnValue = 2; say(ex.msg); } catch (object.Exception ex) { returnValue = 2; say(ex.msg); } if (args.length != 1) { say("Option processing failed. There are %s unprocessed argument(s): ", args.length - 1); foreach (uint i, arg; args[1..args.length]) { say(" %s. \"%s\"", i + 1, arg); } returnValue = 2; } if (numJobs < 1) { returnValue = 2; say("Must allow at least one job!"); } if (returnValue != 0 || help) { say("Usage: bob [options]\n" " --rules print rules\n" " --deps print dependencies\n" " --actions print actions\n" " --details print heaps of details\n" " --jobs=VALUE maximum number of simultaneous actions\n" " --help show this message\n" "target is everything contained in the project Bobfile and anything referred to."); return returnValue; } if (printDetails) { printActions = true; printDeps = true; } // spawn the workers foreach (uint i; 0..numJobs) { //say("spawning worker %s", i); spawn(&doWork, printActions, i, thisTid); } // build everything returnValue = doPlanning(numJobs, printRules, printDeps, printDetails) ? 0 : 1; return returnValue; } catch (Exception ex) { say("got unexpected exception %s", ex); return 1; } }