Skip to content

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 clean step. 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.0 to 3.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 Ncurses versions; 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 gc never 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.json is 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@host runs 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-environment known_hosts file 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 subprocess mocks 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.