Recipes¶
A recipe is a named build script. PCL points at one with recipe = "std.NAME"; the realiser dispatches to the matching module under src/punix/realise/recipes_lib/std/.
module Curl {
pname = "curl"
version = "8.5.0"
recipe = "std.autotools"
recipeArgs = { configureFlags = ["--without-ssl"] }
source = { … }
}
Shipped recipes¶
| Recipe | What it does | Required recipeArgs |
|---|---|---|
std.shell |
Run an arbitrary POSIX shell command with $OUTPUT set |
command: Str |
std.autotools |
./configure && make && make install (with --prefix=$OUTPUT) |
(optional) configureFlags, makeFlags |
std.make |
make && make install (no autoconf) |
(optional) makeFlags, installCommand |
std.cmake |
cmake .. && cmake --build . && cmake --install . |
(optional) cmakeFlags |
std.go |
go build + install |
(optional) package, subdir |
std.python_wheel |
Build a wheel; install via pip | (optional) extras |
std.binary |
Unpack a pre-built archive into $OUTPUT (no build) |
none — source supplies the bytes |
Each recipe lives in src/punix/realise/recipes_lib/std/<name>.py. Recipe code is content-addressed via the canonical derivation — edit a recipe's .py (or its sibling _postinstall.sh, _dep_env.py) ⇒ the hash changes ⇒ every package using that recipe gets a new store path. The conformance suite has a dedicated test for this against a recipe-hook edit.
The recipe field in the canonical derivation¶
recipe = "std.autotools" is not just a string lookup. The canonical derivation includes the recipe's identity:
"recipe": {
"name": "std.autotools",
"source_hash": "<sha256 of the recipe's .py + sibling files>"
}
That means:
- Two packages with
recipe = "std.autotools"share the recipe's source_hash ⇒ if the recipe is edited, both rebuild. - A user-defined recipe with the same name but different bytes (impossible today, but conceptually) would have a different source_hash ⇒ distinct paths.
This is the cache-staleness fix that v2 makes structural. Old: "did I make clean after editing _postinstall.sh?" New: edit the file ⇒ all dependents already have a different hash ⇒ they'll be rebuilt next punix build.
Recipe environment¶
Every recipe runs in a sandbox with:
$OUTPUT— the temp path that becomes the store path on success.$SOURCE— the source dir (postprepare_source).$BUILD— the build dir (under$OUTPUTfor most recipes).$NIX_BUILD_CORES— the recommendedmake -jvalue.$MAKEFLAGS—-j$NIX_BUILD_CORES.$PUNIX_ARCH— the target system (x86_64-linuxetc.).$FINAL_STORE_PATH— the predicted final path. Important: this is the path the artifact will live at after the build. Languages that bake the prefix in (Ruby's--prefix, ELF's RPATH) need this value at build time, before the artifact has moved.$PATH— each dep's<store>/binprepended (in dep order). Nothing else — no system PATH leakage.
Writing a recipe¶
A recipe is class StdRecipe(Recipe): (or a function — see existing recipes for the pattern). The run(req) method receives a RealiseRequest and writes to $OUTPUT:
class Shell(Recipe):
def run(self, req: RealiseRequest, sandbox: Sandbox) -> None:
command = req.recipe_args["command"]
script = f"#!/bin/sh\nset -eu\n{command}\n"
sandbox.run(script=script, …)
You can ship custom recipes in your repo — they participate in the canonical derivation like any other (the recipe's source-hash field makes them addressable). Stage 7+ corpus migration may upstream patterns into std.
Fixed-output recipes¶
A source = { type = "fixed_output", command = "…", hash = "…" } is special:
commandruns in the sandbox (with network permitted — fixed-output IS the network seam).- The resulting tree is hashed (
compute_tree_hash). - If the hash matches the declared one ⇒ success. If not ⇒
[E13].
This is how Punix model arbitrary fetches (git clones, custom protocols, etc.) — the recipe says "this is what I'll produce", the realiser verifies. A [E13] conformance test pins this.
→ CLI: punix build for the build flow. → Reference: error codes for E10–E13.