← docs3 min read

11. Modules and composition

Two complementary…

manual

Two complementary tools:

  • use loads a Lua module that defines helper functions in scope.
  • import pulls another Cookfile in under an alias, so its recipes and chores become alias.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 = "*"
Terminal window
cook modules install # everything in cook.toml; pins cook.lock (commit it)
cook modules install cook_cc # add one
cook modules update # bump within constraints

Then 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.shared target makers, pkg-config and curated discovery, transitive link/include propagation, compile_commands.json generation, 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:

/Cookfile
import web ./apps/web
import 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.

Terminal window
cd apps/web && cook build # web's own build recipe, by its bare name
cd ../.. && cook web.build # the same recipe through the root's alias

Same 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:

Terminal window
cook affected --since=origin/main # affected recipes, one per line
cook affected --since=origin/main --json
cook test --affected --since=origin/main # run only the affected slice's tests

A 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.