Skip to content

Content addressing

Every artifact Punix produces lives at a path that is a deterministic function of its inputs. Same recipe, same arguments, same source bytes, same target system, same dependencies ⇒ same SHA-256 ⇒ same store path. Change any of those ⇒ different path, different artifact, both coexist.

This page is what content-addressing buys you, operationally.

The store layout

~/.punix/store/
├── a1b2…e9f0-curl-8.5.0/         # one artifact, one path
│   ├── bin/, lib/, share/, ...
│   ├── built                      ← marker; if missing, the entry is invalid
│   ├── derivation.json            ← the canonical inputs for this artifact
│   └── provenance.json            ← which PCL contributions produced it
├── c3d4…7e8f-curl-8.5.0/         # different inputs → different hash
│   ├── ...
└── ...

The hash prefix is computed from a structured record of every input: the recipe identity, recipe arguments, source bytes (or source hash for fetched sources), target system, and dependencies. The pname- version suffix is human-readable bait — it doesn't participate in the hash, so two recipes that happen to share a name and version but differ in content still get distinct paths.

What's in the hash

Input Why
Recipe identity (name + content-hash of the recipe code) Editing a build script invalidates the cache for every dependent.
Recipe arguments configureFlags = ["--without-ssl"] is part of the package's identity.
Source bytes or source hash A different upstream tarball is a different artifact.
Target system (x86_64-linux, aarch64-linux, …) The same source on different architectures yields different paths; they coexist.
Dependencies (their store paths) A dependency change propagates into the dependent's hash.

What's not in the hash

Excluded Why it matters
Secret values ({from_env=…} / {from_file=…}) Two deploys with different secret values produce byte-identical store paths. Secrets never reach the store, the manifest, or the build cache.
pname / version suffix in the path They're for humans, not for cache keys.
The store root location (~/.punix/store vs /var/store) Same artifact hashes identically regardless of where the store lives. Bit-for-bit promotion between machines just works.
Wall-clock time / build timestamp The build is deterministic by construction.

What this buys you, operationally

Garbage collection without anxiety

punix store gc walks the store, computes the keep-set as the union of every generation's pinned paths, and removes anything else. The keep-set is computed fresh every pass; a path pinned by any generation is never collected, even if no current process references it. There is no "I ran GC and now my rollback target is gone" failure mode.

Rebuilds on edit, no flag needed

The bad old days: "did I make clean after editing the build script?" The Punix answer: there's nothing to clean. Edit the script ⇒ recipe identity changes ⇒ hash changes ⇒ new path. The old artifact stays in the store (until GC), reachable by name, but no new derivation references it.

Promotion without verification

Build a closure on your dev machine. Push the closure to prod. The prod machine asks "do I have this hash?" — if yes, the bytes are already correct by construction; no per-file checksum needed. This is exactly how Punix's SSH transport's closure-push step works.

If the same store path resolves on two machines, the bytes at that path are identical — same hash means same content, by the hash function. No "but did the build actually do the same thing on both machines?" question.

Multi-version coexistence — no precedence rules

module Ncurses5 { pname = "ncurses"  version = "5.9"  … }
module Ncurses6 { pname = "ncurses"  version = "6.4"  … }
module Rogue    { pname = "rogue"    … deps = [Ncurses5]  }
module Vim      { pname = "vim"      … deps = [Ncurses6]  }

Distinct hashes, distinct paths, both live in the store. Rogue and Vim pin different ncurses paths — and they coexist on the same machine without any "which version is installed" decision. Operators don't need to know about library version coordination at all.

CVE-pin propagation that's precise and auditable

Force Openssl 3.0.0 to 3.0.1 somewhere in your PCL:

  • Every package that transitively depends on OpenSSL gets a new hash (different inputs ⇒ different canonical derivation ⇒ different store path).
  • Every package that does NOT depend on OpenSSL is byte-identical.
  • punix why traces exactly which packages picked up the new version and why.

This isn't a CVE-management feature bolted on top; it's a direct consequence of content-addressing.

Refactor PCL safely

Reorder modules in a file. Rename a binding. Split one file into many. Byte-identical store paths. The canonical derivation is order-independent, sorted by key, and structurally insensitive to how you wrote the source.

This is verified end-to-end by a conformance test: a program and a fully-permuted variant must yield byte-identical store paths. Operationally: PR reviewers can ask "did this change the build?" by checking whether punix store path for any module changed. If yes, the change has semantic consequences; if no, it doesn't.

Walking the property yourself

uv run punix store path stack.pcl Hello
# → /…/store/<hash1>-hello-1.0

Edit hello.pcl: change recipeArgs.command. Re-run:

uv run punix store path stack.pcl Hello
# → /…/store/<hash2>-hello-1.0    ← different hash, different path

Edit hello.pcl: reorder modules, rename internal bindings — anything that doesn't change the canonical derivation:

uv run punix store path stack.pcl Hello
# → /…/store/<hash1>-hello-1.0    ← still hash1

Try it on your own PCL. The hash answers "did this change matter to the build?" without your having to run the build.