authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-10-12 20:34:17-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-10-12 20:34:17-07:00
log0c54ae3c0e0099dc8b382ea9fe9b80b37dbaee06
treef3d5a5aefab5b88d1fef398ab0bb700c78e0eb23
parent89ec7145f1adecce62c3d21c9d96b156b1520cb5

zig build: validate package hash consistency

Any time the same package (based on fingerprint) appears in the dependency tree, if the semantic version compares equal, the hash must be identical, otherwise one of three things has gone wrong and the maintainer must address it.

2 files changed, 83 insertions(+), 2 deletions(-)

lib/compiler/Maker.zig+77
...@@ -1397,6 +1397,8 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig {...@@ -1397,6 +1397,8 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig {
1397 process.exit(0);1397 process.exit(0);
1398 }1398 }
13991399
1400 try validateFingerprints(gpa, &job_queue);
1401
1400 // Create the dependencies.zig file for configurer to1402 // Create the dependencies.zig file for configurer to
1401 // obtain via `@import("@dependencies")`.1403 // obtain via `@import("@dependencies")`.
1402 {1404 {
...@@ -1714,6 +1716,81 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig {...@@ -1714,6 +1716,81 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig {
1714 };1716 };
1715}1717}
17161718
1719const ValidFingerprintKey = struct {
1720 project_id: Package.ProjectId,
1721 version: std.SemanticVersion,
1722
1723 const Context = struct {
1724 pub fn hash(this: @This(), a: ValidFingerprintKey) u32 {
1725 _ = this;
1726 var hasher: std.hash.Wyhash = .init(a.project_id.fingerprint_id);
1727 hasher.update(&a.project_id.padded_name);
1728 hasher.update(@ptrCast((&a.version.major)[0..1]));
1729 hasher.update(@ptrCast((&a.version.minor)[0..1]));
1730 hasher.update(@ptrCast((&a.version.patch)[0..1]));
1731 if (a.version.pre) |pre| hasher.update(pre);
1732 return @truncate(hasher.final());
1733 }
1734 pub fn eql(this: @This(), a: ValidFingerprintKey, b: ValidFingerprintKey, i: usize) bool {
1735 _ = this;
1736 _ = i;
1737 return a.project_id.eql(&b.project_id) and a.version.order(b.version) == .eq;
1738 }
1739 };
1740};
1741
1742const ValidFingerprintTable = std.array_hash_map.Custom(
1743 ValidFingerprintKey,
1744 struct { fetch: *Fetch, pkg_hash: Package.Hash, printed: bool },
1745 ValidFingerprintKey.Context,
1746 true,
1747);
1748
1749/// Ensure that the dependency tree did not have any fingerprint matches with the same semantic version but
1750/// differing package hashes.
1751fn validateFingerprints(gpa: Allocator, job_queue: *const Fetch.JobQueue) !void {
1752 var any_errors = false;
1753
1754 var table: ValidFingerprintTable = .empty;
1755 defer table.deinit(gpa);
1756
1757 for (job_queue.table.keys(), job_queue.table.values()) |pkg_hash, fetch| {
1758 if (!fetch.have_manifest) continue;
1759 const gop = try table.getOrPut(gpa, .{
1760 .project_id = .init(fetch.manifest.name, fetch.manifest.id),
1761 .version = fetch.manifest.version,
1762 });
1763 if (gop.found_existing) {
1764 any_errors = true;
1765 const fingerprint: Package.Fingerprint = .init(fetch.manifest.id, fetch.manifest.name);
1766 const stripped_version: std.SemanticVersion = .{
1767 .major = fetch.manifest.version.major,
1768 .minor = fetch.manifest.version.minor,
1769 .patch = fetch.manifest.version.patch,
1770 .pre = fetch.manifest.version.pre,
1771 };
1772 if (!gop.value_ptr.printed) {
1773 gop.value_ptr.printed = true;
1774 log.err("contradictory hash for package with fingerprint 0x{x} and version {f}: {s}", .{
1775 fingerprint.int(), stripped_version, gop.value_ptr.pkg_hash.toSlice(),
1776 });
1777 }
1778 log.err("contradictory hash for package with fingerprint 0x{x} and version {f}: {s}", .{
1779 fingerprint.int(), stripped_version, pkg_hash.toSlice(),
1780 });
1781 } else {
1782 gop.value_ptr.* = .{ .fetch = fetch, .pkg_hash = pkg_hash, .printed = false };
1783 }
1784 }
1785 if (any_errors) {
1786 log.info("potential cause: package ownership dispute", .{});
1787 log.info("potential cause: maintainer neglected to bump version after making changes", .{});
1788 log.info("potential cause: dependency on unreleased changes", .{});
1789 log.info("hint: use --print-tree to visualize all package versions and hashes", .{});
1790 return error.FailedButCacheIntact;
1791 }
1792}
1793
1717fn printPkgTree(1794fn printPkgTree(
1718 terminal: Io.Terminal,1795 terminal: Io.Terminal,
1719 arena: Allocator,1796 arena: Allocator,
lib/compiler/Maker/Package.zig+6-2
...@@ -8,13 +8,17 @@ pub const Fingerprint = packed struct(u64) {...@@ -8,13 +8,17 @@ pub const Fingerprint = packed struct(u64) {
8 id: u32,8 id: u32,
9 checksum: u32,9 checksum: u32,
1010
11 pub fn generate(rng: std.Random, name: []const u8) Fingerprint {11 pub fn init(id: u32, name: []const u8) Fingerprint {
12 return .{12 return .{
13 .id = rng.intRangeLessThan(u32, 1, 0xffffffff),13 .id = id,
14 .checksum = std.hash.Crc32.hash(name),14 .checksum = std.hash.Crc32.hash(name),
15 };15 };
16 }16 }
1717
18 pub fn generate(rng: std.Random, name: []const u8) Fingerprint {
19 return .init(rng.intRangeLessThan(u32, 1, 0xffffffff), name);
20 }
21
18 pub fn validate(n: Fingerprint, name: []const u8) bool {22 pub fn validate(n: Fingerprint, name: []const u8) bool {
19 switch (n.id) {23 switch (n.id) {
20 0x00000000, 0xffffffff => return false,24 0x00000000, 0xffffffff => return false,