rad π―
A general purpose build tool.
- Concise, statically typed, batteries included.
- No DSL, no stringly typed tasks, no malarkey.
- Command tasks, function tasks, and
make
-like tasks supported.
Jump to:
branch | status |
---|---|
main | |
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@v8.0.1/src/mod.ts";
// command/shell tasks
// [name: string, cmd: string]
const format = ["format", `prettier --write`];
const test = ["test", `deno test`];
// function tasks
const compile: Task = {
dependsOn: [format],
fn: ({ sh, ...toolkit }) => sh("tsc"),
// name: "compile" [optional]
};
const greet = {
fn: ({ fs }) => fs.writeFile("/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 --global -f -A -n rad https://raw.githubusercontent.com/cdaringe/rad/v8.0.1/src/bin.ts |
cli | docker |
docker pull cdaringe/rad 1 |
cli | curl |
curl -fsSL https://raw.githubusercontent.com/cdaringe/rad/v8.0.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/v8.0.1/mod.ts |
1For docker users, consider making a nice shell alias
# shell profile, e.g. .bash_profile
function rad() {
docker run --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:
- simple, programmable task interfaces
- easy to understand, declarative build steps
- type-checked tasks
- no quirky DSLs (
make
,gradle
, and friends π’). your build is code, not an arbitrary language or stringly (read: bummerly) typed script runner. - productive toolkit API for nuanced tasks that benefit from programming. see toolkit
- bottom-up,
make
-style build targets- fast builds, skip redundant work when inputs haven't changed
- cli mode, or library mode
- portability. build automation for any language or project, in many
environments (*limited to Deno target architectures, for the time being.
long term, we may package this in
Rust
) - great UX
- debug-ability. π inspect your data, tasks, or even rad itself
- employs a real scripting language--not
bash/sh
! shell languages are great for running other programs, not for plumbing data
See
why not <my-favorite-build-tool>
?
Read more on our documentation site
Manual
Your guide to rad
!
Understanding rad
rad
is written in typescript and runs on deno- You write tasks, then ask
rad
to run them rad
reads your radfile (i.e.rad.ts
), compiles & type checks it, then runs it through its task graph executor
π€―
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:
- an
import type { Task, Tasks } from 'https://path/to/rad/src/mod.ts
statement - an
export const tasks: Tasks = {...}
statement
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 = ["compile", `clang file.c`];
const greet: Task = ["greet", `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 fielddependsOnSerial: 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.
- get the underlying
deno
command used when runningrad
viacat $(which rad)
#!/bin/sh
# generated by deno install
exec deno run --allow-read --allow-write --allow-net --allow-env --allow-run --allow-hrtime 'https://raw.githubusercontent.com/cdaringe/rad/v8.0.1/src/bin.ts' "$@"
- extract the
deno run ...
command, drop the leadingexec
and trailing$@
, or equivalents for your shell - add a
--inspect
or--inspect-brk
flag afterrun
Example:
deno \
run --inspect-brk -A --allow-run --allow-hrtime \
'https://raw.githubusercontent.com/cdaringe/rad/v8.0.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.
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 | ||||||||
deno scripts | β | |||||||
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.
bazel
is complex. maybe you need that complexity. π€·π»ββοΈgradle
is full of magic and is often hard to reason about where behavior comes from. Once kotlin integration is first class, it's worth revisiting gradle.gulp
/grunt
have no make-style tasks, are generally node only, comparatively slow, & stringly typed in various places.make
is great, but has a lame DSL and is coupled to a poor scripting languagenpm-scripts
/velociraptor
are simple, but suffer related drawbacks to gulp, grunt, and make.
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.
build buds
Some interesting build functions that are not integrated into rad
are
checked into the rad
repository. These are generally considered unofficial or
unsupported tools that may be of interest to rad users.