Why Punix¶
How Punix relates to the tools you already know; Nix, Guix, SlapOS, Homebrew, Ansible, Docker Compose; and what it deliberately leaves on the table.
The one-paragraph version¶
Punix v2 is a typed package manager + service deployer for macOS and Linux. The language above the eval/realise seam is small and decidable (no subtyping, no first-class type members, no removal); the host-bound effects below it (build, transport, service lifecycle) are conventional, mostly reused from prior-art tools. Composition is principled: exactly one merge rule, proven correct. The pure config layer terminates by construction.
That bottom-line lets the type checker do real work; every error surfaces with file:line:col before any build runs.
Feature matrix¶
| Punix v2 | Nix | Guix | SlapOS | Ansible | Docker Compose | |
|---|---|---|---|---|---|---|
| Typed config | ✅ caught at type-check, before any build | ❌ runtime | ❌ runtime (Scheme) | ❌ runtime (Buildout .cfg) | ❌ runtime | ❌ runtime |
| Order-independent source | ✅ proven | ✅ (mostly) | ✅ (mostly) | ❌ | ❌ | ❌ |
| Content-addressed store | ✅ closure hash | ✅ closure hash | ✅ closure hash | partial1 | ❌ | ❌ |
| Atomic deploy + rollback | ✅ one-step, constant-time | ✅ (NixOS only) | ✅ (Guix System / Home) | partial (instance reprovision)2 | ❌ | partial |
| Secrets out of hash | ✅ proven by construction | partial | partial | n/a | n/a | n/a |
| Multi-backend native | ✅ Transport seam; systemd ships, the seam takes more | systemd / launchd | shepherd | ✅ multi-instance, slap-host | yes | docker-only |
| Codebase size | ~15k lines Python | ~80k lines C++/Nix | ~tens of thousands of Scheme + C | ~100k+ lines Python/JS | ~200k lines | ~50k lines Go |
vs Nix¶
Nix solved reproducible builds. Punix adopts the closure-hashed store wholesale and reuses prior-art recipe code below the seam.
Punix fixes the language above the seam:
- Statically typed. Nix's expression language is dynamic Lisp-with-laziness; many errors only fire when a derivation actually evaluates, often deep in a build. Punix's PCL is checked end-to-end with
file:line:colerrors before any build runs. - Decidable. Punix prohibits subtyping and first-class type members. This is the decidability guarantee.
- One merge rule, proven correct. Composition is a single
Resolveoperation with a stated theorem. Nix'smkMerge/mkForce/mkDefault/mkOverrideinteraction surface is much larger and not formally specified.
If you've been put off by Nix's learning curve, Punix may feel familiar (it's typed Python in shape) while giving you the same content-addressed-store + atomic-rollback core.
vs Guix¶
Guix is closest to Punix's spirit: a clean Scheme above the store, with a service framework (Shepherd) integrated. The major differences:
- Scheme vs. typed-Python-shaped language. Guix's Scheme is more powerful as a meta-language but un-typed in the static sense. PCL's surface is smaller and the error story is type-checked.
- System scope. Guix System is a full Linux distribution; Punix targets per-user installs + service stacks. Punix does not replace your OS.
- macOS first-class. Guix is Linux-only. Punix runs on macOS (aarch64 verified on every commit) and Linux (planned).
vs SlapOS¶
SlapOS occupies a different niche: multi-tenant, multi-node, instance-orchestration-first. The buildout-based software-release model is reproducible by pinning (versions + patches in software.cfg), whereas Punix and Nix derive reproducibility from the closure hash. See the SlapOS footnote for detail.
Punix's Transport seam was designed after studying SlapOS's slap-host abstraction; the multi-backend pattern is one adopted from SlapOS.
vs Homebrew¶
Brew is great at installing tools. It's not trying to be a service deployer, and it doesn't pretend to have a content-addressed store (the Cellar layout is path-based; a brew update can change /opt/homebrew/Cellar/foo/1.2.3/ underneath you).
The decision tree is small:
- You want to install CLIs on your Mac and forget about it. Use brew.
- You also want reproducible builds, atomic rollback, declarative project setup, or service deploys. Punix.
Punix can coexist with brew. Many Punix recipes were translated from brew formulae (tools/migrate-brew.py), and the variadic punix install matches brew install's ergonomics.
vs Ansible¶
Ansible is a remote-orchestration tool with no content-addressed store and no typed config. The two are complementary:
- Use Ansible when you need to orchestrate stateful changes on many existing hosts (rolling restarts, configuration drift, credential rotation, mixed-target inventories).
- Use Punix when the target itself can be content-addressed and replaced atomically (build it, hash it, flip the symlink, done).
A typical pattern: Ansible bootstraps the box and lays down system packages; Punix manages the application stack and its rollback boundary.
A config-only deploy needs nothing on the target but sshd and rsync (no Punix, no agent, no daemon) and still gets generations, byte-exact rollback and drift detection. See what the target needs.
vs Docker / Docker Compose¶
Docker's images are content-addressed-ish (layer-hashed), but the application surface (compose files, environment variables, restart policies) is YAML with no type checking, and rollback is a manual docker compose down && docker compose up -d with the previous image tags. Multi-host requires Swarm / k8s; the local Compose model doesn't extend.
Punix and Docker could compose well: a Punix stack including Docker-run services would keep the typed config + atomic-rollback story for the rest of the deploy. A docker-compose backend is not implemented. The backend seam is pluggable (a pure renderer plus an effectful activator) and adding one is a registration rather than a fork, but nobody has.
Reading order from here¶
- Eval/realise seam: the architectural boundary the whole system is organised around.
- Three pillars: reproducible builds + atomic updates + multi-backend, told as one story.
- Content addressing: how the store works.
- Generations and rollback: the D9 atomic-symlink-switch invariant in detail.
-
SlapOS uses
zc.buildoutto build software. A Software Release is identified by a git URL pointing to a buildout profile (e.g.software.cfg); SlapGRID compiles it on the node into/opt/slapgrid/<md5-of-url>/. So the install dir is URL-addressed, not content-addressed by closure hash. What IS content-addressed: (1) Buildout's download cache, keyed by MD5 of file contents (shared across software releases on a node), and (2) Nexedi's shacache (shared prebuilt-binary cache): a node can sometimes retrieve a prebuilt binary by hash and skip the compile. The substrate of installed software is not a Nix/Guix-style closure-hashed derivation graph; reproducibility is enforced by pinned versions + patches in the buildout profile, not by construction. ↩ -
SlapOS instances live in per-instance directories ("computer partitions"); the natural unit of recovery is "destroy and recreate the instance with a different Software Release," not a generation-style atomic flip. The instance lifecycle is deterministic and idempotent, but there's no single
atomic_symlink_switch-equivalent that makes the transition one-shot. Atomic in the strong "single syscall flips live state" sense; no. ↩