maru

Guides build:compile

Your first build

Declare a task with explicit inputs and outputs, then watch maru skip it when nothing changed.

A maru build is a set of tasks. Each task declares the files it reads, the files it writes, and the command that turns one into the other. maru reads those declarations, builds a graph, and runs only what it must.

Declare a task

Tasks live in maru.toml under a [task.<name>] table. Here is a task that compiles a small C program:

[task.compile]
inputs  = ["src/*.c", "include/*.h"]
outputs = ["build/app"]
cmd     = "cc src/*.c -Iinclude -o build/app"

The inputs and outputs fields are not documentation — they are the contract. maru will make exactly the files in inputs visible to the command and will treat anything the command writes outside outputs as an error. This is what lets the cache be trustworthy: a task cannot secretly depend on a file it did not name.

Run it

maru build compile
# ▸ compile        run     412ms

Run it again without changing anything:

maru build compile
# ▸ compile        cached    2ms

The second run does no compilation. maru hashed the inputs, the command, and the toolchain, found a matching entry in the cache, and restored the output. The 2ms is the time to check the key and materialize the result.

Connect tasks

Tasks depend on other tasks through their outputs. A test task that runs the compiled binary simply lists it as an input:

[task.test]
inputs  = ["build/app", "tests/*.txt"]
outputs = ["build/report.txt"]
cmd     = "./build/app --check tests > build/report.txt"

maru sees that build/app is produced by compile, adds the edge, and orders the two tasks automatically. You never write "run compile before test" — the data dependency is the ordering. To see the graph maru inferred, run maru graph; to understand why a particular task ran, run maru explain test, which prints the cache key and the first input whose digest changed.