Mercurial > projects > doodle
diff builder.d @ 28:1754cb773d41
Part-way through getting to compile with configure/builder.
author | Graham St Jack <graham.stjack@internode.on.net> |
---|---|
date | Sun, 02 Aug 2009 16:27:21 +0930 |
parents | |
children | 960b408d3ac5 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/builder.d Sun Aug 02 16:27:21 2009 +0930 @@ -0,0 +1,1031 @@ +module builder; + +private { + import std.stream; + import std.stdio; + import std.string; + import std.process; + import std.file; + import std.path; + import std.date; +} + + +// +// builder is a build tool for D code. It is intended for use during development, +// and so supports: +// * Building only those targets that are out of date. +// * Automated execution and evaluation of regression tests. +// * Automatic linking of libraries that are part of the project. +// * Enforcement of dependency rules (explained below). +// * Enforcement of a directory structure designed to support reusable components. +// * Complete separation of source from targets (friendly on checkouts). +// +// To keep things simple and fast, builder won't build arbitrary code - but it will build +// code written to be built with it. Besides having to obey dependency and directory +// layout and naming rules, you have to do imports as one of: +// public import module_name; +// private import module_name; +// import module_name; +// static import module_name; +// static import module_alias = module_name; +// specifically, the module-name has to be the last thing on the line. +// +// +// The directory structure employs a heirarchy of: +// * Bundle - A collection of products. Has no source code of its own, and does not appear in package paths. +// Corresponds to the root of a checkout/repository. +// * Product - A directory to provide namespace. +// * Package - A self-contained set of library code, executable code, docs, tests, etc. +// Can contain nested packages, and can refer to other packages. +// +// Bundles are the mechanism for reusing source code in other repositories. +// We do this by lines in a bundle "uses" file that specify where to find a used bundle. +// +// The directory structure within a bundle is: +// +// +--configure Script to set up the build directory and check for assumed system libraries +// | +// +--repackage Script to produce a source tarball that can be built with dsss +// | +// +--uses Specifies which other bundles to use, with paths relative to this bundle +// | +// +--product-name(s) Provides namespace - only contains packages +// | +// +--package-name(s) Can contain library code +// | +// +--doc Restructured-text documents and associated images +// | +// +--data Data files required by tests and binaries +// | +// +--test Code for regression tests +// | +// +--prog Code for binaries +// | +// +--package-name(s) Nested packages +// +// The resultant build directory structure is: +// +// +--build Script to build the system (put there by configure) +// | +// +--obj Contains object files and libraries in package structure +// | +// +--test Contains test binaries and results in package structure +// | +// +--bin Contains the binaries built from prog directories +// | +// +--doc Contains the package html docs, in package structure +// +// The dependency rules are: +// * A built file depends on its sources, and anything the sources explicitly import. +// * A parent depends on its children, transitively, and everything they depend on, transitively. +// * Everything depends on the parents and descendants of its explicit imports, +// up to but not including the common ancestor, transitively. +// + + +// TODO - add doc, data +// TODO - add timeout on test execution - say 2 seconds. +// TODO - add excludes files in packages to allow files and directories to be ignored. + + +//------------------------------------------------------------------------------- +// primitives +//------------------------------------------------------------------------------- + + +// +// Some global data +// +class Global { + static string buildPath; + static string optionsPath; + static string[] bundlePaths; + static bool[string] products; +} + + +// +// throw an exception +// +void error(string text) { + writefln("%s", text); + throw new Exception(text); +} + + +// +// return the modification time of the file at path +// +d_time modified_time(string path) { + d_time creation_time, access_time, modified_time; + if (!exists(path)) { + return 0; + } + getTimes(path, creation_time, access_time, modified_time); + return modified_time; +} + + +// +// A string formatter that allows progressive composition +// +class StringFormatter { + char[] buffer; + int pos; + + this() { + buffer.length = 1024; + } + + void clear() { + pos = 0; + } + + // append some formatted text to the internal buffer + void format(...) { + void put(dchar c) { + if (pos+4 >= buffer.length) { + buffer.length = buffer.length * 2; + } + char[4] buf; + char[] result = std.utf.toUTF8(buf, c); + buffer[pos..pos+4] = buf; + pos += result.length; + } + std.format.doFormat(&put, _arguments, _argptr); + } + + // return a copy of the internal buffer + string str() { + return buffer[0..pos].idup; + } +} + +//--------------------------------------------------------- +// parser +//--------------------------------------------------------- + +// FIXME - put in a proper parser that understands about version syntax and all the rest. + +/// +/// Parse file for import statements, returning an array of module names +/// +string[] parseImports(string path) { + + string[] result; + + // + // If this is an import declaration line, return the imported module, else null + // This isn't perfect by any means (but it is simple and fast), but here we go: + // * Strip whitespace from front. + // * If first word is private, public, static or import, and + // * If we have import already or there is another import later in the line, then + // * The last word on the line is a module. + // A less-than-perfect parser is ok because it only has to work for code + // within the project, so we can write code that it works for. + // + void parseForImport(string line) { + static string[] leaders = ["private ", "public ", "static ", "import "]; + bool found; + string stripped = stripl(line); + if (stripped.length > 8) { + foreach (i, leader ; leaders) { + if (stripped[0..leader.length] == leader) { + // this is a possibility - look for import token + scope string[] tokens = split(stripped); + if (tokens.length > 1) { + if (i == 3) { + found = true; + } + else { + foreach (token; tokens[0..$-1]) { + if (token == "import") { + found = true; + break; + } + } + } + } + + if (found) { + //writefln("found import of %s", tokens[$-1][0..$-1]); + result ~= tokens[$-1][0..$-1].idup; + } + + break; + } + } + } + } + + scope file = new BufferedFile(path); + foreach (char[] line; file) { + parseForImport(line.idup); + } + //writefln("imports of %s are %s", file, result); + return result; +} + + +//------------------------------------------------------------------------------ +// Dependency graph +//------------------------------------------------------------------------------ + +// +// An item that may depend on things and be depended on in turn +// +abstract class Item { + static Item[string] mItems; // items by path so parent-path items can be found + + string mKind; // for logging + string mPath; // identifies the item + long mTime; // modification time, 0 ==> doesn't exist, -1 ==> don't care + Item[string] mDepends; // items this one depends on + + bool mChecking; + bool mChecked; + bool mDirty; + + this(string kind, string path) { + //writefln("creating %s-item at %s", kind, path); + mKind = kind; + mPath = path; + mTime = modified_time(path); + mItems[path] = this; + } + + // add a dependency to this item + final void addDepends(Item item) { + if (!(item.mPath in mDepends)) { + //writefln("%s depends on %s", this.mPath, item.mPath); + mDepends[item.mPath] = item; + } + } + + // Find out if this item is dirty (needs to be built), returning true if dirty. + final bool dirty() { + if (mChecking) { + error(format("circularity: %s depends on itself", mPath)); + } + if (!mChecked) { + mChecking = true; + if (mTime == 0) { + //writefln("%s is dirty because it doesn't exist", basename(mPath)); + mDirty = true; + } + foreach (other; mDepends) { + if (mDirty) break; + if (other.dirty) { + //writefln("%s is dirty because %s is dirty", basename(mPath), basename(other.mPath)); + mDirty = true; + } + else if (mTime != -1 && other.mTime != -1 && other.mTime > mTime) { + //writefln("%s is dirty because %s is younger", basename(mPath), basename(other.mPath)); + //writefln(" other=%s, mine=%s", other.mTime, mTime); + mDirty = true; + } + } + mChecking = false; + mChecked = true; + } + return mDirty; + } + + // build just this item + protected abstract void build(); + + // build this item and everything it depends on + final void buildChain() { + if (dirty) { + foreach (other; mDepends) { + other.buildChain; + } + build; + if (mTime != -1) { + mTime = modified_time(mPath); + } + mDirty = false; + } + } + + string toString() { + return mKind ~ "-item-" ~ mPath; + } +} + + +// +// A directory +// +class DirectoryItem : Item { + + this(string path) { + super("directory", path); + mTime = mPath.exists ? 1 : 0; + + // this directory depends on its parent directory chain + string parent_path = path.dirname; + if (parent_path.length > 0 && parent_path[0] != '.' ) { + Item* parent = parent_path in mItems; + if (parent) { + assert(cast(DirectoryItem *)parent); + addDepends(*parent); + } + else { + // recursively create chain of directories above this, + // and depend on it. + addDepends(new DirectoryItem(parent_path)); + } + } + } + + // build this directory + override void build() { + writefln("Directory %s", mPath); + mkdir(mPath); + } +} + + +/// +/// A group Item - used to help manage build order via dependencies +/// All FileItems are members of exactly one group, and they all depend on +/// all the groups their owning group depends on. +/// +class GroupItem : Item { + + static GroupItem[string] mGroups; // all groups + + Item[] mMembers; + + this(string path) { + super("group", path); + mTime = -1; + mGroups[mPath] = this; + } + + void addMember(Item member) { + addDepends(member); + mMembers ~= member; + } + + // add dependencies on other group and its ancestors + // Only called AFTER all items have been greated and assigned their group. + void dependsOnGroup(GroupItem other) { + if (other !is this) { + + // this group and everything in it (other than source) depends on other group + addDepends(other); + foreach (member; mMembers) { + if (!cast(SourceItem) member) { + member.addDepends(other); + } + } + + // and all other group's higher-level groups until common ancestor + string other_parent_path = other.mPath.dirname; + if (other_parent_path != "." && + other_parent_path.length < mPath.length && + mPath[0..other_parent_path.length] != other_parent_path) + { + GroupItem* other_parent = other_parent_path in mGroups; + if (other_parent) { + dependsOnGroup(*other_parent); + } + } + } + } + + // build just this item + override void build() { + // nothing to do + writefln("Group %s", mPath); + } +} + + +// +// a physical file in the filesystem +// +abstract class FileItem : Item { + GroupItem mGroup; + + this(string kind, string path, GroupItem group) { + super(kind, path); + mGroup = group; + group.addMember(this); + + // this file depends on on its parent directory + string parent_path = path.dirname; + Item* parent = parent_path in mItems; + if (parent) { + assert(cast(DirectoryItem *)parent); + addDepends(*parent); + } + else { + addDepends(new DirectoryItem(parent_path)); + } + } +} + + +// +// A source file - must exist +// +class SourceItem : FileItem { + + SourceItem[string] mImports; // the source items this one imports + + this(string path, GroupItem group) { + super("source", path, group); + if (!isfile(path)) error(format("source file %s not found", path)); + } + + // Add an import to this source item. + // These are used later to determine all the non-structural item dependencies. + void addImport(SourceItem source) { + if (!(source.mPath in mImports)) { + //writefln("%s imports %s", mPath, source.mPath); + mImports[source.mPath] = source; + } + } + + override void build() { + // nothing to do, and should never be dirty + writefln("Source %s", mPath); + error("should never need to build Source"); + } +} + + +// +// An object file +// +class ObjectItem : FileItem { + SourceItem mSource; + + this(string path, SourceItem source, GroupItem group) { + super("object", path, group); + addDepends(source); + mSource = source; + } + + // Use our source's imports to add non-structural dependencies. + // The rules are: + // * We depend on any sources our source imports, transitively. + // * Our group depends on the imported source's group, and any of its + // ancestral groups that aren't our ancestors. + // * We must not trace a chain of imports back to our source. + void resolveImports() { + + void dependsOnImportsOf(SourceItem source) { + foreach (s; source.mImports) { + if (s is mSource) { + error(format("circular chain of imports - last link from %s to %s", + source.toString, s.toString)); + } + if (s.mGroup !is mGroup) { + addDepends(s.mGroup); + } + addDepends(s); + dependsOnImportsOf(s); + } + } + + dependsOnImportsOf(mSource); + + foreach (s; mSource.mImports) { + mGroup.dependsOnGroup(s.mGroup); + } + } + + // an object file is built from its source + override void build() { + writefln("Object %s", mPath); + scope cmd = new StringFormatter; + + cmd.format("dmd -c @%s", Global.optionsPath); + foreach (path; Global.bundlePaths) { + cmd.format(" -I", path); + } + cmd.format(" -od%s -of%s %s", dirname(mPath), basename(mPath), mSource.mPath); + + if (std.process.system(cmd.str)) { + writefln("%s", cmd.str); + error(format("build of %s failed", mPath)); + } + } +} + + +// +// A library +// +class LibraryItem : FileItem { + string mName; + + this(string path, string name, GroupItem group) { + super("library", path, group); + mName = name; + } + + // a library is built by archiving all of its contributing ObjectFiles + override void build() { + writefln("Library %s", mPath); + scope cmd = new StringFormatter; + cmd.format("ar csr %s ", mPath); + foreach (item; mDepends) { + auto obj = cast(ObjectItem) item; + if (obj) { + cmd.format(" %s", obj.mPath); + } + } + if (std.process.system(cmd.str)) { + writefln("%s", cmd.str); + error("command failed"); + } + } + + // Add this library and any it depends on to libs, if they aren't there already. + // NOTE - they are added with libraries appearing after those they depend on + // (reverse of compiler command-line). + void addNeeded(inout LibraryItem[] libs) { + + void add(LibraryItem lib) { + foreach (item; libs) { + if (lib is item) { + return; + } + } + libs ~= lib; + } + + foreach (item; mDepends) { + auto lib = cast(LibraryItem) item; + if (lib) { + lib.addNeeded(libs); + } + } + add(this); + } +} + + +// +// A program +// +class ProgramItem : FileItem { + ObjectItem mObject; + + this(string path, ObjectItem object, GroupItem group) { + super("program", path, group); + mObject = object; + addDepends(object); + } + + // a program file is built from its object and all the libraries + // the object needs, transitively + override void build() { + writefln("Program %s", mPath); + scope cmd = new StringFormatter(); + + cmd.format("dmd -g @%s -L-L%s", Global.optionsPath, join(Global.buildPath, "lib")); + cmd.format(" -of%s %s", mPath, mObject.mPath); + + // add the libraries we need + LibraryItem[] libs; + foreach (item; mDepends) { + auto lib = cast(LibraryItem) item; + if (lib) { + lib.addNeeded(libs); + } + } + foreach_reverse (lib; libs) { + cmd.format(" -L-l%s", lib.mName); + } + + if (std.process.system(cmd.str)) { + writefln("%s", cmd.str); + error("command failed"); + } + } +} + + +// +// A test result - run the program to generate the result file +// +class ResultItem : FileItem { + ProgramItem mProgram; + + this(string path, ProgramItem program, GroupItem group) { + super("result", path, group); + mProgram = program; + addDepends(program); + } + + override void build() { + writef("Test %s", mPath); + if (exists(mPath)) { + mPath.remove(); + } + scope cmd = new StringFormatter(); + scope tmpPath = (mPath ~ ".failed").idup; + cmd.format("%s > %s 2>&1", mProgram.mPath, tmpPath); + + if (std.process.system(cmd.str)) { + // failed + writefln(" failed"); + writefln("%s", cmd.str); + error("test failed"); + } + else { + tmpPath.rename(mPath); + writefln(" passed"); + } + } +} + + + + +//------------------------------------------------------------------------------- +// Tree of modules, packages, products and system, etc - things in the source tree. +// When a node explicity depends on another, it (and its parent up to the common ancestor) +// implicity depend on that node and its ancestors and descendants, and everything they depend on. +// Circularities are not allowed. +//------------------------------------------------------------------------------- + +// +// a node in a tree. It depends on explicit mDepends, plus any children, and +// anything any of those depend on. The dependencies are tracked to enforce +// dependency rules. Actual build-order dependencies are done with Items. +// +class Node { + string mName; + Node mParent; + Node[] mChildren; + string mSlashName; + string mDotName; + + Node[] mDepends; + + // create the root of the tree + this() { + } + + // create a node in the tree + this(string name, Node parent) { + assert(parent); + mName = name; + mParent = parent; + if (parent.mName.length) { + // child of non-root + mSlashName = parent.mSlashName ~ sep ~ mName; + mDotName = parent.mDotName ~ "." ~ mName; + } + else { + // child of the root + mSlashName = mName; + mDotName = mName; + } + parent.mChildren ~= this; + } + + // return a node with the given name-chain, or null if not found. + // The root node is not considered part of the chain + final Node find(string[] chain) { + + // return child of node as specified by chain, or null if not found + Node locate(Node node, string[] chain) { + foreach (child; node.mChildren) { + if (child.mName == chain[0]) { + if (chain.length > 1) { + return locate(child, chain[1..$]); + } + else { + return child; + } + } + } + return null; + } + + // ascend to root and recursively descend to specified node + assert(chain.length > 0); + Node node = this; + while (node.mParent) { + node = node.mParent; + } + return locate(node, chain); + } +} + + +// +// A source-code module +// +class Module : Node { + SourceItem mSource; + ObjectItem mObject; + FileItem mClump; // library or program item this module contributes to + + this(string name, string path, Node parent, GroupItem group) { + super(name, parent); + string obj_path = join(Global.buildPath, "obj", mSlashName ~ ".o"); + mSource = new SourceItem(path, group); + mObject = new ObjectItem(obj_path, mSource, group); + //writefln("loaded Module %s from %s", mDotName, mSource.mPath); + } + + void setClump(FileItem clump) { + assert(!mClump); + mClump = clump; + mClump.addDepends(mObject); + } + + // trace direct imports + final void trace() { + + // add an import dependancy (if it is inside our project) + void imports(string importing) { + string[] chain = split(importing, "."); + assert(chain.length > 0); + + // only depend on internal products + if (!(chain[0] in Global.products)) { + return; + } + + // find the imported Module's source item and add it to our source item's imports + Module other = cast(Module) find(chain); + if (!other || !other.mClump) { + writefln("import of unknown module " ~ importing ~ " from " ~ mDotName); + } + else if (cast(LibraryItem)other.mClump) { + // the other's mClump is a library + + // our source imports other's source - only allowed for library source + mSource.addImport(other.mSource); + + if (mClump.mPath != other.mClump.mPath) { + // this module's clump depends on other's library (needed for linking) + mClump.addDepends(other.mClump); + } + } + else { + error(format("import of non-library module %s from %s", + importing, mDotName)); + } + } + + assert(mClump); + foreach (importing; parseImports(mSource.mPath)) { + //writefln("%s imports module %s", mDotName, importing); + imports(importing); + } + } +} + + +// +// An executable program. +// +class Exe : Module { + ProgramItem mProgram; + ResultItem mResult; + + this(string name, string path, Node parent, bool test, GroupItem group) { + super(name, path, parent, group); + if (test) { + mProgram = new ProgramItem(join(Global.buildPath, "test", mSlashName), mObject, group); + mResult = new ResultItem(join(Global.buildPath, "test", mSlashName ~ ".result"), mProgram, group); + } + else { + mProgram = new ProgramItem(join(Global.buildPath, "bin", mName), mObject, group); + } + setClump(mProgram); + //writefln("loaded Program %s", mProgram.mPath); + } +} + + +// +// a package +// +class Package : Node { + LibraryItem mLibrary; + GroupItem mGroup; // represents the package + + // load the package + this(string name, string path, Node parent) { + super(name, parent); + mGroup = new GroupItem(mSlashName); + + string lib_name = replace(mDotName, ".", "_"); + + // examine all the children of the package's directory + foreach (string child; listdir(path)) { + string p = join(path, child); + if (child[0] != '.') { + + if (isfile(p)) { + + if (child.length > 2 && child[$-2..$] == ".d") { + // a library module + if (!mLibrary) { + mLibrary = new LibraryItem(join(Global.buildPath, "lib", "lib" ~ lib_name ~ ".a"), + lib_name, mGroup); + } + Module m = new Module(getName(child), p, this, mGroup); + m.setClump(mLibrary); + } + } + + else if (isdir(p)) { + + if (child == "build-tool") { + // reserved for build-tool + } + else if (child == "doc") { + // TODO + } + else if (child == "data") { + // TODO + } + else if (child == "test") { + // test programs + foreach (string grandchild; listdir(p)) { + string p2 = join(p, grandchild); + if (grandchild[0] != '.' && + isfile(p2) && + grandchild.length > 2 && + grandchild[$-2..$] == ".d") + { + Exe exe = new Exe(getName(grandchild), p2, this, true, mGroup); + } + } + } + else if (child == "prog") { + // deliverable programs + foreach (string grandchild; listdir(p)) { + string p2 = join(p, grandchild); + if (child[0] != '.' && + isfile(p2) && + grandchild.length > 2 && + grandchild[$-2..$] == ".d") + { + Exe exe = new Exe(getName(grandchild), p2, this, false, mGroup); + } + } + } + else { + // a child package + Package pkg = new Package(child, p, this); + mGroup.addDepends(pkg.mGroup); + } + } + } + } + } + + // trace dependancies + void trace() { + foreach (child; mChildren) { + Package pkg = cast(Package) child; + Module m = cast(Module) child; + if (pkg) { + pkg.trace(); + } + else if (m) { + m.trace(); + } + } + } +} + + +// +// A Product - a top-level package +// +class Product : Package { + string mPath; + + this(string name, string path, Node parent) { + writefln("loading Product %s from %s", name, path); + mPath = path; + super(name, path, parent); + } +} + + +// +// the whole project +// +class Project : Node { + + // create the project, loading its bundle and those it uses + this(string path) { + super(); + load(path); + } + + // load the bundle at path + void load(string path) { + //writefln("loading bundle from %s", path); + + // add path to Global for use when compiling + Global.bundlePaths ~= path; + Global.optionsPath = path.join("options"); + + // + // load bundles specified in the uses file - lines are bundle paths relative to path + // + string uses_path = join(path, "uses"); + if (exists(uses_path) && isfile(uses_path)) { + //writefln("reading uses file: %s", uses_path); + scope file = new BufferedFile(uses_path); + foreach (char[] line; file) { + load(join(path, line.idup)); + } + } + + // + // load local products + // + foreach (string name; listdir(path)) { + if (name[0] != '.') { + string p = join(path, name); + if (isdir(p)) { + foreach (node; mChildren) { + Product existing = cast(Product)node; + if (existing && existing.mName == name) { + error(format("product %s has two paths: %s and %s", + name, p, existing.mPath)); + } + } + new Product(name, p, this); + Global.products[name] = true; + } + } + } + + + // + // trace imports, then finish the Item dependency graph + // + foreach (child; mChildren) { + Product product = cast(Product) child; + if (product) { + product.trace(); + } + } + foreach (item; Item.mItems) { + ObjectItem obj = cast(ObjectItem) item; + if (obj) { + obj.resolveImports; + } + } + + // + // print a dependency graph + // + foreach (group; GroupItem.mGroups) { + // TODO + } + + // + // Build everything that is out of date + // + foreach (group; GroupItem.mGroups) { + group.buildChain; + } + writefln("all done"); + } +} + + +//-------------------------------------------------------------------------------------- +// main - args[1] is where the project source is, and args[2] is where the build happens +// All the paths are set to absolute so that the source files +// can be found by an editor or debugger, wherever its current directory is. +//-------------------------------------------------------------------------------------- +int main(string[] args) { + int usage() { + writefln("Usage: builder <project-path> <build-path>"); + return -1; + } + + if (args.length < 3) return usage; + + string sourcePath = args[1]; + Global.buildPath = args[2]; + + writefln("building source at %s into %s", sourcePath, Global.buildPath); + + // do the work + scope project = new Project(sourcePath); + + return 0; +}