Reproducible archives
Packing the same directory twice produces byte-identical archives:
$ zpack pack assets/ a.zpak$ zpack pack assets/ b.zpak$ cmp a.zpak b.zpak # no output: identicalThat holds across machines, across platforms, and across runs. It is not a best-effort property - it falls out of four deliberate choices.
What makes it hold
Section titled “What makes it hold”Entries are sorted
Section titled “Entries are sorted”Directory walk order is undefined and varies by filesystem, by inode allocation,
and by how the tree was created. pack sorts paths byte-wise before writing the
index, so walk order never reaches the output.
Sorting also gives unpack its contiguous-parent optimisation:
files sharing a directory are adjacent, so the same parent is never created
twice.
Paths are normalized
Section titled “Paths are normalized”Paths are stored /-separated regardless of host separator, so an archive
packed on Windows is byte-identical to one packed on Linux from the same tree.
No timestamps or metadata
Section titled “No timestamps or metadata”Nothing time-varying is recorded. There are no mtimes, no permissions, no “created by” field, and no archive-level timestamp. The format has nowhere to put one - see the v1 layout.
Compression is deterministic
Section titled “Compression is deterministic”Deflate at level 6 with a fixed window produces the same bytes for the same input, every time. The store-versus-deflate decision is a pure comparison of two sizes, so it too is a function of the input alone.
What would break it
Section titled “What would break it”| Change | Effect |
|---|---|
| A different zpack build with a different deflate implementation | Compressed bytes could differ; the contents still round-trip identically |
| Adding, removing, or renaming a file | Changes the index size, which moves every offset |
| Editing a file | Changes its size, hash, and possibly its method |
A different .zpackignore |
Changes the set of entries |
| Packing a different directory whose contents happen to match | Identical, in fact - only contents and paths matter |
The first row is the one to be aware of: reproducibility is guaranteed for a given zpack binary. Two different zpack versions could in principle produce different compressed bytes for the same input while both round-trip perfectly. Pin the version if bit-identical output across time matters to you.
Checking it
Section titled “Checking it”With cmp
Section titled “With cmp”zpack pack assets/ a.zpakzpack pack assets/ b.zpakcmp a.zpak b.zpak && echo reproducibleWith a checksum in CI
Section titled “With a checksum in CI”zpack pack assets/ game.zpaksha256sum game.zpakCommit the expected digest and compare. A change means the assets changed - which is exactly what you want a build to tell you.
With a manifest
Section titled “With a manifest”More informative than a digest, because it says what changed:
zpack manifest game.zpak > /tmp/manifest.zondiff manifest.zon /tmp/manifest.zonA changed hash with an unchanged size means contents changed. A new block
means an asset was added. See manifests.
Packing into the directory you are packing
Section titled “Packing into the directory you are packing”zpack pack . out.zpak is repeatable, which is less obvious than it sounds. Two
mechanisms make it work:
- A file whose relative path equals the output path is skipped during the scan,
so the previous
out.zpakis not folded into the new one. - The temporary file
packbuilds into is created after the scan, so it cannot appear in its own archive.
Without the first, each run would embed the last run’s archive and grow forever. Without the second, the output would contain a partial copy of itself.
Atomic replacement
Section titled “Atomic replacement”pack builds into a temporary file beside the destination and moves it into
place only once complete. A run that fails partway leaves any existing archive
untouched rather than replacing it with a truncated one.
Combined with reproducibility, that means a failed build never leaves a plausible but wrong archive behind - you either get the correct bytes or the previous ones.
Why it matters
Section titled “Why it matters”Caching. A build system can skip work when the inputs are unchanged, because unchanged inputs provably produce an unchanged archive. See build integration.
Patching. A binary diff between two releases is proportional to what actually changed, not to how the filesystem happened to enumerate directories that day.
Review. An unintended asset change shows up as a manifest diff rather than as an opaque binary blob nobody can review.
Verification. Anyone can rebuild the archive from the source tree and check that it matches what shipped - provided they use the same zpack version.