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, not in the middle of an evaluator's recursion at minute 12 of a corpus build.
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 / 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 doesn't try to improve on that — it adopts the closure-hashed store wholesale and reuses prior-art recipe code below the seam.
What Punix tries to fix is the language above the seam:
- Typed, not duck-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, not Turing-complete-and-hope. Punix prohibits subtyping and first-class type members. That's not a simplification to revisit — it's 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. We don't 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), not reproducible-by-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 of the things we lifted.
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, not hash-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.
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 compose well: a Punix stack can include Docker-built services (the docker-compose backend lands in Stage 8) without giving up the typed config + atomic-rollback story for the rest of the deploy.
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. ↩