Your first build¶
Goal: get a single PCL module to type-check, lower to IR, and realise into the content-addressed store. ~5 minutes.
A minimal package¶
Create a directory:
mkdir -p ~/punix-tutorial
cd ~/punix-tutorial
mkdir src # the package's source dir (any contents work)
Write hello.pcl:
module Hello {
pname = "hello"
version = "1.0"
recipe = "std.shell"
recipeArgs = {
command = "mkdir -p $OUTPUT/bin && echo '#!/bin/sh\necho hi from punix' > $OUTPUT/bin/hi && chmod +x $OUTPUT/bin/hi"
}
source = { type = "local" path = "./src" }
}
What each field does:
pname+version: the human-readable name/version. They appear in the store path but do not participate in the hash directly (the canonical derivation does).recipe = "std.shell": the recipe to use.std.shellruns therecipeArgs.commandas a POSIX shell script in a sandbox, with$OUTPUTset to a temp dir that becomes the store path on success.source: where the build's input comes from.localis the simplest; other kinds areurl,git,url_per_arch,fixed_output.
Type-check first¶
Always: check before build. The type-checker catches every config error with file:line:col locations before any byte hits the store.
Expected:
If you mistype a field — e.g. recpie = "std.shell" instead of recipe — you get a located [E#]:
Realise¶
Expected:
→ building 1 package(s) into /Users/you/.punix/store
per-package build logs: /Users/you/.punix/punix-build-logs/<module>.log
[1/1] Hello
[1] realise Hello
ok Hello /Users/you/.punix/store/a1b2c3d4…-hello-1.0
1 packages: 1 ok, 0 failed
The store path is content-addressed: same recipe + same inputs ⇒ same hash ⇒ same path. Run build again:
The realise reporter logs cache hit instead of realise. The store path is identical because nothing about the derivation changed.
Inspect the store¶
Prints the predicted store path without building (uses RealiseDryRun). Lets you wire scripts that need the path without paying the build cost.
bin/hi: what the recipe wrote.built: a marker that says "this store entry is complete." Realise writes it last; if it's missing, the entry is invalid (caught byStore.is_valid).derivation.json: the canonical derivation — every input that fed the hash. The "why is this path THIS hash" answer offline.provenance.json: per-resolved-binding, the chain of contributions that produced its value. Reads as a "blame" trace for the PCL composition — useful for "why is this build pulling that version of OpenSSL?"-class questions.
Try it:
cat /Users/you/.punix/store/*-hello-1.0/bin/hi
# #!/bin/sh
# echo hi from punix
/Users/you/.punix/store/*-hello-1.0/bin/hi
# hi from punix
Mutate something — see the cache work¶
Edit hello.pcl and change version = "1.0" to version = "1.1". Re-run:
A new store path appears (different hash). The old one is still there — content-addressed paths are immutable; "updating" really means "writing a new entry alongside."
Edit recipeArgs.command instead (change hi from punix to hello from punix). Same thing: new hash, new path. This is the recipe-identity hash property — the canonical derivation includes the recipe's identity (name + args + hook bodies), so editing a build script invalidates the cache for that package.
Why does that matter?¶
→ Concepts: Content addressing walks through why this property is load-bearing for everything else (GC, multi- arch coexistence, secret-hash-exclusion, atomic rollback).
→ Or move on to your first deploy, where you take this package and turn it into a running service.