Packages and Distribution
A package
A package is content rooted at a directory with a package.json, selected under a shop
package identity. Nothing inside it assigns that identity. An installed package is found
through the shop’s package store; an editable package is found through an explicit shop
link. The build resolves only identities it was given. It never walks a checkout looking
for nested manifests, so co-located package directories are packages only when the shop has
linked or installed them.
The manifest is written by hand and committed, and it has three fields:
| field | says |
|---|---|
aliases | short names this package’s modules may write for other packages, with their version selectors |
modules | a logical module name and its implementation per target |
compilation | CFLAGS and LDFLAGS, with per-target overrides |
That is the whole of it. There is no name, because the shop mapping supplies identity. There is no
natives listing the package’s C, because a C file’s path is its name too — render.c is
<pkg>::render and listing that selected package directory is reading the module list.
There is no members: package dependencies and recipe lists provide the identities to
resolve. A package of ten modules that writes full locators everywhere has an empty manifest
and works perfectly well.
Alongside it sits lock.json, generated and committed. The lockfile pins every
transitive package to an exact {version, commit, tree_hash}, flags which of them
bring C with them, and is the record that makes a build reproducible. The manifest states
intent; the lockfile states what intent resolved to.
Where aliased packages come from
An alias names a source, and every source is a store — something that
answers get_manifest and get_content:
- a git host over https or the git protocol;
- a directory on disk, which supplies its own
package.json; - a git repository on disk, which already has one;
- a shop, mounted directly or served by an actor in front of it;
- a bundle, a portable snapshot of one;
- a cart, which is a store too, placed for a target.
There is no privileged source and no second protocol behind any of them. Whatever the backend, resolution is manifest-first: read the manifest, take the hashes the closure needs and the local layers lack, verify each on arrival. A git host is asked for the blobs and trees behind those hashes, not for a repository.
On first use a package is pinned. One added without a constraint tracks the main branch; one added with a constraint records it and moves within it. Constraints are written once and stay put; pins move when you ask them to.
One version per executable
Versions resolve when an executable is built, over that executable’s own closure, and
exactly one version of any module ends up in it. One executable cannot contain the same
file from the same package at two different versions; the closure builder decides between
them or refuses, and a closure needing two incompatible majors of one package is a build
error reported rather than resolved by picking one. <package>::<file> exists exactly once
in a closure.
The rule is written down in three places that agree by construction. Aliases are the version pins — a package says which packages it depends on and which version of each. The lock records what those pins resolved to, exactly. The executable manifest carries one identity arm per canonical package: one version, one tree, one row. Nothing downstream gets to disagree, because nothing downstream re-decides.
That rule rests on identity. Resolution lands on a package identity and never re-parses
locator text: every candidate carries the identity it resolved to, so two files that came
from one package are recognisably from one package however they were named. A step that
guessed a unit’s origin by splitting its locator on / would quietly agree that the two were
different, and the collision would go undetected instead of being reported.
This is stricter than resolving per build, and deliberately so: an executable is the unit that gets sealed and shipped, so it is the unit whose graph must be unambiguous. Two different executables in one checkout may legitimately resolve the same package differently.
Sameness is content: two aliases that resolve to the same version resolve to the same content hash, so they are stored once and shared. Aliases are the version knob, and they fold into the realization key.
Shipping
A program is sent as a manifest first. The recipient compares that manifest against what its own store already holds, fetches only the objects it is missing, and verifies each one against its hash as it arrives. Nothing is trusted because of where it came from — the signature says who published it, the hashes say the bytes are intact, and the recipient’s own policy decides what the result may hold.
There are three ways to ship a game, and they trade openness against size and speed.
A boot cartridge and a bundle. The cartridge is small and boots the runtime; the bundle carries the game. A minimal boot cartridge runs the bundle’s finalized pools directly, needing only the VM. The same bundle carries source and the lockfile in its development slice, so a player with a full development runtime can open it, change it, and rebuild. One artifact serves both.
A development cartridge and a bundle. The same arrangement with a cartridge built for debugging, so a program running on real hardware can be inspected from a development machine.
A sealed ROM. The cartridge, the runtime binary, and the native code linked into one object. This is the smallest and fastest form — memory is laid out once, C calls bind directly — and it is the form for a target that loads exactly one image.
Which of these a project ships, and what rides inside the cart, is the build description: the arguments of the invocation that composes it. It is a decision made per build, not a property of the machine being built for — see Recipes.
Bundles
A bundle is a snapshot of a shop that can be opened: content objects, a manifest naming them, and the lockfile. It is signed, mountable, and verified object by object — the same store the developer built from, made portable.
The signature covers the manifest’s bytes, and it travels beside them. A bundle carries its signature as its own member, over the manifest member exactly as written — so a recipient verifies the bytes it received and never re-derives them. Nothing a verifier must reconstruct can be part of a signature: the manifest is a record, records are unordered, and two constructions of one record are not obliged to encode alike. The manifest names each object’s content hash, so the signature authenticates the manifest and the hashes authenticate the objects.
It is sliced, so a recipient takes only the part it needs:
| Slice | Carries |
|---|---|
player | realizations, pools or native images, assets — enough to run |
development | sources, mcode, compiler inputs and build metadata — enough to rebuild |
debug | origin maps and source trees — enough to read a stopped frame |
Carrying the lockfile is what makes a bundle a starting point rather than an endpoint. Content addressing means the developer and the player agree, byte for byte, on what the player started from. The lockfile names that state in terms the developer’s own repository understands. So a player who changes a module can send back the change and the commit it was made against, and the developer can reconstruct exactly that starting point and merge into it.
A cartridge is the same store with a placement plan. It holds the same content-addressed objects behind the same manifest; what it adds is final addresses, banking, and residency for one machine, and what it gives up is portability. Openness is a matter of which slices a build ships, not of which container they arrive in — a cart may carry source and a compiler, and a bundle may carry nothing but a player slice. See Cartridges.