11. Modules and composition
Two complementary…
Two complementary tools:
useloads a Lua module that defines helper functions in scope.importpulls another Cookfile in under an alias, so its recipes and chores becomealias.name.
Blessed modules
cook resolves modules through LuaRocks against
rocks.usecook.com. Declare them in a cook.toml
next to your Cookfile:
[modules]cook_cc = "*"cook modules install # everything in cook.toml; pins cook.lock (commit it)cook modules install cook_cc # add onecook modules update # bump within constraintsThen use the module and call its target makers (a top-level use plus a
column-0 module call is all it takes):
use cook_cc
cook_cc.bin("game", { sources = { "src/main.c" }, needs = { "SDL3" },})Today’s roster:
cook_cc: the flagship. C and C++ native builds:cc.bin/cc.lib/cc.sharedtarget makers, pkg-config and curated discovery, transitive link/include propagation,compile_commands.jsongeneration, and autoconf-style feature checks. This is the reference for what a real module looks like.cook_pnpm: pnpm-driven JS/TS monorepos: Turborepo-style task pipelines running on cook’s content-addressed graph, so a JS workspace gets the same caching and unified graph as everything else.
cook_rust currently reserves the name and is not yet a working build
integration.
Workspaces and directory hopping
For a monorepo, each subproject keeps its own Cookfile and a root Cookfile
composes them with import:
import web ./apps/webimport api ./services/api
recipe ship: web.build api.build cook "out/release.tar" { tar cf $<out> dist }Then hop. Run cook from anywhere in the tree and it walks up to the nearest
Cookfile and behaves exactly as if you’d started there: bare names resolve
against that Cookfile’s own recipes, and its imports answer to their aliases.
cd apps/web && cook build # web's own build recipe, by its bare namecd ../.. && cook web.build # the same recipe through the root's aliasSame recipe, same work, same cache entry. Cache identity comes from a recipe’s own Cookfile and the workspace root, never from the directory you invoked from or the alias an importer chose, so hopping around (or renaming an alias) never splits the cache: the second command above is a cache hit on the first.
The workspace root is inferred by walking upward for the Cookfile that imports
the tree you’re standing in. Drop an empty .cookroot file at the top to make
the boundary explicit; it also stops the upward walk, so cook will never select
a Cookfile from an unrelated enclosing project. --root overrides everything.
cook affected
A monorepo PR usually touches one corner of the graph; CI shouldn’t build the
world to find out. cook affected maps a git diff onto the graph:
cook affected --since=origin/main # affected recipes, one per linecook affected --since=origin/main --jsoncook test --affected --since=origin/main # run only the affected slice's testsA recipe is affected when the diff touches its declared file inputs, or when it
depends (directly or transitively) on one that is: invalidation flows
downstream, so ship is affected whenever web.build is. The diff uses
three-dot merge-base semantics against --since and includes your working
tree: staged, unstaged, and untracked-but-not-ignored files all count.
--recipe <name> narrows the listing to recipes with that bare name across the
workspace (--recipe=build lists every *.build and *:build). The same
--affected --since pair on an ordinary run restricts it to the affected
slice; on cook test it’s the CI pattern from The cook CLI.