What works¶
This page covers the engine, store, profile, services, and deploy machinery. For the package corpus (what you can actually punix install <name>), see Corpus status — that's the page that lists every recipe by topic and what builds today.
1224 tests green across Python 3.13 + 3.14 on x86_64 + aarch64 Linux. Every major property below has a dedicated test under tests/c_e2e/, and any regression is a CI release-blocker.
Package corpus at a glance¶
- 365 recipes ship green today, organised by topic in Corpus → working. Every one builds from source under
make dogfoodagainst the Punix store — no Homebrew bridge under the hood, no prebuilt binary toolchain, no DYLD shims at runtime. - Coverage against Homebrew's top-500 install-on-request analytics: 62 % (310 / 500 formulae fully resolvable through the corpus; the rest are tracked in Corpus → blockers with explicit failure classes).
- The corpus is regenerated weekly. A single command (
uv run python tools/status-report.py) refreshes the docs frompackages/official/.
This page is a stage-by-stage rollup of what shipped at the engine level, in plain language. For the error-code numbers and design-decision identifiers, see Reference: error codes, Reference: conformance, and Reference: decisions.
Stage 0 — Foundation ✅¶
- Repo bootstrap:
uvpackage manager,rufflint + format,ty/pyrefly/mypytypecheckers,noxmatrix across Python 3.13 / 3.14. - The IR core (a small inheritance-calculus evaluator) is pulled in as a pinned git source — no vendored copy in the repo.
- Five foundational design decisions frozen: where the store lives, no subtyping / first-class type members in the language, no
importkeyword (whole-tree composition), and recipe identity in the canonical hash.
Stage 1 — Typed configuration language ✅¶
punix check FILEruns the type checker over a.pclfile or directory. Every error comes back with file:line:col locations + a short code. The pass is single-shot and provably terminating.- Hand-written parser; structural type checker. ~30 unit tests.
- All six type-system error categories shipped with minimal-program examples that produce each error.
→ CLI: punix check · Reference: error codes
Stage 2 — Order-independent evaluation + provenance ✅¶
- IR lowering: every secret reference flows through the IR as a reference string, never a value — the property that makes secrets stay out of the build hash.
- Canonical derivation: keys sorted, recipe identity included.
- Evaluator handles conflicting contributions with explicit priority arbitration; same-priority conflicts surface as a located error.
punix why FILE Module.field: prints a resolved value along with every contribution that produced it, with locations.punix store path FILE Module: predicts a build's content- addressed store path without doing the build.- Conformance tests prove: permuting source ⇒ byte-identical store paths; a pinned version change propagates exactly to its dependents; a secret value never affects a store path.
→ Concepts: content addressing · CLI: punix why
Stage 3 — Real builds ✅¶
- Realise adapter: sandboxed builds with the seven shipped recipes (
std.shell,std.autotools,std.cmake,std.make,std.go,std.python_wheel,std.binary). - Source kinds:
local,url,git,mirror://,fixed_output(network-permitted with hash verification),url_per_arch(multi- arch binary sources). - Pre-pass error class for build-closure problems (cycles, missing recipe on a dep'd module) — surfaces before any sandbox runs.
- Source-hash mismatch, fixed-output mismatch, and missing-per-arch errors all surface as located, coded errors.
- The cache-staleness bug is gone: editing a recipe's
.py(or a sibling_postinstall.sh) actually invalidates the cache for every package using that recipe. - Conformance tests prove: a real autotools package builds and re-builds as a store hit; two versions of
Ncursescoexist with distinct paths; editing a recipe hook body changes the store path.
→ Language: recipes · CLI: punix build
Stage 4 — Atomic deploys + multi-backend services ✅¶
- A
Transportabstraction with two implementations:LocalTransport(root)for local deploys (and hermetic tests by pointingrootat a sandbox dir), andSshTransport(host, user, port, key)for remote deploys (wired but only exercised by argv-construction tests in this stage). - Numbered generation manifests on disk: every deploy writes a
gen-NNN.jsoncontaining every store path pinned, every config file's hash, the source bytes' hash, and the backend. - Atomic flip: the live state changes in exactly one rename syscall on the
currentsymlink. Crash earlier ⇒ previous generation stays wholly live. - Garbage collection respects every generation's pins: a path referenced by any live or rollback-reachable deploy never gets collected.
compose_stack: read a stack PCL module into aServiceStackthe backend generator can render.- systemd backend: one
.serviceunit per service + any stack- declared config files. punix service deploy STACK --file PCL [--dry-run]andpunix service rollback STACKshipped.--transport-root PATHrebases the entire filesystem surface under a directory — turns the same code into a hermetic test fixture (no subprocess mocks).- Conformance tests prove the five sub-properties of atomic updates: kill mid-deploy ⇒ previous gen lives; rollback restores the previous gen exactly; GC respects pins; rollback after GC works; pinned-path-missing surfaces a clear error.
→ Concepts: generations and rollback · CLI: punix service
Stage 5 — SSH transport live ✅¶
--target ssh://user@host[:port]+--key+--known-hostsonservice deployandservice rollback.- Closure push step: before flipping
current, walks every store path in the stack andrsyncs anything the target doesn't already have. Content-addressed = bandwidth-free: existence on the target IS correctness (same hash-name ⇒ same bytes by construction). Summary line reportsN pushed/M cached. - A pytest fixture spins up a real
sshdon a free localhost port with a fresh keypair and a pre-populatedknown_hostsfile — so SSH tests exercise the actual ssh/rsync code path and stay hermetic. Auto-skips when sshd / ssh-keygen aren't available. - Strict host-key checking stays on through tests via the fixture's
known_hosts— no pollution of the developer's~/.ssh/known_hosts. - Conformance tests prove the same atomic-update invariants hold over SSH (different syscall path, same property).
- A real portability bug was caught at this stage via the live fixture: the GNU-only
mv -Tflag fails on macOS BSDmv. Replaced with baremv(samerename(2)atomicity guarantee on POSIX).
→ Deploy over SSH · Concepts: transport and backends
Stage 6 — Hermeticity gap closed ✅¶
The release-evidence prerequisite. Three independent tracks:
Secrets at deploy. A service's environment can mix plain strings with {from_env="NAME"} or {from_file="path"} references. At deploy time the resolver:
- reads the actual values from the deploy host's environment or filesystem;
- writes them into the rendered systemd
Environment=lines (the one place the value reaches); - keeps the references (not the values) in the generation manifest;
- collects every missing reference into one clear error message rather than failing on the first.
Conformance tests prove that two deploys differing only in a secret value produce byte-identical source_hash + store_paths
+ services records; that three unset references all surface
together; and that the secret value never appears in the generation manifest.
Fixed-output verification. Recipes that produce content via a network-permitted command (e.g. fetching a tarball whose URL doesn't directly verify) declare an expected hash; mismatch surfaces a clear error with both the expected and observed hashes in the build log.
Multi-arch CI. The matrix runs on ubuntu-latest AND ubuntu-24.04-arm, both Python 3.13 + 3.14. Conformance tests prove same recipe on x86_64 vs aarch64 yields distinct store paths, and that a binary source missing per-arch entries surfaces a clear error naming both the missing target and the available ones.
→ Concepts: secrets · Deploy: secrets at deploy
punix install + per-user profile ✅¶
A top-level command lands binaries from any successful build onto a per-user $PATH, with generation-backed rollback:
$ punix install just # → gen-001 (just on PATH; FILE defaults to ./packages/)
$ punix install atuin # → gen-002 (just + atuin)
$ punix uninstall just # → gen-003 (atuin only)
$ punix profile list
gen-001 2026-05-27T… just
gen-002 2026-05-27T… atuin, just
* gen-003 2026-05-27T… atuin
active: gen-003
$ punix profile switch 2 # atomic flip back
switched to gen-002
$ just --version
just 1.51.0
Each generation lives at ~/.punix/profiles/gen-NNN/ (a directory with bin/<symlinks> + a profile.json Generation record). The active profile is a single symlink at ~/.punix/current → profiles/gen-N. Every install/uninstall writes a new generation and atomically flips that symlink via os.replace — the same D9 primitive used by punix service rollback. Crash before the flip returns: old generation stays active. After: new generation is active. No partial state.
Setup is one line in your shell rc: export PATH="$HOME/.punix/current/bin:$PATH".
→ CLI: punix install · uninstall · profile list/switch/diff
→ Concepts: Generations and rollback
Atomic-flip + generation history applies across both service-stack deploys AND user-profile management — the infrastructure cost is amortized.
Phase 5 — daily-driver UX ✅¶
Four read-only commands that turn the typed PCL tree into a brew-comparable inspection surface:
punix list— what's installed in the active profile (parses pname/version from the store path; supports--format json).punix search REGEX [FILE]— case-insensitive regex overpname+meta.descriptionacross every package module in the tree. ~150 ms over 100+ packages; no on-disk index.FILEis optional — Punix discovers the tree from$PUNIX_PACKAGESor./packages/.punix info MODULE [FILE]— recipe metadata (pname, version, recipe, deps, everymeta.*field) + predicted store path (RealiseDryRun) +installed: yes — gen-NNNif it's in the active profile.punix upgrade --check-all [FILE]— per-package upstream-drift report. For every module withmeta.upstream = { type, repo }, queries GitHub via theghCLI and reports<pname>: <current> → <latest>. Resolver errors are captured per-candidate (one flaky repo doesn't sink the report).v-prefix normalized in the compare. Read-only; PCL rewrite is deferred.
The brew translator auto-emits meta.upstream = { type = "github-release" repo = "X/Y" } when the source URL matches github.com/X/Y/releases/download/... (or github-tag for /archive/... URLs), so most translated recipes pick up upgrade-tracking for free.
→ CLI: punix list · search · info · upgrade
Test count over time¶
| Stage | Tests at end |
|---|---|
| 1 | ~30 |
| 2 | ~65 |
| 3 | ~120 |
| 4 | 248 |
| 5 | 272 |
| 6 | 302 |
6 + punix install v1 |
368 |
6 + punix install + profile v2 |
378 |
| 6 + Phase 5 (list/search/info/upgrade) | 400 |
6 + punix help + ergonomics + translator-improvements + corpus-restructure |
451 |
| 6 + translator patterns (npm/meson/cargo/go) + hard-reset audit | 473 |
| 6 + recipe-class expansion (std.ruby_gem, std.swift, std.python_venv) + git-tag sources + corpus scale-up | 555 |
| 6 + Stage 7 corpus arc (365 recipes, 62 % top-500 coverage) | 583 |
| + deploy pillar: generic engine, tangled/NLnet reproductions, hermetic builds, live demos, cross-host artifacts, path confinement | 1224 |
What you can do with v0.6¶
- Write typed configuration; type-check it; ship it with no surprises at runtime.
- Build any single-arch package or multi-package corpus into a content-addressed store. Edit recipes; rebuild correctly.
- Compose a service stack; deploy it locally (with root for the
/etc/systemd/system/writes) or to any SSH-reachable remote. - Roll back a deploy in one command, constant time.
- Use
{from_env}/{from_file}secrets that flow into the running service's environment and nowhere else — provably. punix install <module>/punix uninstall <module>/punix profile list / switch N / diff N M— manage a per-user profile of installed binaries on your$PATHwith brew-comparable UX. Every operation writes a new generation and atomically flips the active symlink (same D9 primitive as service-stack rollback). Constant-time rollback to any prior generation.punix list / search / info / upgrade --check-all— daily-driver inspection: enumerate the active profile, regex-search the PCL tree, inspect one module's metadata + predicted store path, or report per-package upstream drift via theghCLI.- Run the test suite (
make test) and see it green on x86_64 AND aarch64 Linux.
What's deliberately not in v0.6¶
→ Known limitations — the explicit gap list. → Roadmap — what's planned next.