§ 24 — Both-phase API
24. Both-phase API
Normative. This chapter defines the Cook Lua API surfaces that are available in both register-phase and execute-phase Lua. The behavioural contract — signatures, error model, working-directory rooting — is identical across phases; only the surrounding scheduling differs.
One name and one sentence per door. A door defined in this chapter is one door. A conforming implementation MUST make every phase reach it under the same name, and MUST report a condition this chapter defines with the same diagnostic in every phase that can raise it. [added CS-0213]
This binds the whole chapter and applies to § 22‘s register-only surface from the other direction: where § C.6.12 requires an execute-phase call to a register-only helper to raise, the helper it refuses MUST be named identically to the one the register phase installs. A guard that refuses a name nothing installs refuses nothing, and it fails silently — the call resolves to nil and the author is told only that they attempted to call a nil value.
The requirement is bounded by what this chapter actually specifies. Where a section states phase-different behaviour — § 24.4.2, whose execute-phase form is withdrawn, and § 24.5.3, likewise — the two phases refuse for genuinely different reasons and their diagnostics differ accordingly. What MUST NOT differ is the answer to one question asked twice.
Nothing about this is automatic, and that is why it is written down rather than assumed. The two phases are separate Lua hosts, so every door this chapter names exists twice: once where registration installs it, once where execution does. Drift between them does not fail to compile and does not fail to link. It changes what a Cookfile may call in one phase and not the other, and the author discovers it at build time in the phase they happened to reach second. CS-0211 recorded one instance of this — a cook.sh failure located in one phase and anonymous in the other — and treated it as a fact about cook.sh. It was a fact about having two hosts, and this section states it once for every door instead of once per defect. (In the reference implementation the names live in cook_contracts::registration and the doors whose whole implementation is shared live in cook-lua-stdlib; that arrangement is informative, the requirement above is not.)
24.1. cook.sh
Signature. cook.sh(cmd: string) -> string
Phase. Both.
cook.sh executes cmd via /bin/sh -c in the Cookfile’s working directory with the inherited environment and nothing merged onto it — a declared variable is not a process-environment entry (§ 5.3), so $NAME in the command names an ambient variable, never a build variable — and returns the captured standard output as a string. A non-zero exit status MUST be reported as a Lua runtime error. The function name is intentionally shared between phases because the user-visible behaviour — “run this command and give me its stdout, error on non-zero” — is the same in both; only the surrounding scheduling differs.
In the register phase, cook.sh is a utility for control-flow decisions during registration (feature detection, tool discovery, reading version files). It is NOT the mechanism by which work units are recorded; use cook.add_unit for that. The capture-mode contract of § 14 applies: a register-phase cook.sh call MUST be deterministic with respect to the Cookfile source and the register-phase record accumulated so far, and MUST NOT modify files under the working directory in a way that affects declared outputs.
In the execute phase, cook.sh runs on the worker VM that is evaluating an execute-phase Lua chunk — for example, a cook / test / probe step’s >{ … } body payload (§ 23.1), whose Lua runs in that step’s own execute-phase work unit. As of CS-0134 a recipe body carries no imperative region, so there is no coalesced “body unit”; the former imperative-region run no longer exists for recipes (§ 15.2 is vacuous for recipe bodies). cook.sh remains the mechanism by which a lua_line or lua_block shells out, and inside a chore body it is the call into which the chore’s shell steps desugar under the single-drain model of § 7.
Both streams are captured, and both are attributed (CS-0188). Stdout is captured and returned to the calling Lua code, and stderr is captured too. A conforming implementation MUST attribute both to the calling unit, and MUST reach the user’s diagnostic channel with each carrying its stream of origin, so that a warning printed by one unit’s command is not confused with another’s. A unit whose body calls cook.sh more than once MUST preserve the order of those calls in what it reports; within a single call the two streams are buffered separately, and their relative interleaving is not preserved and MUST NOT be presented as though it were.
Until CS-0188 this section said only that “stderr is streamed to the worker’s stderr”, which named a destination rather than an owner and was not implemented: the reference implementation discarded a successful cook.sh call’s stderr entirely, so a command that compiled with warnings reported none of them. Attribution is stated here rather than left to the diagnostic layer because the worker’s stderr is one file descriptor shared by every unit running in parallel, and bytes written to it carry nothing that says which unit produced them.
A failure is located, and located the same way in both phases (CS-0211). When an implementation can determine the Cookfile line the failing cook.sh call sits on, the reported failure MUST carry that line, and MUST do so in both phases: a call that fails during registration and the same call failing during execution MUST NOT differ in whether the author is told where it was. When the implementation cannot determine a line — the call is reached from Lua whose source is not the Cookfile, for instance a probe’s produce body — it MUST report the failure without a location rather than with a substitute one. A placeholder line is worse than no line, because a reader cannot tell the two apart.
The phase symmetry is the point, and it is not automatic: the two phases are separate Lua hosts, so “which line called this” is a question each can answer independently and therefore answer differently. Before CS-0211 the reference implementation’s execute phase supplied a constant zero to every cook.sh call, so a failure inside a >{ … } body, a lua_line, or a chore body was reported with no location at all. Its register phase walked the Lua stack for the Cookfile frame when registering a recipe and supplied the same constant zero to cook.sh — so a register block’s failing cook.sh was anonymous too, and remained so after CS-0211 fixed the other phase. [amended CS-0215] Nothing in this section had said the two owed the same answer, which is why nothing detected that they were not giving one.
What is reported is what the author wrote (CS-0215). Where a reported failure quotes the failing command, the quoted text MUST be the command as the Cookfile expresses it. An implementation that composes text around an author’s body — the set -e prelude § 8.3.1 puts in front of a shell_block, and any comparable prelude — MUST NOT include that text in what it reports, on any report of the failure: the progress line for a failing step, the final diagnostic, and a test report’s account of what blocked a test are each a report of one failure and the rule binds each of them. The prelude is the implementation’s, and a reader shown it is being asked to debug a line they cannot find in their file.
An implementation MAY abbreviate a quoted command differently in different reports — a progress line has a width a final diagnostic does not — provided every abbreviation is of the author’s text. What is forbidden is quoting composed text anywhere, not varying the length.
How a failure travels is not part of what is reported (CS-0215). An implementation whose phases exchange a failure through an encoded payload — the reference implementation encodes one into a Lua error message, because a Lua host returns a string and not a structure — MUST decode it before display. The encoding MUST NOT reach the user’s diagnostic channel, on any path, including paths where the failure is not the one the run was waiting on. A diagnostic that is the transport rather than the message is worse than an unhelpful one: it presents an implementation’s private format as the thing the author is meant to read.
Note 24.1.1 (informative; CS-0215)
The two rules above are one shape seen twice, and it is the shape § 24.1 keeps producing: a cook.sh failure passes through more than one renderer, and every renderer that re-decides how a failure reads is a chance for two of them to decide differently. CS-0211 found it in the location — one renderer omitted a zero line, the next printed line 0. CS-0215 found it in the command text, where the reference implementation had four quoting sites and one of them, the progress line, quoted the composed text so that every failing block opened with a set -e the author had not written; and in the payload, which had two producers and one consumer, so the phase whose failures did not reach that consumer printed the encoding verbatim.
Both were fixed the same way, and it is the way this Standard’s phase-symmetry rules are meant to be met: the decision is made once, over the failure’s data, and each renderer asks for the answer rather than computing one.
24.2. cook.load_module
Signature. cook.load_module(target: string) -> table
Phase. Both.
cook.load_module resolves target against the calling work unit’s source Cookfile and returns the module’s Lua table. target is a module name or a tree-relative path, under the same rule that separates the two forms of the use declaration (App. A.2): it is a path if and only if it ends in .lua. [amended CS-0206]
The lookup is lexical per Cookfile (§ 12.2): an imported Cookfile’s body unit, whose working_dir is its own import directory, resolves cook.load_module("foo") against that directory’s module tree, and cook.load_module("./lua/foo.lua") against <import_dir>/lua/foo.lua. The importing Cookfile cannot reach into the imported Cookfile’s modules and vice versa.
A conforming implementation MUST apply § 12.2.1 to a path target — rejecting an absolute path, a // sigil path, a .. segment, and a path whose resolved location lies outside the calling Cookfile’s directory subtree — with a runtime error. This call is a door the surface check cannot cover: a Cookfile’s use declarations are validated at parse time, and a cook.load_module call is composed at run time from whatever the body computes. Enforcing at the door is what makes the containment rule a property of the system rather than of the spellings a parser happened to see. [amended CS-0206]
A conforming implementation MUST configure package.path and package.cpath for the calling Lua VM such that sub-requires within a multi-file module resolve against the project’s installed-module search paths (§ 12.2) rather than the host system’s Lua paths. A conforming implementation MUST allow C extension modules (.so/.dll) to be loaded during cook.load_module evaluation. [amended CS-0066]
When cook.load_module is exposed on the execute-phase Lua VM, the execute-phase implementation MUST honour the same resolution order used at register phase (§ 12.2), and the same containment rule for a path target (§ 12.2.1). Recipe-body Lua scripts that call cook.load_module(name) directly therefore see the same set of candidate paths as register-phase callers, eliminating “works at register but not at execute” surprises. [amended CS-0069] In the reference implementation both phases probe the ONE candidate list and compose package.path/package.cpath through the ONE composer in cook_contracts::layout (COOK-393), and since CS-0203 the whole loader sequence around them — memoisation and cycle detection keyed by (working_dir, name) per § 12.3, resolution, init(), and the diagnostics — is likewise one implementation (cook-lua-stdlib) installed on both VMs, so the agreement is structural rather than manually mirrored (informative).
A conforming implementation MUST report a runtime error when target does not resolve to a file. The diagnostic MUST identify target and the absolute path(s) that were searched.
The body of a module loaded into the execute-phase VM MUST NOT call register-only API (cook.add_unit, cook.exec, cook.interactive, cook.step_group, cook.recipe). A conforming implementation MUST surface such a call as the § C.6.12 runtime error. Module functions whose bodies are restricted to both-phase API (cook.sh, var, cook.probes, cook.platform, fs.*, path.*) work in either phase; module authors document per-function which phase a function targets.
cook.load_module is the resolver behind the use declaration (§ 12.1), for both of its forms: a path-form declaration binds through this same call, with the path as the argument (local helpers = cook.load_module("./build/helpers.lua")), and not through a second resolver of its own. A path-loaded module is a module — same lifecycle, same init(), same memoisation and cycle detection, and, decisively, the same door, so § 17.1.1.3 records it exactly as it records a named one. [amended CS-0206]
A conforming implementation MUST bind the alias in both phases, and MUST bind it by calling cook.load_module: at register phase by emitting local foo = cook.load_module("foo") at the top of the Cookfile’s register-phase Lua, and at execute phase by prepending the same binding to the Lua source of every execute-phase body the declaring Cookfile contains — a cook_step body, a test_step body, a chore body, and a probe’s produce body alike (§ 12.1). Binding through this call rather than through an implementation-private path is what makes an alias-bound module a determinant of the unit that used it (§ 17.1.1.3). [amended CS-0205]
An implementation MAY omit the execute-phase binding for a body that does not reference the alias, and MUST make that decision from the body’s Lua source, treating a FREE occurrence of the alias as a reference: an identifier outside a string or a comment that is not the field or method half of an access. t.foo and t:foo() name a field, not the alias; x..foo.f() names the alias, because the dot before it is the second half of the concatenation operator. The binding is a Lua local, and a local is reachable only by code that names it lexically — a chunk built by load and a lookup through _ENV cannot see it at all — so omitting an unreferenced binding changes nothing an author can observe. Emitting it unconditionally would not be merely wasteful: it evaluates the module’s top-level chunk and init() on a worker VM for a body that never asked for the module, which is a runtime error for a module whose top level uses register-only API (§ C.6.12), and it makes that module’s source a cache determinant of every Lua-bodied unit in the Cookfile. [amended CS-0205]
Module lifecycle — the top-level chunk’s once-per-VM execution, the init() call, per-VM caching, cross-VM rehydration, and cycle detection — is specified in § 12.3, not here. This section defines only the call surface.
Note 24.2.1 (informative; CS-0206)
Lua’s dofile and loadfile are not doors. This Standard neither sanctions nor forbids them in a module or recipe body — they are banned only inside a config_body’s sandbox (§ 5.3) — but § 17.1.1.3 observes the two doors § 12.3.6 names, cook.load_module and require, and neither of these is one of them. A body that reads Lua through dofile therefore composes a cache key with no term for the file it just ran, on a key that addresses a store shared between machines.
Before CS-0206 dofile was the only way to reach a Lua file that was not in the package directory, which made the hazard hard to avoid and easy to excuse. It is neither now. The path form gives the same reach with the lifecycle, the memoisation, the cycle detection, and the keying; an author who reaches past it is opting out of all four at once, and this note exists so that the choice is legible rather than accidental.
24.3. var
Type. table (read-only outside a config_body)
Phase. Both.
var is the declared-variable namespace of § 5.3: a global table whose contents are exactly the names the Cookfile’s config blocks exported. It is NOT the process environment, and a conforming implementation MUST NOT seed it from one (CS-0172). The table it exposes has three semantic distinctions from a bare table:
- Reads are checked.
var.NAMEfor a name no config block exported MUST raise a Lua runtime error suggesting the closest exported names. Presence-guarding an undeclared variable (if var.maybe then) is therefore not a supported idiom — declare it with a default instead. - Writes are refused. An assignment through
varoutside aconfig_bodyMUST raise. A declared value is a determinant of every unit keyed against it (§ 17.1); mutating it mid-pass would invalidate a key already computed. - Values carry their declared type on the register-phase VM:
string,number, orbooleanper § 5.3.
On the execute-phase VM, values are visible in their string form (§ 5.3). A conforming implementation MUST reject, at register phase, a Lua-body unit (§ 8.7) whose statically-recorded reads (§ 17.1.2) name a variable whose declared value is not a string — naming the variable and the phase. Silently handing the body a truthy "false" is the failure this rule exists to prevent; the author either interpolates $<NAME> or branches in a register block.
Within a config_body (§ 5.3), var is the writable output sink instead — see § 21.3.
Example 24.3.1
config
var.compiler = "clang"
var.optimize = "-O2"
var.symbols = true
cc.toolchain({
compiler = var.compiler,
extra_cflags = var.optimize,
debug_symbols = var.symbols,
})
The config body writes the sink; the top-level module call reads the same names through the read-only global, with var.symbols arriving as a boolean. The names are equally resolvable as $<compiler> / $<optimize> / $<symbols> placeholders in recipe bodies (§ 9.3), where $<symbols> interpolates the string true.
24.4. cook.probes
Phase. Both. [Pinned by CS-0070.]
cook.probes is the Cook-managed key/value store available to register-phase and execute-phase Lua. Its purpose is to let modules persist register-phase computations (compiler detection, package finder results) into a form that execute-phase code can read without redoing the work.
24.4.1. cook.probes.get
Signature. cook.probes.get(key: string) -> any
On the execute-phase VM, cook.probes.get(key) MUST read from the per-run probe-value store specified in § 22.5.8. The store is populated by completed probe units in topological order before any consuming unit dispatches. A key absent from the store MUST raise § 22.5.8‘s unmaterialised-read error (CS-0152); a key that IS materialised whose canonical value is null reads as Lua nil with no error. [This sentence said “an unknown key returns nil” until CS-0203 — the nil-on-miss rule CS-0152 had already withdrawn where it is normatively specified.] cook.probes.set is NOT available on the execute-phase VM (deprecated entirely per CS-0074).
On the register-phase VM, cook.probes.get(key) returns the value previously stored under key via cook.probes.set (or nil if no value is stored). The value MUST round-trip through any serialisation the implementation uses for persistence; non-serialisable values (Lua functions, userdata, coroutines) MUST raise a Lua runtime error when passed to cook.probes.set.
24.4.2. cook.probes.set
Signature. cook.probes.set(key: string, value: any) -> nil
Deprecated (CS-0074, v0.11; renamed
cook.cache→cook.probesin CS-0136). Not available on the execute-phase VM; full removal deferred to a later 1.x revision. Usecook.probe(§ 22.5) for observed values.
Stores value under key. Subsequent cook.probes.get(key) returns value (or its deserialisation-equal counterpart).
A register-phase write MUST be persisted by the implementation in a way that survives across Cook invocations: a register-phase set followed by a later register-phase get in a different Cook invocation MUST return the previously-stored value. The on-disk format is implementation-defined.
cook.probes.set is NOT available on the execute-phase VM; a conforming implementation MUST raise a Lua runtime error when cook.probes.set is called from execute-phase Lua.
24.4.3. cook.probes.scope
Signature. cook.probes.scope(label: string) -> table
Deprecated (CS-0074, v0.11; renamed
cook.cache→cook.probesin CS-0136). Full removal deferred to a later 1.x revision. Usecook.probe(§ 22.5) for observed values.
Returns a “scoped” table view whose get(key) and set(key, value) operations are equivalent to cook.probes.get(label .. ":" .. key) and cook.probes.set(label .. ":" .. key, value) respectively — on whichever VM the view was created, with that phase’s get/set semantics and restrictions (§ 24.4.1, § 24.4.2). In particular, a scoped set on the execute-phase VM raises exactly as the unscoped form does, and a scoped get there reads the per-run store and reports the full prefixed label:key in the § 22.5.8 unmaterialised-read error (CS-0152). The label string MUST NOT contain the : character; a conforming implementation MUST raise a Lua runtime error on such a label.
cook.probes.scope MUST be available on both VMs. [CS-0203. The deprecation banner above claimed “not available on the execute-phase VM” while § 22.5.8 normatively described the execute-phase scoped get; the reference implementation meanwhile had the inversion — scope installed on the execute VM and absent from the register VM, where the scoped pattern this section defines died with a nil-call error. Both the banner and the implementation were wrong in opposite directions; conformance fixture 37-register-probes-scope pins the register side, and the label rule above is now enforced on both.]
Scopes are conventionally named after the module that owns them (e.g. cook_cc:toolchain, cook_cc:find:raylib) to prevent key collisions between modules.
24.5. cook.export and cook.import
Phase. Register only. [CS-0200; was “Both” and cited CS-0071, see below.]
This section is retained in the both-phase chapter for stable anchors; the surface is register-phase and calling either function from an execute-phase VM MUST raise, in the manner of § 24.4.2.
cook.export and cook.import are the Cook-managed transitive-link-info channel used by target makers (e.g. cc.bin/cc.lib per § 28.4) to publish a target’s outward-facing fields and by consumers to read them.
24.5.1. cook.export
Signature. cook.export(name: string, info: table) -> nil
Records that the target named name exposes the fields in info. The info table’s shape is conventionally established by the publisher’s module documentation; for cc targets the conventional shape is documented in § 28.4. Beyond requiring a Lua table, the Standard constrains info only to be serialisable under the § 22.5.5 value contract (CS-0198): the exported value crosses VM boundaries as JSON, so a table that JSON cannot faithfully carry — mixed string/integer keys, an array with holes, a non-finite number, a non-UTF-8 string, a function/userdata/cycle anywhere within — MUST be rejected with a register-phase (or execute-phase, per the calling VM) Lua error naming the offending path, in the manner of § 22.5.5. A conforming implementation MUST NOT silently drop or coerce the unserialisable parts: before CS-0198 the reference implementation did exactly that (mixed keys dropped, holes compacted, NaN to null, non-UTF-8 lossy-substituted), so an export could differ from what the author published with no diagnostic.
A conforming implementation MUST persist the exported value in a way that subsequent register-phase cook.import(name) calls, within the same Cookfile evaluation, return the exported value.
24.5.2. cook.import
Signature. cook.import(name: string) -> table?
Returns the table previously published for name via cook.export, or nil if no export has occurred.
A conforming implementation MUST resolve cook.import against the same workspace’s export records — cook.import MUST NOT cross workspace boundaries. The interaction with cross-Cookfile composition is: a target maker invoked from an imported Cookfile (§ 11.2) publishes exports against the workspace-global table, but resolution by recipe name in a different Cookfile uses the qualified name alias.name per § 11.3.
24.5.3. Why this is register-phase only
The paragraph this replaces permitted “a per-worker scratch store when the recipe body is both producer and consumer”, setting the minimum bar at round-tripping “within a single worker VM”. § 12.3.4 simultaneously required a register-time export to be observable to an execute-phase cook.import. A per-worker store cannot satisfy that, so the two clauses could not both be met, and the reference implementation met the permissive one: its execute-phase VM built an empty per-VM table, never seeded it, and returned nil for every register-time export.
Neither clause described anything a module needed. The transitive-link walk that motivates the whole channel (§ 28.4) runs at register time, because its product is the compile and link command lines, which must exist before any unit is captured. Reading an export also forces the referent’s recipe body (§ 22.9), which is a register-phase act by definition.
The per-worker store was additionally unsound on its own terms: units of one recipe are dispatched from a shared queue to whichever worker is free, so “within a single worker VM” is not a scope an author can rely on. Two units of one recipe routinely land on different VMs, making an execute-phase export visible or invisible according to scheduling.
CS-0200 therefore withdraws the execute-phase half rather than implementing it, matching the treatment § 24.4.2 gives cook.probes.set for the same reason: a write into a per-VM store that no other unit can observe is refused by name rather than allowed to half-work.
24.6. cook.platform
Type. table
Phase. Both.
cook.platform is a Cook-managed read-only table exposing the host platform’s identity. The following fields MUST be populated by a conforming implementation:
| Field | Type | Value |
|---|---|---|
os | string | "linux", "darwin", "windows", or "unknown". |
arch | string | "x86_64", "aarch64", "arm", "riscv64", or "unknown". |
triple | string | The host’s compiler target triple, e.g. "x86_64-unknown-linux-gnu", "aarch64-apple-darwin". Implementation-defined when the host is not recognised by the implementation’s target-triple resolver. |
The values MUST be identical between the register-phase Lua VM and every execute-phase worker VM within a single Cook invocation. Implementations realise this by sharing a single source-of-truth resolver across VMs; the reference implementation does so via cook-lua-stdlib::platform.
Forcing (register phase, inside a recipe body). When cook.import(N) is called from within a recipe body during the register phase and N has published no export yet, a conforming implementation MUST force N’s body to completion before returning, to the standard § 22.9 defines — same transitivity, same at-most-once rule, same prohibition on degrading the forcing to a silent skip. The call then returns N’s export.
Forcing is a property of reading an export, not of declaring a dependency. A module resolving a link reference calls cook.import at exactly the moment it needs the referent’s body to have run; requiring it to arrange that separately, through an API whose other effect is a whole-recipe ordering edge, is what coupled export resolution to execution ordering in the first place (§ 22.11, CS-0161).
Three boundaries on the rule. Where N has already exported, the implementation MUST NOT force — the value is returned directly, so the common case stays a lookup and the rule is a no-op once a body has run. Where the call is made outside a recipe body — a cook.on_register_complete finalizer (§ 22.10) reading exports to emit a compilation database, say — the recipe set is closed and forcing is illegal; cook.import MUST remain a plain lookup there, returning nil for an absent export. And where N has no export and is not a recipe registered in the current pass, the implementation MUST surface the forcing error rather than returning nil: a silent nil is the mislink § 22.9 exists to prevent, and a caller that merges the result into a link line emits one missing the library.
Example 24.6.1
if cook.platform.os == "darwin" then cook.add_unit({ command = "install_name_tool -id @rpath/libfoo.dylib build/lib/libfoo.dylib", cache = false, })endThe cook.platform.os check happens at register time on the register-phase VM; the unit is recorded only on macOS hosts.
24.7. cook.dep_output and cook.dep_output_list
Phase. Both.
cook.dep_output and cook.dep_output_list are the Lua-side counterpart of the $<NAME> string-substitution surface (§ 10.5). A >{ … } or test >{ … } body that needs to reference another recipe’s output uses these functions instead of textual placeholder substitution.
24.7.1. cook.dep_output
Signature. cook.dep_output(name: string) -> string
Returns the named recipe’s output list joined by single space characters. Equivalent to the textual expansion of $<NAME> per § 10.5.
If the named recipe’s output list is empty, returns the empty string; a conforming implementation MUST emit a warning at register time naming the referring recipe and the referent. The reference itself is not an error.
If name does not name a recipe in scope, a conforming implementation MUST raise a Lua runtime error at the calling step’s register time. Both phases can reach this condition — registration resolves the reference, execution resolves it against the registration record — and per § 24‘s one-name-and-one-sentence rule the diagnostic MUST be the same sentence in each. It reports the name as the author wrote it, not as the implementation qualified it: a reader looking for no_such_recipe in their Cookfile cannot find some.prefix.no_such_recipe. [amended CS-0213]
24.7.2. cook.dep_output_list
Signature. cook.dep_output_list(name: string) -> table
Returns the named recipe’s output list as a 1-indexed Lua table of strings. Equivalent to cook.dep_output(name) but preserved as a sequence so that callers can iterate, index, or apply path accessors (§ 10.3) per-element.
Same error model as cook.dep_output.
Example 24.7.1
recipe libs
cook "build/lib/libfoo.a" { ar rcs $<out> ... }
cook "build/lib/libbar.a" { ar rcs $<out> ... }
recipe app
cook "build/bin/app" >{
local libs = cook.dep_output_list("libs")
local cmd = "gcc -o " .. output .. " main.c"
for _, lib in ipairs(libs) do
cmd = cmd .. " " .. lib
end
cook.sh(cmd)
}
The cook.dep_output_list("libs") call returns { "build/lib/libfoo.a", "build/lib/libbar.a" }. An equivalent shell-body recipe would use $<libs> per § 10.5.
24.8. cook.json_decode and cook.yaml_decode
Phase. Both.
24.8.1. cook.json_decode
Signature. cook.json_decode(text: string) -> table | string | number | boolean | nil
Parses text as JSON and returns the corresponding Lua value: objects and arrays become tables (arrays 1-indexed), strings/numbers/booleans map to their Lua counterparts, and JSON null becomes nil (as an object member value, the key is absent). Malformed JSON is a Lua runtime error tagged json error.
24.8.2. cook.yaml_decode
Signature. cook.yaml_decode(text: string) -> table | string | number | boolean | nil
Parses text as YAML under the same value mapping as cook.json_decode. Malformed YAML is a Lua runtime error tagged yaml error.
Note 24.8.1
The codecs are pure functions of their argument; they perform no I/O and consult no phase state. A conforming implementation MUST install both on the register-phase VM and on every execute-phase worker VM, so a probe produce body (§ 22.5.3) behaves identically whether it is evaluated by the gather <probe> register pre-pass or by a demand-driven worker (CS-0123).
24.9. cook.tools.id
Phase. Both.
Signature. cook.tools.id(name: string) -> { hash: string, path: string } | nil
Resolves name on the current PATH and returns its canonical tool identity: hash is the lowercase-hex SHA-256 of the resolved binary’s content — a machine-independent identity — and path is the location it resolved to right now. Returns nil when the name does not resolve. The call MUST resolve and hash the binary as it exists at call time.
An implementation MAY memoize the hash per resolved path within a single run, provided that on every call it re-checks what the host makes cheaply observable about the file the path names, and re-reads whenever that observation has moved. A memo that serves its first read for the remainder of the run does not satisfy the preceding paragraph: a run may itself rebuild a binary it later hashes. The permission therefore covers avoiding a repeated read of observably unchanged content and nothing more, and an implementation SHOULD state the residual window its observation leaves rather than claim it has none (CS-0212).
The function exists so a module can fold a real toolchain identity into a probe value it seals (§ 12.7.5) without hashing large binaries in Lua: the hash field is the value to fold — a compiler upgrade changes the bytes, the hash, the sealed value, and therefore every sealing unit’s key. The path field is location metadata for invocation and MUST NOT be folded into a sealed value: a machine-specific path in a sealed value re-keys identical toolchains across machines, defeating the cross-machine reuse § 17 exists to provide (the same rule the native tools producer’s value shape enforces, § 22.5.2).
Note 24.9.1
A conforming implementation MUST install cook.tools.id on the register-phase VM and on every execute-phase worker VM, so a probe produce body behaves identically on the register pre-pass and the demand-driven worker path (the CS-0123 rule, applied to this API). The reference implementation backs both installs with the same per-run tool-hash memo a top-level tools declaration’s value resolution uses (§ 22.5.2), so calling cook.tools.id re-reads a binary only when the file it resolved to has changed since the memo last read it. (CS-0158, CS-0212.)