Manifests
A manifest is the archive’s index as ZON: everything the archive describes, without any of its contents.
zpack manifest game.zpak > manifest.zonIt is derived from the archive when the command runs, not written alongside it at pack time. There is no second file to forget to regenerate, and no way for the two to disagree.
The shape
Section titled “The shape”.{ .format_version = 1, .archive_bytes = 12828, .entry_count = 5, .total_bytes = 13309, .stored_bytes = 12565, .entries = .{ .{ .path = "audio/blip.wav", .id = 0x40a8e93753577eb5, .size = 2444, .stored_size = 2370, .method = .deflate, .hash = 0xfe514dafacec030e, }, .{ .path = "data/noise.bin", .id = 0xbc5d025c01217e9b, .size = 4096, .stored_size = 4096, .method = .store, .hash = 0x8ca43708f0ada27c, }, },}Sizes are exact byte counts, not the rounded units list prints.
Entries appear in sorted path order, so two manifests of the same tree diff
cleanly - see the field reference.
Reading it back
Section titled “Reading it back”ZON is Zig’s own literal syntax, so the standard library parses it with no dependency and no hand-written parser:
const Manifest = struct { format_version: u32, archive_bytes: u64, entry_count: u32, total_bytes: u64, stored_bytes: u64, entries: []const Entry,
const Entry = struct { path: []const u8, id: u64, size: u64, stored_size: u64, method: enum { store, deflate }, hash: u64, };};
const source = try dir.readFileAllocOptions(io, "manifest.zon", gpa, .limited(1 << 24), .of(u8), 0);defer gpa.free(source);
const manifest = try std.zon.parse.fromSliceAlloc(Manifest, gpa, source, null, .{});defer std.zon.parse.free(gpa, manifest);The struct is yours - leave out fields you do not care about and ZON parsing ignores them.
What to use it for
Section titled “What to use it for”Enforce a size budget
Section titled “Enforce a size budget”The failure mode this catches is an artist checking in a 400 MB uncompressed texture nobody notices until a release build.
const budget = 64 * 1024 * 1024;if (manifest.archive_bytes > budget) { std.log.err("archive is {d} bytes, budget is {d}", .{ manifest.archive_bytes, budget }); return error.OverBudget;}
// Or per asset.for (manifest.entries) |entry| { if (entry.size > 8 * 1024 * 1024) { std.log.err("{s} is {d} bytes", .{ entry.path, entry.size }); return error.AssetTooLarge; }}Assert an asset exists
Section titled “Assert an asset exists”Cheaper than opening the archive, and it runs anywhere the manifest is checked in:
const required = [_][]const u8{ "textures/player.png", "audio/theme.ogg", "levels/level-01.json",};
outer: for (required) |want| { for (manifest.entries) |entry| { if (std.mem.eql(u8, entry.path, want)) continue :outer; } std.log.err("missing required asset: {s}", .{want}); return error.MissingAsset;}Diff two builds
Section titled “Diff two builds”Because entries are sorted and sizes are exact, diff does the work:
zpack manifest old.zpak > old.zonzpack manifest new.zpak > new.zondiff old.zon new.zon.size = 153216,.hash = 0x1e4c43f38fdf17b6,.size = 401992,.hash = 0x9a3f0e2b7c118d44,A changed hash with an unchanged size means the contents changed. A changed
method means a file crossed the compress-or-store threshold. A new block means
an asset was added.
Track compression effectiveness
Section titled “Track compression effectiveness”const saved = manifest.total_bytes - manifest.stored_bytes;const percent = 100.0 * @as(f64, @floatFromInt(saved)) / @as(f64, @floatFromInt(manifest.total_bytes));std.log.info("compression saved {d:.1}%", .{percent});If that number drops sharply, something incompressible got added - usually a
video or an already-compressed archive that would be better handled outside the
.zpak.
Check in a manifest as a lockfile
Section titled “Check in a manifest as a lockfile”Commit manifest.zon, then fail CI when a build disagrees with it:
zpack manifest game.zpak > /tmp/manifest.zondiff manifest.zon /tmp/manifest.zon || { echo "assets changed; commit the new manifest" >&2 exit 1}Because archives are reproducible, that diff is empty unless the assets genuinely changed. It makes an unintended asset change reviewable, in the diff, rather than invisible in a binary.
Without the CLI
Section titled “Without the CLI”Archive.writeManifest takes any std.Io.Writer, so a build.zig step or a
tool can produce a manifest without invoking the binary:
var archive = try zpack.Archive.open(gpa, io, .cwd(), "game.zpak");defer archive.deinit();
const file = try Dir.cwd().createFile(io, "manifest.zon", .{});defer file.close(io);
var buffer: [4096]u8 = undefined;var writer = file.writer(io, &buffer);try archive.writeManifest(&writer.interface);try writer.interface.flush();Only the index is read, so this is as cheap as list. See
build integration for wiring it into a build step.
The manifest is not the archive format
Section titled “The manifest is not the archive format”format_version describes the archive. The manifest itself is a reporting
format, and its shape may gain fields between zpack releases without the archive
format moving. Parse the fields you need and let ZON ignore the rest.