From 98d3f26362f22aa91ace98b07249af07132e350b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 4 Sep 2026 19:09:38 -0700 Subject: [PATCH] add cache-cat subcommand for printing .zig-cache/h/* files to zon format --- lib/compiler/Maker.zig | 98 ++++++++++++++++++++++++++++++++++++++++- lib/std/Build/Cache.zig | 11 +++-- src/main.zig | 10 +++-- 3 files changed, 111 insertions(+), 8 deletions(-) diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig index 76fb47ac7546d620a2c9c7ba5253ee5c0aa84ff1..738b381762fd53482e241d9c3fd7125308890191 100644 --- a/lib/compiler/Maker.zig +++ b/lib/compiler/Maker.zig @@ -199,12 +199,13 @@ pub fn main(init: process.Init.Minimal) !void { .random_seed = parseRandomSeed(seed_arg), }; - const cmd = stringToEnum(enum { libc, init, fetch, build }, cmd_name) orelse + const cmd = stringToEnum(enum { libc, init, fetch, build, @"cache-cat" }, cmd_name) orelse fatal("bad command name: {q}", .{cmd_name}); switch (cmd) { .libc => return cmdLibC(gpa, &graph, args[arg_i..]), .init => return cmdInit(gpa, &graph, args[arg_i..]), .fetch => return cmdFetch(gpa, &graph, args[arg_i..]), + .@"cache-cat" => return cmdCacheCat(gpa, &graph, args[arg_i..]), .build => {}, } @@ -1880,6 +1881,101 @@ fn cmdFetch(gpa: Allocator, graph: *Graph, args: []const []const u8) !void { return process.cleanExit(io); } +fn cmdCacheCat(gpa: Allocator, graph: *Graph, args: []const []const u8) !void { + const io = graph.io; + + var arg_i: usize = 0; + var contents: std.ArrayList(u8) = .empty; + defer contents.deinit(gpa); + + while (nextArg(args, &arg_i)) |arg| { + if (mem.startsWith(u8, arg, "-")) { + if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) { + try Io.File.stdout().writeStreamingAll(io, usage_cache_cat); + return process.cleanExit(io); + } else { + fatal("unrecognized parameter: {q}", .{arg}); + } + } else { + var file = Dir.cwd().openFile(io, arg, .{}) catch |err| fatal("opening {q} failed: {t}", .{ arg, err }); + defer file.close(io); + + var manifest_reader = file.reader(io, &.{}); // Reads positionally from zero. + contents.clearRetainingCapacity(); + manifest_reader.interface.appendRemainingUnlimited(gpa, &contents) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.ReadFailed => switch (manifest_reader.err.?) { + error.Canceled => |e| return e, + else => |e| fatal("reading from {q} failed: {t}", .{ arg, e }), + }, + }; + const hex_digest = Dir.path.basename(arg); + cacheCatOne(hex_digest, contents.items, initStdoutWriter(io)) catch |err| switch (err) { + error.WriteFailed => fatal("writing to stdout failed: {t}", .{stdout_writer_allocation.err.?}), + else => |e| fatal("parsing {q} failed: {t}", .{ arg, e }), + }; + try stdout_writer_allocation.flush(); + } + } +} + +fn cacheCatOne(input_hex_digest: []const u8, contents: []const u8, writer: *Io.Writer) !void { + var bin_digest: Cache.BinDigest = undefined; + _ = try fmt.hexToBytes(&bin_digest, input_hex_digest); + + var hh: Cache.HashHelper = .{}; + hh.hasher.update(&bin_digest); + + var serializer: std.zon.Serializer = .{ .writer = writer }; + var top_level = try serializer.beginStruct(.{}); + try top_level.field("input_hash", input_hex_digest, .{}); + var files_tuple = try top_level.beginTupleField("files", .{}); + var off: usize = 0; + while (off + 1 < contents.len) { + const file_off: Cache.Manifest.File.Offset = @fromBackingInt(@intCast(off)); + const file = try file_off.getFallibleConst(contents); + const path = try Cache.Manifest.filePathFallible(contents, file_off); + if (path.len == 0) return error.InvalidFormat; + + var file_obj = try files_tuple.beginStructField(.{ .whitespace_style = .{ .wrap = false } }); + try file_obj.field("size", file.size, .{}); + try file_obj.field("inode", file.inode, .{}); + try file_obj.field("mtime", file.mtime, .{}); + const hex_digest = Cache.binToHex(file.digest); + try file_obj.field("digest", @as([]const u8, &hex_digest), .{}); + if (file.flags.is_directory) try file_obj.field("directory", true, .{}); + if (file.flags.metadata_only) try file_obj.field("metadata", true, .{}); + try file_obj.field("prefix", file.flags.prefix, .{}); + try file_obj.field("path", path, .{}); + try file_obj.end(); + + hh.hasher.update(&file.digest); + + off += Cache.Manifest.File.sizeOf(path.len); + } + + try files_tuple.end(); + + var discovered_bin_digest: Cache.BinDigest = undefined; + hh.hasher.final(&discovered_bin_digest); + const discovered_hex_digest = Cache.binToHex(discovered_bin_digest); + try top_level.field("discovered_hash", @as([]const u8, &discovered_hex_digest), .{}); + + try top_level.end(); + try writer.writeByte('\n'); +} + +const usage_cache_cat = + \\Usage: zig cache-cat + \\ + \\ Prints .zig-cache/h/* manifest files in text form. + \\ + \\Options: + \\ -h, --help Print this help and exit + \\ + \\ +; + const usage_fetch = \\Usage: zig fetch [options] \\Usage: zig fetch [options] diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index 16d2e3a1c5c46061592f95fb121b63f446270b76..ab03895f423fa0bfd5808b2209d5b0e23b2bb3c9 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -408,9 +408,14 @@ pub const Manifest = struct { } pub fn getFallible(offset: Offset, contents: []u8) error{InvalidFormat}!*File { + // TODO make @constCast support in-memory coercion across error unions and optionals + return @constCast(try getFallibleConst(offset, contents)); + } + + pub fn getFallibleConst(offset: Offset, contents: []const u8) error{InvalidFormat}!*const File { if (@backingInt(offset) + @sizeOf(File) >= contents.len) return error.InvalidFormat; if (!mem.isAligned(@backingInt(offset), @alignOf(File))) return error.InvalidFormat; - return get(offset, contents); + return getConst(offset, contents); } }; @@ -461,7 +466,7 @@ pub const Manifest = struct { } /// `path_len` does not include the null byte. - fn sizeOf(path_len: usize) usize { + pub fn sizeOf(path_len: usize) usize { const end = @offsetOf(File, "path_start") + path_len; const needed_alignment = @alignOf(File) - (end % @alignOf(File)); assert(needed_alignment >= 1); // Always need at least a null byte. @@ -1615,7 +1620,7 @@ pub const Manifest = struct { hasher.update(contents[hash_start..hash_end]); } - fn filePathFallible(contents: []const u8, off: File.Offset) error{InvalidFormat}![:0]const u8 { + pub fn filePathFallible(contents: []const u8, off: File.Offset) error{InvalidFormat}![:0]const u8 { const path_start = @backingInt(off) + @offsetOf(File, "path_start"); const path_end = mem.findScalarPos(u8, contents, path_start, 0) orelse return error.InvalidFormat; return contents[path_start..path_end :0]; diff --git a/src/main.zig b/src/main.zig index 46f81a0ba26b56251c1795626320ad8b542c3392..a846e2dd12b5725d134d2b8b15ed649a14468c28 100644 --- a/src/main.zig +++ b/src/main.zig @@ -107,6 +107,7 @@ const normal_usage = \\ \\ env Print lib path, std path, cache directory, and version \\ help Print this help and exit + \\ cache-cat Print a zig-cache manifest file as zon \\ std View standard library documentation in a browser \\ libc Display native libc paths file or validate one \\ targets List available compilation targets @@ -242,6 +243,10 @@ const Cmd = enum { ar, build, + @"cache-cat", + fetch, + init, + libc, clang, @"-cc1", @@ -258,10 +263,7 @@ const Cmd = enum { fmt, objcopy, objdump, - fetch, - libc, std, - init, targets, version, env, @@ -351,7 +353,7 @@ fn mainArgs( dev.check(.ar_command); return process.exit(try llvmArMain(arena, args)); }, - .build, .fetch, .init, .libc => { + .build, .fetch, .init, .libc, .@"cache-cat" => { return jitCmd(gpa, arena, io, cmd_args, environ_map, .{ .cmd_name = "maker", .root_src_path = "Maker.zig", -- 2.54.0