← docs2 min read

2. Your first recipe

cook init drops a minimal starter Cookfile and a .gitignore. The Cookfile is deliberately…

manual
Terminal window
mkdir hello-cook && cd hello-cook
cook init

cook init drops a minimal starter Cookfile and a .gitignore. The Cookfile is deliberately tiny:

recipe build
cook "build/hello.txt" { echo "built with cook" > $<out> }
chore clean
rm -rf build

Run it:

Terminal window
cook build # or just `cook`: with no name, cook runs the recipe called `build`
cook # again: cached, zero work
cook clean # remove the build output

A recipe is a named bundle of work. It starts with the keyword recipe, a name, and (optionally) a colon-separated list of dependencies. The indented body below declares the work: here, a single cook step naming an output (build/hello.txt) and the command that produces it. Add a gather line to build from input files, as the next sections show.

A recipe body is declarative: a list of inputs and outputs, not a script. The step kinds it may contain are gather, cook, test, seal, and module calls. A loose shell command (echo hi on its own line) is not one of them: imperative, run-every-time commands belong in a chore. This is the one thing to unlearn if you’re coming from make or npm scripts.

Two phases: register, then execute

When you run cook build, cook does two distinct things. First it registers the recipe: it runs the body once, on a single Lua VM, in capture mode, to record the work to be done (it doesn’t run the echo yet, it records a unit that says “later, run this command”). Then it builds a dependency graph and executes the units. When you read a Cookfile, you’re reading a plan, not a script. This two-phase model is what makes the caching and the dependency graph possible.

Handy from day one:

Terminal window
cook menu # list every recipe and chore in the workspace