Recipes
A recipe is a named bundle of lever positions, committed as an ordinary file. It is the whole of what a target is. Nothing in the runtime, the VM, cake, or the shop takes a target name to mean “these things” — the name is a label the recipe declares for its own settings, and inventing a target is writing a file.
recipes/darwin.json
recipes/playdate.json
recipes/n64.json
Listing that directory is reading the list of targets. There is no enum, no supported-target array, and no second place where a target’s facts are kept.
What a recipe is made of
A recipe has four parts: the platform package it draws its C from, the stamp levers, the build levers, and the shop’s lists.
recipes/ps1.json, whole — the smallest target in the tree, and every field the schema has:
{
"pit.recipe": 1,
"name": "ps1",
"platform": "ps1",
"system": "ps1",
"stamp": {
"profile_name": "ps1-nan32-v1",
"rep": "nan32", "endian": "le",
"text": "utf32", "record": "hash",
"gc": "compact", "wota": 32
},
"build": {
"sched": "single",
"budgets": {
"ar_seconds": 60, "fast_turn_seconds": 1,
"slow_turn_seconds": 60, "slow_strikes": 3,
"dynamic_region": 65536, "static_region": 16384, "max_images": 2
},
"capabilities": {
"native_payload": false, "dynamic_native": false,
"daemon": false, "press": false
},
"linked_packages": [
"shoplib", "pitlib", "std", "cart", "wgpu", "libgit", "daemon-stub"
]
},
"lists": {
"aliases": {},
"fallback": ["pitlib", "std"],
"endowments": ["lang-endowments", "std_endowments"],
"endowments_whole": ["std_endowments", "ps1"],
"c_native": [],
"bin": ["shop_tools", "cake"]
}
}
name is what the recipe calls itself and must match the filename. system is the token a
package’s module arms are matched against —
the one sanctioned place a target name appears outside a recipe, where a package declares how
it varies. platform names the package that supplies this machine’s distinguished C
roles. It is a package identity, resolved by the shop exactly like linked_packages and
the provider lists; the recipe assigns no filesystem location. In this repository the
Makefile links identities such as ps1, darwin, and playdate to directories below
platform/ for development. Those directories can move to separate repositories without
changing Cake or the recipe format.
When a platform package also contains provider modules, the recipe includes it in
endowments_whole. platform assigns the target-specific roles described below;
endowments_whole selects all of its top-level C providers for static linking. Other
provider packages are selected the same way. Darwin, for
example, selects posix, bsd, apple, darwin, metal, and spritekit there, while its
linked_packages selects support packages such as host, posix-runtime, posix-daemon,
and cjson.
The format is JSON because a recipe is data. The loader validates it before anything acts on it, and a validator cannot validate a module that has already run.
The two lever groups never mix
This is the structural rule of the schema, and it is enforced rather than documented: a stamp
lever written under build, or a build lever written under stamp, is a load failure that
names the offending lever.
Stamp-entering levers change interpretability
value, rep, endian, text, record, gc, wota — and the profile_name
they hang off. All but value are written; value is derived from rep (below) and a
recipe that states it fails to load. These decide the layout of bytes, so an artifact built under one position is
not slightly wrong under another, it is uninterpretable. They render back into the
profile string exactly:
ps1-nan32-v1;value=32;rep=nan32;endian=le;text=utf32;record=hash;gc=compact;wota=32
The stamp group is the profile string and nothing more — the axis list and its order are
the schema. sched is a build lever, not a stamp axis: the scheduler arrangement
selects provider files and changes no artifact byte, so identical pools load under a
single-threaded and a threaded runtime alike. Stamping it would refuse byte-identical
pools over an axis that never touches bytes — the exact false fork the stamp’s own rule
(interpretability axes only) exists to prevent.
Every Mach pool, every cart, and the runtime binary carry that string. Mounting compares them and refuses a mismatch by naming both.
The recipe is the authority, and the build derives from it. Every stamp fact a compiled binary states about itself is taken from the recipe at forge time, never hardcoded per axis arm. A combination the compiling toolchain cannot honor is refused at forge, by naming the lever and the reason; a binary whose self-description disagrees with its recipe is a build that must not exist. One record decides, the build renders it, and the artifact stamps repeat it — three copies of one truth, flowing one direction.
value is derived, not written. A rep names a complete value-word scheme, and the
word width is part of what it names — so the recipe states rep and forge derives
value from it, rendering both into the profile string. A contradiction between them is
not refused; it is unwritable. wota stays a recipe lever: it usually matches the
value width because both follow the machine, but they answer different questions — rep is
chosen for the value representation the target wants to live in, wota for what the
architecture serializes fastest — and a build may legitimately pair a small rep with a
wide wota when memory and messaging pull in different directions.
Non-stamp levers change what a build contains or may do
budgets, capabilities, linked_packages. None of them moves a byte in any artifact, and
none may enter the stamp.
capabilities says what a target is capable of — a machine with no unix sockets cannot
have a daemon at any setting. It does not say what one product built for that target
carries: that is a per-product feature selection, derived from the product’s own start
declaration and its closure, which narrows linked_packages on the way past. So
capabilities.daemon: true on darwin means “darwin can”, while a one-shot product forged
for darwin still compiles none of the daemon’s C and swaps posix-daemon for
daemon-stub. See
Features are per product, not per target.
budgets carries two kinds of number. The paging numbers — dynamic_region,
static_region, max_images — say how the runtime windows memory. The lifetime
budgets say what the scheduler decides about actors:
| Budget | Meaning |
|---|---|
ar_seconds | how long an actor may sit idle before it is reaped; -1 is never |
fast_turn_seconds | how long one turn runs before it is frozen and re-queued |
slow_turn_seconds | how long a resumed turn runs before that counts as a strike |
slow_strikes | how many strikes halt the actor |
These are the whole of the runtime’s liveness policy (see Actors and Memory), and they belong to a target rather than to the code: a console reclaims sooner and tolerates less than a workstation, and the same source should not have to know which it is running on. Each has a compiled-in fallback, so a recipe states only what it wants to move.
That restraint is load-bearing rather than tidy. If a capability lever entered the stamp, every development runtime would sit in its own artifact universe: the realization cache would fork, and a cart built against a dev binary would refuse to mount on the shipped binary it was built for. A budget is the same argument in miniature — a larger region window holds the same bytes, so keying artifacts on it would invalidate a cache for a number no reader of those bytes consults.
The two groups are two records, not one flat bag with a comment, because that is what lets the loader tell them apart.
The recipe describes the runtime; composition is the composer’s arguments
A recipe describes one product: the runtime binary. The other two products — the cart and the bundle — are described per build, by whoever composes them: which actors go in, which payload form ships, whether the cart carries a compiler. Those are the composing invocation’s own arguments:
pit bundle app my/game::main.ce --name Game --payload mcode \
--with pit-shop/logger,pit-shop/courier
--payload is the form this build’s modules ship as; --with is the service fleet the
build carries beyond its own root, and passing none is a real answer — it is what a bare
console build wants. There is no schema and no file, because these are one
verb’s parameters: everything they decide is recorded in the product’s own manifest, so
reproducibility lives in the output while the invocation keeps a makefile’s agility. The
standard invocations are committed where invocations live — in the makefiles and scripts
that build each product — and a cart for a small device that carries a minimal compiler
plus game source, compiling on first boot, is one more such script. No per-target table
exists on either side: a console gets an empty fleet because its build script passes no
--with, not because it is a console.
The line that earns a file is the recipe’s: data several tools must agree on, that keys artifacts. One invocation’s parameters never qualify.
The platform package names roles; packages supply membership
The platform package answers for what it supplies. Its package.json declares
requirements — only the distinguished roles a runtime for this platform needs.
{
"requirements": {
"entry": "runtime::platform/n64/source/main_n64.c",
"scheduler": "runtime::platform/n64/source/sys_thread_single.c",
"daemon": "runtime::platform/daemon-stub/source/daemon_stub.c",
"provides": ["runtime::internal/os"]
}
}
There is no source-file list. Every selected package contributes the C below its source/
directory as runtime support, and a provider package selected by endowments_whole
contributes its top-level C as providers. entry, scheduler, and daemon merely identify which structurally discovered
files play those roles. provides names modules that support C defines by hand rather than as
an extension: os_n64.c is runtime::internal/os on that machine, so the package says so.
There is no platform-owned link list either. Every selected package states what it needs in
its ordinary compilation.LDFLAGS. Thus apple owns Foundation, CFNetwork, Security, and
Network; metal owns Metal and QuartzCore; and spritekit owns SpriteKit. An optional
package’s flags remain only when its PIT_USE_PROBE module compiled, so a separate
link_probe table would restate the same dependency.
Toolchain accommodations go in each package’s ordinary
compilation block, the same field every package has:
-x objective-c on the Apple platforms, -fPIC where a build needs it, the linker’s
dynamic-export spelling. They are flags a package needs for its own C, so that package says
them, and the runtime builder reads the selected package
closure without knowing which platform it is reading.
None of it is a target name interpreted by code. The recipe selects packages; directory position supplies membership; the platform package assigns the few distinguished roles.
The toolchain is the gate. Whether a combination works is answered by compiling it with the target’s toolchain: a build that pulls in a package whose headers don’t exist on that toolchain stops right there, and that failure is the signal — precise, current, and maintained by nobody. The same is true of levers. Every position is expressible on every platform, because at this level it is just portable C; a 64-bit double-math build for a 32-bit console compiles if its toolchain can, and how it runs is the machine’s answer.
The committed recipes are suggestions: worked lever positions that suit each machine, written down so the next person starts from experience. Writing a different recipe for the same machine is the normal way to disagree with one.
The other way is pit forge <name> --set stamp.<lever>=<value>, which is one build
disagreeing rather than one machine. The override goes through this same validator, so it can
no more mix the groups than a file can. What decides between the two is whether the position
is anybody’s: a target somebody builds for earns a file, because a file is what other tools
can agree on and what artifacts key against. A position that exists for the length of one
run — the thirteen the profile sweep walks to prove the space still
compiles — describes no machine and outlives no command, so it is an argument. That is the
same line the composition parameters fall on, one product over.
How a build selects one
A build names a recipe, and everything downstream reads it:
- The build names a target —
--target n64. That name selectsrecipes/n64.jsonand does nothing else. It never reaches code that knows what an n64 is. - The loader reads the file and validates the schema and the two-group separation.
- The stamp levers render the profile string that every artifact this build produces will carry, and that every artifact it mounts must match.
- The build levers decide what compiles in and what the result may do; the lists decide what
useand$startmean inside it. - The
systemtoken selects the module arms in each package’s own manifest, so a package with per-platform implementations binds the right one.
Nothing in that sequence consults a table keyed by target name. The name selects a file; the file says everything else.
Cake is where that promise is kept or broken, and it is kept: hand cake a name no recipe describes and every per-target answer comes back empty — no runtime C, no link flags, no static rows, no endowments. There is no “everything else” branch left to fall into.
The one reader
shoplib::recipe reads recipes and nothing else does. It lives in shoplib because both
sides need the same answer and cake cannot import the shop: cake asks it what compiles, and
the shop folds its answer into recipe_fingerprint and therefore into every realization key.
Two consequences follow from that fold. Editing a recipe re-keys every realization, exactly as editing the compiler does — an artifact realized under the old recipe was realized for a different target description. And the fold takes every recipe rather than the host’s, because a shop realizes artifacts for other targets too, so keying on the host’s alone would let a cross-target lever move without re-keying what it moved.
Adding a target
Write a recipe and fill a platform package with the C that compiles on the machine. No page in this track changes, no enum grows, nothing in the VM or in cake learns a name, and the only things that gain a row are the recipe file itself and the target matrix, which is a record of recipes people have written rather than a list anything consults.
The profile axes are the reference for what each stamp lever means, and Building C is how cake turns the answer into binaries.