Skip to content

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 clean step. 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.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. 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 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. "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@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. 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 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, 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.