Skip to main content

TDBuilder: A build system using Deno

Using TDBuilder you can specify rules for building files or directories, or running other tasks.

It is based on https://github.com/TOGoS/NodeBuildUtil.

TDBuilder does not do anything by itself, but can be used by a script, which you might call make.ts, which might look something like this:

import Builder from 'https://deno.land/x/tdbuilder@0.3.4/Builder.ts';

const builder = new Builder({
    rules: {
        "hello-world.txt": {
            description: "An example text file",
            invoke(ctx:BuildContext) {
                Deno.writeTextFile(ctx.targetName, "Hello, world!\n");
                return Promise.resolve();
            }
        },
        // A rule to build foobar.txt is not specified,
        // so it must be present already, e.g. committed to the project repo.
        "concatenation.txt": {
            description: "An example of a rule with prerequisites",
            prereqs: ["hello-world.txt", "foobar.txt"],
            async invoke(ctx:BuildContext) {
                const allContent = await Promise.all(ctx.prereqNames.map(file => Deno.readTextFile(file)))
                return Deno.writeTextFile(ctx.targetName, allContent.join(""));
            }
        },
        "test": {
            description: "Run all unit tests!",
            cmd: [Deno.execPath(),"test", "--allow-read=src", "src/test/deno"]
        }
    },
    logger: console,
    // If the user just runs `deno run make.ts`, we'll build the listed targets:
    defaultTargetNames: ["concatenation.txt","test"]
});
Deno.exit(await builder.processCommandLine(Deno.args));

The build rules and related API look like this:

type BuildResult = { mtime: number };
type BuildFunction = (ctx:BuildContext) => Promise<void>;

export interface MiniBuilder {
    build( targetName:string, stackTrace:string[] ) : Promise<BuildResult>;
}

export interface BuildContext {
    builder: MiniBuilder;
    logger: Logger;
    prereqNames: string[];
    targetName: string;
}

export interface BuildRule {
    description?: string;
    /** a list of names of targets that must be built before this build rule can be invoked */
    prereqs?: string[]|AsyncIterable<string>;

    /** Function to invoke to build the target */
    invoke? : BuildFunction;
    /** An alternative to invoke: a system command to be run */
    cmd?: string[],

    // Metadata to allow Builder to automatically fix existence or mtime:

    /** If false, the target will be removed if the build rule fails */
    keepOnFailure?: boolean;
    /**
     * What does the target name name?
     * - "auto" (default assumption) :: the target may be a file, a directory, or nothing.
     *   If a file or directory by the name does exist, its modification time will be used.
     *   Builder won't verify existence after invoking the build rule.
     * - "directory" :: it is expected that a directory matching the name of the target
     *   will exist after the rule is invoked, and builder will automatically (by unspecified means)
     *   update the modification timestamp of the directory after the rule is invoked.
     *   If the target does not exist after invoking the build rule, or is not a directory
     *   (or symlink to one), an error will be thrown.
     * - "file" :: the target name names a file to be created or updated,
     *   and if the file does not exist or is not a regular file (or symlink to one) after the rule is invoked,
     *   an error will be thrown.
     * - "phony" :: the target is assumed to not correspond with anything on the filesystem,
     *   and will always be run.
     */
    targetType?: TargetTypeName
}

There is currently (as of 0.3.4) no way to indicate a build rule that builds multiple targets. As a workaround, define one of the targets (preferrably a non-phony one) and list it as a prerequisite for the others.

Rules are run in parallel as much as possible. Lock a mutex in invoke() if you want to prevent certain build steps from running at the same time.

Normally you shouldn’t need to reference ctx.builder, but you can if you need to dynamically request to build a prerequisite.

Run your script with -v to generate some info on the console about targets being built.