Skip to content

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
std.shell Run an arbitrary POSIX shell command with $OUTPUT set: the escape hatch
std.autotools ./configure --prefix=$OUTPUT && make && make install
std.make make && make install (no autoconf)
std.cmake cmake -B build && cmake --build build && cmake --install build
std.meson meson setup && meson compile && meson install
std.cargo cargo install / cargo build --release
std.go go build + install
std.npm npm install
std.ruby_gem gem build && gem install
std.python_wheel Build a wheel and install it (a single package)
std.python_venv pip install through pip's resolver (multi-package)
std.swift swift build -c release
std.binary Unpack a pre-built archive into $OUTPUT (no build); source supplies the bytes
std.copy_tree Copy files only

Each recipe lives in src/punix/realise/recipes_lib/std/<name>.py, and the recipeArgs each one accepts are documented at the top of that file. That docstring is the normative list. Frequently useful ones: command, src_dir, configure_flags, make_flags, cmake_flags, manifest_path, install_command.

src_dir is the one to reach for when a build fails with "manifest path not found" or "go.mod not found": the tarball extracted into a subdirectory the class didn't auto-detect. Use src_dir = "." for a flat extract, or src_dir = "foo-1.2.3" for the usual <name>-<version>/ layout. 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 (post prepare_source).
  • $BUILD: the build dir (under $OUTPUT for most recipes).
  • $NIX_BUILD_CORES: the recommended make -j value.
  • $MAKEFLAGS: -j$NIX_BUILD_CORES.
  • $PUNIX_ARCH: the target system (x86_64-linux etc.).
  • $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>/bin prepended (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:

  • command runs 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.