Three pillars¶
Punix is built on three co-equal properties. Each is held by construction: a structural feature of the architecture. No operator discipline can replicate this guarantee. Each has a dedicated test suite that ships as a CI release-blocker.
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. Content-addressing makes "did the build pick up a different system library this time?" a question with exactly one answer.
- Edit a recipe, get a rebuild. The hash includes the recipe's identity; editing a build script invalidates the cache for every package that uses it, without a
make cleanstep. 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, so the question "which version is installed?" never arises. - 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: while writing the manifest, writing config files, or 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: a single rename. The size of the dependency closure and the duration of the original deploy are irrelevant. 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. Local fs, remote host via SSH, future backends like Docker Compose or launchd: where the deploy lands 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. Every connection verifies the host key. - 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, and the GC contract stay as they are. Punix ships
systemd; the others land when there is a driver.
→ Transport and deploy targets: how this works in practice.
Why three? Why these three?¶
Punix is defined by the combination of 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, and reverting requires reasoning backward through whatever changed, without the atomic pointer that makes rollback constant-time.
The three together 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.