Three pillars¶
Punix is built on three co-equal properties. Each is held by construction (a structural feature of the architecture), not by convention (a discipline operators apply). Each has a dedicated test suite that ships as a CI release-blocker.
This page is the operator-facing summary of what those three properties mean for you in practice.
1. Reproducible builds¶
Same recipe + same inputs ⇒ same hash ⇒ same store path. Permuting source order, renaming modules, swapping declaration orders — all yield byte-identical store paths.
What this means operationally:
- Your CI can build the package; your prod server can verify it by hash. No "did the build pick up a different system library this time?" — content-addressing means there's only one answer.
- Edit a recipe, get a rebuild. No
make cleanstep. The hash includes the recipe's identity, so editing a build script invalidates the cache for every package that uses it. The artifact you ran yesterday and the one you built today are distinguishable. - Pin a CVE fix, get a precise rebuild. Bump
Openssl 3.0.0to3.0.1: only the packages that transitively depend on OpenSSL get a new hash. Every other package stays byte-identical. Your patch surface is minimal and auditable. - Multiple versions coexist without precedence rules. Two packages can depend on different
Ncursesversions; both live in the store with distinct hashes. No "which version is installed" question. - Same recipe on different architectures yields different store paths. A cross-arch closure can't accidentally collide with the local-arch one.
→ Content addressing — the property in detail, with the operational consequences.
2. Atomic updates with rollback¶
Every deploy produces a numbered generation. The live state changes in exactly one rename syscall on a single symlink. Kill the deploy mid-flight: the previous generation stays wholly live. Rollback is one command and constant-time.
What this means operationally:
- A crashed deploy is harmless. SIGKILL the deploy process at any point — between writing the manifest, between writing config files, between rsyncing closure chunks. The currently-running services keep running on the previous generation's binaries because nothing observable has changed yet.
- Rollback latency is bounded by one syscall. Not bounded by "how big is your dependency closure"; not bounded by "how long does the original deploy take to re-evaluate." One rename. Constant time.
- Rollback never re-evaluates or re-fetches. The previous generation's manifest carries every store path it pinned; rolling back walks that list and verifies it still exists, then flips the symlink. If the source PCL is gone, rollback still works.
punix store gcnever collects a path you might roll back to. The garbage collector reads every generation's manifest and unions their pinned paths into the keep-set; anything else is collectable.- Tooling can inspect a deploy after the fact. Each
gen-NNN.jsonis a complete record: which packages were pinned, which config files were written (and their hashes), the source PCL's hash, the deploy timestamp. Audit without re-running anything.
→ Generations and rollback — the manifest schema and the atomic flip in detail.
3. Multi-backend services, any host¶
Punix abstracts every deploy operation behind a uniform interface. "Where the deploy lands" — local fs, remote host via SSH, future backends like Docker Compose or launchd — is a single configuration choice; the rest of Punix is unchanged.
What this means operationally:
- Deploy to a remote host with one flag.
punix service deploy STACK --target ssh://user@hostruns the same code as a local deploy. The atomic-update guarantees hold over SSH. - Closure push is bandwidth-free where possible. Punix asks the target "do you already have this content-hashed path?" — if so, it skips. Re-deploying the same stack to multiple servers only transfers what's missing from each.
- Strict host-key verification stays on. The SSH transport passes
StrictHostKeyChecking=yes; you can pin a per- environmentknown_hostsfile via--known-hosts. There is no "accept any host key" mode. - Hermetic testing is trivial. Internal Punix tests rebase the filesystem under a sandbox directory and run the same deploy code paths — no
subprocessmocks involved. If you write your own deploy-validating tests, the same primitive is available (--transport-root). - Adding a new deploy target is a small, contained change. Future backends (launchd, supervisord, docker-compose, k8s) slot into the same abstraction; the manifest format, the rollback flow, the GC contract — none of those change. Punix v0.6 ships
systemd; Stage 8 adds the others.
→ Transport and deploy targets — how this works in practice.
Why three? Why these three?¶
You can ship one without the other two, but you can't claim Punix without all three.
- Reproducible builds alone is Nix. Useful, hard to integrate into services on non-NixOS hosts.
- Atomic updates alone is "blue-green deploy" — flip a load balancer between two pre-baked stacks. Works, but every package upgrade is a separate operational concern.
- Multi-backend alone is Ansible. The deploy is a script; the next run can drift; reverting requires reasoning, not flipping a pointer.
The three together are what make a system: the bytes that deploy are the bytes that built; the bytes that built are the bytes that the type checker proved consistent; the bytes that the type checker proved consistent are the bytes you wrote.