2. Your first recipe
cook init drops a minimal starter Cookfile and a .gitignore. The Cookfile is deliberately…
mkdir hello-cook && cd hello-cookcook initcook 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 buildRun it:
cook build # or just `cook`: with no name, cook runs the recipe called `build`cook # again: cached, zero workcook clean # remove the build outputA 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:
cook menu # list every recipe and chore in the workspace