RAD RAD
^click-me

rad πŸ’―

A general purpose build tool. Concise, statically typed, batteries included. Command tasks, function tasks, and make-style tasks supported.

Jump to:

  1. Usage
  2. Install
  3. What
  4. Why not <my-favorite-build-tool>?
  5. Manual
branch status
main main
next next

Usage

Rad is generally used as a CLI:

$ rad <task-name> [--help]

For example, $ rad build or $ rad --log-level=info test!

It can be used as a library too :).

Rad always consumes a rad.ts file, such as the one shown here:

// rad.ts
import { Task, Tasks } from "https://deno.land/x/rad@v6.7.1/src/mod.ts";

// command/shell tasks
const format = `prettier --write`;
const test = `deno test`;

// function tasks
const compile: Task = {
  dependsOn: [format],
  fn: ({ sh, ...toolkit }) => sh("tsc"),
};
const greet = {
  fn: () => Deno.writeTextFile("/tmp/hello", "world"),
};

// make-style tasks
const transpile: Task = {
  target: "phony",
  prereqs: ["prereq1", "prereq2"],
  async onMake({ logger }, { changedPrereqs /*, prereqs */ }) {
    for await (const req of changedPrereqs) {
      logger.info(`req: ${req.path} ${req.isFile}`);
    }
  },
};

export const tasks: Tasks = {
  compile,
  format,
  greet,
  test,
};

Install

There are a few formal ways to use rad. Regardless of the route you choose, know that all strategies support using pinned versions, adherent to semver. See the releases page.

usage install-method install-steps
cli deno deno install --unstable -f -A -n rad https://raw.githubusercontent.com/cdaringe/rad/v6.7.1/src/bin.ts
cli docker docker pull cdaringe/rad 1
cli curl curl -fsSL https://raw.githubusercontent.com/cdaringe/rad/v6.7.1/assets/install.sh | sh (versioned)
curl -fsSL https://raw.githubusercontent.com/cdaringe/rad/main/assets/install.sh | sh (latest)
library deno import * as rad from https://github.com/cdaringe/rad/blob/main/v6.7.1/mod.ts

1For docker users, consider making a nice shell alias

# shell profile, e.g. .bash_profile
function rad() {
  docker run --unstable --rm -v $PWD:/rad cdaringe/rad --log-level info "$@";
}

What is it

A build tool! It competes with make, npm-scripts, velociraptor, bazel, gradle, ant, gulp, or any of the other many tools out there! On various metrics, rad is subjectively better than some of the outstanding tools out there, and in some cases, not-so-much. We invite you to understand some of its core characteristics and interfaces.

rad offers:

See why not <my-favorite-build-tool>?

Read more on our documentation site

why not <my-favorite-build-tool>?

your build tool is probably great. keep using it if you love it. the intent here it not to dump on anyone or any tool, but articulate feature gaps.

tool DSL-less static types standalone polyglot incremental debug-able beautiful dependency manager
bazel βœ“ βœ“
denox
gradle βœ“ βœ“ βœ“
gulp/grunt βœ“ βœ“ βœ“
make βœ“ βœ“ βœ“
npm-scripts βœ“ βœ“
rad βœ“ βœ“ βœ“ βœ“ βœ“ βœ“
velociraptor partial partial

ant, scons, ninja, etc were omitted. haven't used 'em! other builders, such as cargo, dune, nx, and friends were omitted, as they are not generally considered general purpose. npm-scripts & the Deno tools are not general purpose either, but included merely by proximity of underlying tech stacks used.

Here are some genuine, not-trying-be-rude-opinions.

Loose typing, unneeded DSLs, lack of great static analysis, and other gaps leave room for improvement in this space. rad is the first tool to bring a subjectively powerful scripting language into the build system space coupled with great static analysis and make-capable performance.

Manual

Your guide to rad!

Understanding rad

🀯

Getting started

The first step to using rad is installation. Please see the install section for guidance.

The CLI also has a decent help page. Once you have installed rad, try running rad --help, to grow acquainted with some of the options you may expect to use down the road.

Next up, creating a radfile!

Setting up rad.ts

To create a new radfile (rad.ts), run the following command:

$ rad -l info --init

rad.ts should have two key traits:

Tasks are named in rad.ts strictly by their key in the tasks object.

export const tasks: Tasks = {
  meet: /* omitted task */,
  greet: /* omitted task */
}

The above file has exactly two tasks--meet and greet! Simple!

rad will look in the working directory for your radfile by default. If you so choose, you may place rad.ts elsewhere, and tell rad where to find it using the -r/--radfile flag. Next up, let's define those tasks.

Tasks

Tasks can take a couple of different forms. Generally, you can simply refer to the Task type in your radfile and get cracking. Let's write a few tasks of each type.

Command tasks

Command tasks are the simplest tasks. they are shell commands for rad to execute:

// rad.ts
import type { Task, Tasks } from "url/to/rad/mod.ts";

const compile: Task = `clang file.c`;
const greet: Task = `echo "hello, world!"`;

export const tasks: Tasks = { compile, greet };

Command tasks are the only 'string'ly defined tasks. The associated command will get executed by rad in a child process, a la <your-shell> -c '<your-cmd>'. For example, rad greet would be executed via bash -c 'echo "hello, world!"' under the hood if you are using the bash shell.

☝🏼Command tasks should tend to be fast. If the executed command is not fast, you may consider trying a function or make style task to speed things up, if feasible.

Function tasks

Function tasks are the most capable of all tasks. All other task types internally get transformed into a function task. To use function tasks, create a POJO with a fn key and function value. fn brings one argument--toolkit--that offers a nice suite of batteries for your convenience. ⚑️

// rad.ts
import type { Task, Tasks } from "url/to/rad/mod.ts";

const build: Task = {
  fn: async (toolkit) => {
    const { logger, sh } = toolkit;
    await sh(`clang hello.c -o hello`);
    logger.info(`compile ok, asserting executable`);
    await sh(`./hello`); // stdout: Hello, world!
    logger.info("binary ok");
  },
};

export const tasks: Tasks = { build };

This is a pretty basic function task. When you get to the toolkit section, you will see the other interesting utilities provided to do great things with! You can of course also simply run rad, and introspect the toolkit API if you are using a deno plugin in your code editor!

Make tasks

Make tasks are in honor of gnu make. Our make task is intentionally not feature complete with the proper make task--but it does have an essential core parity--providing an API to access only files that have changed since the last task run. More specifically, it offers an API to access only files that have been modified since the target has been last modified. target is make-speak for an output file. Our make tasks also exposes all files specified by your prerequisites as well. One essential difference between our make task and proper-make tasks is that your onMake function will still run even if no files have changed since the make target has changed--it is up to you to do nothing in the task handler if no work is warranted. How do you know if know work is warranted? You can consult the changedPrereqs iterator or the getChangedPrereqFilenames function. Both symbols signal to you changes that have occurred since the target's last modification.

Let us take inspiration from a make task in our very own source project--the build for this very website. Here is a simplified version:

// rad.ts
import type { Task, Tasks } from "url/to/rad/mod.ts";

const site: Task = {
  target: "public/index.html",
  prereqs: ["assets/site/**/*.{md}"], // globs only
  onMake: async (
    /* toolkit api -- see #toolkit */
    { fs, logger },
    /* make task api */
    {
      getPrereqFilenames, // Promise<string>
      /**
       * prereqs, // AsyncIterable<WalkInfo>
       * changedPrereqs, // AsyncIterable<WalkInfo>
       * getChangedPrereqFilenames, // Promise<string>
       */
    },
  ) => {
    await fs.mkdirp("public");
    logger.info("collecting prereq filenames");
    const filenames = await getPrereqFilenames();
    const html = await Promise.all(
      filenames.map((filename) =>
        Deno.readTextFile(filename).then((markdown) => marked(markdown))
      ),
    ).then((htmlSnippets) => htmlSnippets.join("\n"));
    await Deno.writeTextFile("./public/index.html", html);
  },
};

If you have many prereqs, you should consider using the AsyncIterator implementations referenced above so as to not eat all of your memory πŸ˜€.

gnu make also has a pattern syntax for when your task maps N prereqs to M targets. This is a pretty handy feature. If you have prereqs and the targets can be considered functions of the prereq files, make style tasks can remove the target task key, and instead use mapPrereqToTarget. Here is what that looks like in practice:

export const tasks: Tasks = {
  clean: `rm -rf 'build'`, // command style task
  build: {
    // make style task
    prereqs: ["src/*"],
    mapPrereqToTarget: ({ cwd, /* string */ prereq, /* string */ reroot }) =>
      reroot("src", "build", "coffee", "js"),
    // maps src/tacos.coffee => build/tacos.js
    async onMake() {
      /* snip snip */
    },
  },
};

This is certainly more verbose than make's syntax. However, it has the benefit of being a clear, debuggable function πŸ€“! Further, this API does not force you to have 1:1 mappings between inputs and outputs. If prereqs a and b mapped to foo and c mapped to bar--no problem, you can express that easily in mapPrereqToTarget!

Check out the type definitions for more on make tasks!

Task dependencies

All task types, except command style tasks, accept an optional dependsOn array. dependsOn is an array of task references. Task references must be actual task references--string based task lookups are not supported, intentionally. Stringy lookups are brittle, and would be redundant functionality in rad.

dependsOn tasks can be serialized by setting the sibling field dependsOnSerial: true

// rad.ts
import type { Task, Tasks } from "url/to/rad/mod.ts";

const install: Task = {
  target: "node_modules",
  prereqs: ["package.json"],
  onMake: async ({ sh }, { getChangedPrereqFilenames }) => {
    const changed = await getChangedPrereqFilenames();
    if (changed.length) await sh(`npm ci --verbose`);
  },
};
const lint: Task = {
  dependsOn: [install],
  fn: ({ sh }) => sh(`npm run lint`),
};
const test: Task = {
  dependsOn: [install],
  fn: ({ sh }) => sh(`npm test`),
};

const ci: Task = {
  dependsOn: [lint, test],
};

export const tasks: Tasks = {
  ci,
  install,
  lint,
  test,
};

Sweet! I bet node.js users wish they could just clone a repo and run rad test, then let the system "know" exactly what is needed to be test-ready! Similiarly, as shown above, our ci task should depend on lint & test tasks, of which both will await an install to complete!

You can see what a task dependsOn by using --print-graph:

$ rad ci --print-graph
└─ ci
   β”œβ”€ lint
   β”‚  └─ install
   └─ test
      └─ install

Dude. Nice! We can have nice things!

Toolkit

The toolkit is the first argument to function based tasks!

It has the following type!

export type Toolkit = {
  Deno: typeof Deno;
  fs: fs.FsUtil;
  sh: typeof sh;
  dependentResults: any[];
  logger: Logger;
  path: typeof path;
  task: RadTask;
  iter: typeof iter;
};

Well that's not super helpful! Let us study each these keys, one-by-one:

key value
Deno see the deno api docs
fs a few sugar methods, { readFile, writeFile, mkdirp } that work on strings, vs buffers, and assume utf8 for shorthand
sh execute a shell command. see the command task section above!
dependentResults results of dependsOn tasks. currently these are untyped. getting type inference here is tricky. PRs welcome!
logger the rad logger! a standard Deno logger with the commonplace log-level methods (e.g. .info(...), .debug(...), etc). see the source
path a direct reference to deno node path. this API is likely to change if Deno implements a full, proper path module
task a reference to the internal RadTask
iter AsyncIterable utility functions

Debugging

To debug your tasks, you can use deno's built in debugging functionality from v8.

#!/bin/sh
# generated by deno install
exec deno run --allow-read --allow-write --allow-net --allow-env --allow-run --allow-plugin --allow-hrtime --unstable 'https://raw.githubusercontent.com/cdaringe/rad/v6.7.1/src/bin.ts' "$@"

Example:

deno  \
  run --inspect-brk -A --allow-plugin --allow-hrtime --unstable \
  'https://raw.githubusercontent.com/cdaringe/rad/v6.7.1/src/bin.ts' \
  test
# ^ add any args of interest

Finally, ensure you have setup your debugger/IDE of choices to connect. See this project's <root>/.vscode/launch.json to see example settings & the deno debugging documentation for more.