zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit 1c6d19b0ff6c816cf9f73507e7e4ff35b19a1e1d (tree)
parent ab46b8235450cd1d8cb1a483859c2265588a1863
Author: kcbanner <kcbanner@gmail.com>
Date:   Fri,  5 Jun 2026 01:55:36 -0400

tests: more work on the linker snapshot testing framework

build: add the ability to test the output of a Run step against a snapshot
objdump:  add --redact, --omit-element, --snapshot, and --only-symbol

Diffstat:
Mlib/compiler/Maker/Step/Run.zig | 49+++++++++++++++++++++++++++++++++++++++++++++++--
Mlib/compiler/configurer.zig | 8++++++++
Mlib/compiler/objdump.zig | 745+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mlib/std/Build/Configuration.zig | 6+++++-
Mlib/std/Build/Step/Run.zig | 9+++++++++
Mtest/link.zig | 35++++++++++++++++++++++++-----------
Atest/link/exports.zig | 9+++++++++
Atest/link/snapshots/exports-dynamic.lib.dmp | 14++++++++++++++
Atest/link/snapshots/exports-static.lib.dmp | 3+++
Mtest/src/Link.zig | 67+++++++++++++++++++++++++++++++++++++++++++------------------------
Mtest/tests.zig | 33++++++++++++++++++++++++---------
11 files changed, 634 insertions(+), 344 deletions(-)

diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig @@ -2100,6 +2100,47 @@ fn runCommand( }); } } + const snapshots: []const ?struct { + path: Cache.Path, + result: enum { stderr, stdout }, + } = &.{ + if (conf_run.expect_stderr_snapshot.value) |path| .{ + .path = try maker.resolveLazyPathIndex(arena, path, run_index), + .result = .stderr, + } else null, + if (conf_run.expect_stdout_snapshot.value) |path| .{ + .path = try maker.resolveLazyPathIndex(arena, path, run_index), + .result = .stdout, + } else null, + }; + for (snapshots) |opt_snapshot| { + const snapshot = opt_snapshot orelse continue; + + const file = snapshot.path.root_dir.handle.openFile(io, snapshot.path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open snapshot file {f}: {t}", .{ snapshot.path, err }); + defer file.close(io); + + var file_reader = file.reader(io, &.{}); + const snapshot_contents = file_reader.interface.allocRemaining(gpa, .unlimited) catch |err| + return step.fail(maker, "unable to read snapshot file {f}: {t}", .{ snapshot.path, err }); + defer gpa.free(snapshot_contents); + + const result = switch (snapshot.result) { + .stdout => generic_result.stdout.?, + .stderr => generic_result.stderr.?, + }; + if (!mem.eql(u8, snapshot_contents, result)) { + return step.fail(maker, + \\ + \\========= snapshot file: ========= + \\{f} + \\========= contained: ============= + \\{s} + \\========= {t} output was: ======== + \\{s} + , .{ snapshot.path, snapshot_contents, snapshot.result, result }); + } + } }, else => { // On failure, report captured stderr like normal standard error output. @@ -2283,11 +2324,15 @@ fn setColorEnvironmentVariables( } fn checksContainStdout(conf_run: *const Configuration.Step.Run) bool { - return conf_run.expect_stdout_exact.value != null or conf_run.expect_stdout_match.slice.len != 0; + return conf_run.expect_stdout_exact.value != null or + conf_run.expect_stdout_match.slice.len != 0 or + conf_run.expect_stdout_snapshot.value != null; } fn checksContainStderr(conf_run: *const Configuration.Step.Run) bool { - return conf_run.expect_stderr_exact.value != null or conf_run.expect_stderr_match.slice.len != 0; + return conf_run.expect_stderr_exact.value != null or + conf_run.expect_stderr_match.slice.len != 0 or + conf_run.expect_stderr_snapshot.value != null; } /// If `path` is cwd-relative, make it relative to the cwd of the child instead. diff --git a/lib/compiler/configurer.zig b/lib/compiler/configurer.zig @@ -1006,6 +1006,8 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { status: Configuration.Step.Run.ExpectTermStatus, value: u32, } = null; + var expect_stderr_snapshot: ?Configuration.LazyPath.Index = null; + var expect_stdout_snapshot: ?Configuration.LazyPath.Index = null; switch (run.stdio) { .check => |checks| for (checks.items) |check| switch (check) { .expect_stderr_exact => |bytes| expect_stderr_exact = try wc.addBytes(bytes), @@ -1022,6 +1024,8 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { .stopped => |x| .{ .status = .stopped, .value = @intFromEnum(x) }, .unknown => |x| .{ .status = .unknown, .value = x }, }, + .expect_stderr_snapshot => |path| expect_stderr_snapshot = try s.addLazyPath(path), + .expect_stdout_snapshot => |path| expect_stdout_snapshot = try s.addLazyPath(path), }, else => {}, } @@ -1061,6 +1065,8 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { .expect_stdout_match = expect_stdout_match.items.len != 0, .expect_term = expect_term != null, .expect_term_status = if (expect_term) |t| t.status else .exited, + .expect_stderr_snapshot = expect_stderr_snapshot != null, + .expect_stdout_snapshot = expect_stdout_snapshot != null, }, .file_inputs = .{ .slice = try s.initLazyPathList(run.file_inputs.items) }, .args = .{ .slice = try s.initArgsList(run.argv.items) }, @@ -1081,6 +1087,8 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { .expect_stdout_exact = .{ .value = if (expect_stdout_exact) |bytes| bytes else null }, .expect_stderr_match = .{ .slice = expect_stderr_match.items }, .expect_stdout_match = .{ .slice = expect_stdout_match.items }, + .expect_stderr_snapshot = .{ .value = expect_stderr_snapshot orelse null }, + .expect_stdout_snapshot = .{ .value = expect_stdout_snapshot orelse null }, .stdin = .{ .u = switch (run.stdin) { .none => .none, .bytes => |bytes| .{ .bytes = try wc.addBytes(bytes) }, diff --git a/lib/compiler/objdump.zig b/lib/compiler/objdump.zig @@ -16,9 +16,12 @@ const Options = struct { input_path: []const u8, member_filters: []const []const u8 = &.{}, member_headers: bool, + omit_elements: std.enums.EnumArray(Element, bool), + redact: std.enums.EnumArray(FieldKind, bool), relocs: bool, section_filters: []const []const u8 = &.{}, section_headers: bool, + symbol_filters: []const []const u8 = &.{}, strings: bool, symbols: bool, tls: bool, @@ -27,6 +30,20 @@ const Options = struct { linker_member: ?std.coff.ArchiveMemberHeader.Kind, }; +const FieldKind = enum { + va, + rva, + ord, + size, +}; + +const Element = enum { + @"file-type", + @"table-header", + @"header-names", + newlines, +}; + pub fn main(init: std.process.Init) !void { const io = init.io; const args = try init.minimal.args.toSlice(init.arena.allocator()); @@ -40,12 +57,15 @@ pub fn main(init: std.process.Init) !void { var opt_input_path: ?[]const u8 = null; var opt_linker_member: ?std.coff.ArchiveMemberHeader.Kind = null; var opt_member_headers: ?bool = null; + var omit_elements: @FieldType(Options, "omit_elements") = .initFill(false); + var redact: @FieldType(Options, "redact") = .initFill(false); var opt_relocs: ?bool = null; var opt_section_headers: ?bool = null; var opt_strings: ?bool = null; var opt_symbols: ?bool = null; var opt_tls: ?bool = null; var section_filters: std.ArrayList([]const u8) = .empty; + var symbol_filters: std.ArrayList([]const u8) = .empty; var member_filters: std.ArrayList([]const u8) = .empty; while (i < args.len) : (i += 1) { const arg = args[i]; @@ -73,14 +93,37 @@ pub fn main(init: std.process.Init) !void { opt_linker_member = .second_linker; } else if (mem.eql(u8, arg, "--member-headers")) { opt_member_headers = true; - } else if (mem.startsWith(u8, arg, "--only-section=")) { - (try section_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-section=".len..]); + } else if (mem.startsWith(u8, arg, "--omit-element=")) { + const kind = arg["--omit-element=".len..]; + if (std.meta.stringToEnum(Element, kind)) |format_kind| { + omit_elements.set(format_kind, true); + } else if (std.mem.eql(u8, kind, "all")) { + omit_elements = .initFill(true); + } else { + fatal("unrecognized element: {s}", .{kind}); + } } else if (mem.startsWith(u8, arg, "--only-member=")) { (try member_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-member=".len..]); + } else if (mem.startsWith(u8, arg, "--only-section=")) { + (try section_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-section=".len..]); + } else if (mem.startsWith(u8, arg, "--only-symbol=")) { + (try symbol_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-symbol=".len..]); + } else if (mem.startsWith(u8, arg, "--redact=")) { + const kind = arg["--redact=".len..]; + if (std.meta.stringToEnum(FieldKind, kind)) |field_kind| { + redact.set(field_kind, true); + } else if (std.mem.eql(u8, kind, "all")) { + redact = .initFill(true); + } else { + fatal("unrecognized redaction kind: {s}", .{kind}); + } } else if (mem.eql(u8, arg, "--relocs")) { opt_relocs = true; } else if (mem.eql(u8, arg, "--section-headers")) { opt_section_headers = true; + } else if (mem.eql(u8, arg, "-s") or mem.eql(u8, arg, "--snapshot")) { + omit_elements = .initFill(true); + redact = .initFill(true); } else if (mem.eql(u8, arg, "--strings")) { opt_strings = true; } else if (mem.eql(u8, arg, "--symbols")) { @@ -105,10 +148,13 @@ pub fn main(init: std.process.Init) !void { .linker_member = opt_linker_member, .member_filters = member_filters.items, .member_headers = opt_member_headers orelse false, + .omit_elements = omit_elements, + .redact = redact, + .relocs = opt_relocs orelse false, .section_filters = section_filters.items, .section_headers = opt_section_headers orelse false, - .relocs = opt_relocs orelse false, .strings = opt_strings orelse false, + .symbol_filters = symbol_filters.items, .symbols = opt_symbols orelse false, .tls = opt_tls orelse false, }; @@ -120,7 +166,15 @@ pub fn main(init: std.process.Init) !void { var buffer: [4096]u8 = undefined; var file_reader = file.reader(io, &buffer); var stdout_writer = std.Io.File.stdout().writerStreaming(io, &stdout_buffer); - dump(init.gpa, &opts, &file_reader, &stdout_writer.interface) catch |err| switch (err) { + + const ctx: DumpContext = .{ + .gpa = init.gpa, + .opts = &opts, + .fr = &file_reader, + .w = &stdout_writer.interface, + }; + + dump(&ctx) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, error.WriteFailed => return stdout_writer.err.?, error.UnknownFile => fatal("unrecognized file: {s}", .{opts.input_path}), @@ -130,60 +184,82 @@ pub fn main(init: std.process.Init) !void { try stdout_writer.flush(); } -fn dump(gpa: std.mem.Allocator, opts: *const Options, fr: *Io.File.Reader, w: *Io.Writer) !void { - const r = &fr.interface; +fn dump(d: *const DumpContext) !void { + const r = &d.fr.interface; try r.fill(4); elf: { if (!mem.eql(u8, r.buffered()[0..4], std.elf.MAGIC)) break :elf; - return elf.dump(r, w); + return elf.dump(r, d.w); } macho: { if (mem.readInt(u32, r.buffered()[0..4], .little) != std.macho.MH_MAGIC_64) break :macho; - return macho.dump(r, w); + return macho.dump(r, d.w); } wasm: { comptime assert(std.wasm.magic.len == 4); if (!mem.eql(u8, r.buffered()[0..4], &std.wasm.magic)) break :wasm; - return wasm.dump(r, w); + return wasm.dump(r, d.w); } coff: { - const ext = std.fs.path.extension(opts.input_path); - const basename = std.fs.path.basename(opts.input_path); + const ext = std.fs.path.extension(d.opts.input_path); + const basename = std.fs.path.basename(d.opts.input_path); if (std.mem.eql(u8, ext, ".exe") or std.mem.eql(u8, ext, ".dll")) { if (!mem.eql(u8, r.buffered()[0..2], "MZ")) break :coff; try r.discardAll(std.coff.pe_pointer_offset); const sig_offset = try r.takeInt(u32, .little); - try fr.seekTo(sig_offset); + try d.fr.seekTo(sig_offset); const sig = try r.take(4); if (!std.mem.eql(u8, sig, std.coff.pe_signature)) { - try w.print("invalid PE signature: {x}", .{sig}); + try d.w.print("invalid PE signature: {x}", .{sig}); return error.ParseFailure; } - try w.print("{s}: PE/COFF image\n\n", .{basename}); - return coff.dumpObject(gpa, opts, true, basename, fr, w); + if (d.element(.@"file-type")) + try d.w.print("{s}: PE/COFF image\n\n", .{basename}); + + return coff.dumpObject(d, true, basename); } else if (std.mem.eql(u8, ext, ".lib")) { r.fill(std.coff.archive_signature.len) catch break :coff; if (!mem.eql(u8, r.buffered()[0..std.coff.archive_signature.len], std.coff.archive_signature)) break :coff; - try w.print("{s}: COFF archive\n\n", .{basename}); - return coff.dumpArchive(gpa, opts, fr, w); + if (d.element(.@"file-type")) + try d.w.print("{s}: COFF archive\n\n", .{basename}); + + return coff.dumpArchive(d); } else if (std.mem.eql(u8, ext, ".obj")) { - try w.print("{s}: COFF object\n\n", .{basename}); - return coff.dumpObject(gpa, opts, false, basename, fr, w); + if (d.element(.@"file-type")) + try d.w.print("{s}: COFF object\n\n", .{basename}); + + return coff.dumpObject(d, false, basename); } } return error.UnknownFile; } -fn failParse( +const DumpContext = struct { + gpa: std.mem.Allocator, opts: *const Options, - comptime fmt: []const u8, - args: anytype, -) noreturn { - std.log.err("error parsing '{s}'", .{std.fs.path.basename(opts.input_path)}); - fatal(fmt, args); -} + fr: *Io.File.Reader, + w: *Io.Writer, + + fn element(self: *const DumpContext, e: Element) bool { + return !self.opts.omit_elements.get(e); + } + + fn redacted(self: *const DumpContext, opt_kind: ?FieldKind) bool { + const kind = opt_kind orelse return false; + return self.opts.redact.get(kind); + } + + fn failParse( + ctx: *const DumpContext, + comptime fmt: []const u8, + args: anytype, + ) noreturn { + std.log.err("error parsing '{s}'", .{std.fs.path.basename(ctx.opts.input_path)}); + fatal(fmt, args); + } +}; const elf = struct { fn dump(r: *Io.Reader, w: *Io.Writer) !void { @@ -230,29 +306,33 @@ const coff = struct { file_mode: u24, size: u34, - pub fn fromRaw(opts: *const Options, raw_header: *const std.coff.ArchiveMemberHeader, opt_longnames: ?[]const u8) @This() { + pub fn fromRaw(d: *const DumpContext, raw_header: *const std.coff.ArchiveMemberHeader, opt_longnames: ?[]const u8) @This() { const name = raw_header.parseName(opt_longnames) catch |err| switch (err) { - error.BadName => failParse(opts, "malformed member name: '{s}'", .{&raw_header.name}), - error.NoLongNames => failParse(opts, "member uses a long name, but there was no longnames member", .{}), + error.BadName => d.failParse("malformed member name: '{s}'", .{&raw_header.name}), + error.NoLongNames => d.failParse("member uses a long name, but there was no longnames member", .{}), }; return .{ .name = name, .date = raw_header.parseDate() catch |err| - failParse(opts, "unable to parse date '{s}' in member '{s}': {t}", .{ raw_header.date, name, err }), + d.failParse("unable to parse date '{s}' in member '{s}': {t}", .{ raw_header.date, name, err }), .user_id = raw_header.parseUserId() catch |err| - failParse(opts, "unable to parse user_id '{s}' in member '{s}': {t}", .{ raw_header.user_id, name, err }), + d.failParse("unable to parse user_id '{s}' in member '{s}': {t}", .{ raw_header.user_id, name, err }), .group_id = raw_header.parseGroupId() catch |err| - failParse(opts, "unable to parse group_id '{s}' in member '{s}': {t}", .{ raw_header.group_id, name, err }), + d.failParse("unable to parse group_id '{s}' in member '{s}': {t}", .{ raw_header.group_id, name, err }), .file_mode = raw_header.parseFileMode() catch |err| - failParse(opts, "unable to parse file_mode '{s}' in member '{s}': {t}", .{ raw_header.file_mode, name, err }), + d.failParse("unable to parse file_mode '{s}' in member '{s}': {t}", .{ raw_header.file_mode, name, err }), .size = raw_header.parseSize() catch |err| - failParse(opts, "unable to parse size '{s}' in member '{s}': {t}", .{ raw_header.size, name, err }), + d.failParse("unable to parse size '{s}' in member '{s}': {t}", .{ raw_header.size, name, err }), }; } }; - fn dumpArchive(gpa: std.mem.Allocator, opts: *const Options, fr: *Io.File.Reader, w: *Io.Writer) !void { + fn dumpArchive(d: *const DumpContext) !void { + const gpa = d.gpa; + const fr = d.fr; + const w = d.w; + const r = &fr.interface; r.toss(std.coff.archive_signature.len); @@ -272,26 +352,26 @@ const coff = struct { while (pos < size) : (pos = fr.logicalPos()) { if ((pos & 1) != 0) try r.discardAll(1); const raw_header = try r.takeStruct(std.coff.ArchiveMemberHeader, .little); - const header: ArchiveHeader = .fromRaw(opts, &raw_header, opt_longnames); + const header: ArchiveHeader = .fromRaw(d, &raw_header, opt_longnames); if (!std.mem.eql(u8, &raw_header.end_of_header, std.coff.archive_end_of_header)) - return failParse(opts, "malformed end-of-header field in member '{s}': {x}", .{ header.name, raw_header.end_of_header }); + return d.failParse("malformed end-of-header field in member '{s}': {x}", .{ header.name, raw_header.end_of_header }); const dump_header = - (opts.member_headers and filterMatches(opts.member_filters, header.name)) or - (opts.linker_member == opt_expected_kind); + (d.opts.member_headers and filterMatches(d.opts.member_filters, header.name)) or + (d.opts.linker_member == opt_expected_kind); if (dump_header) - try dumpArchiveHeader(w, &header, @intCast(pos)); + try dumpArchiveHeader(d, &header, @intCast(pos)); const member_end = fr.logicalPos() + header.size; if (member_end > size) - return failParse(opts, "out-of-bounds length 0x{x} in member '{s}'", .{ header.size, header.name }); + return d.failParse("out-of-bounds length 0x{x} in member '{s}'", .{ header.size, header.name }); if (opt_expected_kind) |expected_kind| switch (expected_kind) { .first_linker => { if (!std.mem.eql(u8, header.name, "/")) - return failParse(opts, "expected first linker member, found '{s}'", .{header.name}); + return d.failParse("expected first linker member, found '{s}'", .{header.name}); const num_symbols = try r.takeInt(u32, .big); if (dump_header) @@ -301,7 +381,7 @@ const coff = struct { \\ , .{ expected_kind, num_symbols }); - if (opts.linker_member == .first_linker) { + if (d.opts.linker_member == .first_linker) { try w.writeAll( \\ \\Archive symbols: @@ -314,11 +394,15 @@ const coff = struct { for (0..num_symbols) |symbol_i| { const symbol = r.takeDelimiter(0) catch |err| - return failParse(opts, "unable to read first linker member string table: {t}", .{err}); - try w.print("{x: >8} {s}\n", .{ std.mem.readInt(u32, offsets[symbol_i * 4 ..][0..4], .big), symbol.? }); + return d.failParse("unable to read first linker member string table: {t}", .{err}); + const offset = std.mem.readInt(u32, offsets[symbol_i * 4 ..][0..4], .big); + try w.print("{f} {s}\n", .{ + fmtIntField(d, offset, .{ .kind = .va }), + symbol.?, + }); } } - if (dump_header) try w.writeByte('\n'); + if (dump_header and d.element(.newlines)) try w.writeByte('\n'); try fr.seekTo(member_end); opt_expected_kind = .second_linker; @@ -326,12 +410,12 @@ const coff = struct { }, .second_linker => { if (!std.mem.eql(u8, header.name, "/")) - return failParse(opts, "expected second linker member, found '{s}'", .{header.name}); + return d.failParse("expected second linker member, found '{s}'", .{header.name}); const num_members = try r.takeInt(u32, .little); pos = fr.logicalPos(); if (pos + num_members * @sizeOf(u32) > member_end) - return failParse(opts, "invalid member count 0x{x} in second linker member", .{num_members}); + return d.failParse("invalid member count 0x{x} in second linker member", .{num_members}); try members.ensureTotalCapacity(gpa, num_members); for (0..num_members) |_| @@ -342,7 +426,7 @@ const coff = struct { const num_symbols = try r.takeInt(u32, .little); pos = fr.logicalPos(); if (pos + num_symbols * @sizeOf(u16) > member_end) - return failParse(opts, "invalid symbol count 0x{x} in second linker member", .{num_symbols}); + return d.failParse("invalid symbol count 0x{x} in second linker member", .{num_symbols}); if (dump_header) try w.print( @@ -356,13 +440,14 @@ const coff = struct { for (0..num_symbols) |_| symbol_member_indices.addOneAssumeCapacity().* = (try r.takeInt(u16, .little)) - 1; - if (opts.linker_member == .second_linker) { - try w.writeAll( - \\ - \\Archive Symbols: - \\& Member Symbol - \\ - ); + if (d.opts.linker_member == .second_linker) { + if (d.element(.@"table-header")) + try w.writeAll( + \\ + \\Archive Symbols: + \\& Member Symbol + \\ + ); pos = fr.logicalPos(); var symbol_i: u32 = 0; @@ -373,23 +458,26 @@ const coff = struct { const symbol_name = if (r.takeDelimiter(0) catch |err| switch (err) { error.StreamTooLong => null, else => |e| return e, - }) |n| n else return failParse(opts, "unterminated string found in second linker member", .{}); - - try w.print("{x: >8} {s}\n", .{ - members.items[symbol_member_indices.items[symbol_i]].offset, + }) |n| n else return d.failParse("unterminated string found in second linker member", .{}); + + try w.print("{f} {s}\n", .{ + fmtIntField( + d, + members.items[symbol_member_indices.items[symbol_i]].offset, + .{ .kind = .va }, + ), symbol_name, }); } if (symbol_i != num_symbols) - return failParse( - opts, + return d.failParse( " expected {d} entries in second linker member string table, but found {d}", .{ num_symbols, symbol_i }, ); } - try w.writeByte('\n'); + if (d.element(.newlines)) try w.writeByte('\n'); try fr.seekTo(member_end); opt_expected_kind = .longnames; continue; @@ -401,12 +489,13 @@ const coff = struct { if (dump_header) try w.print("{t: >16} type\n", .{expected_kind}); - if (opts.linker_member == .longnames) { - try w.print( - \\ - \\Longnames (0x{x} bytes): - \\ - , .{opt_longnames.?.len}); + if (d.opts.linker_member == .longnames) { + if (d.element(.@"table-header")) + try w.print( + \\ + \\Longnames (0x{x} bytes): + \\ + , .{opt_longnames.?.len}); var lr = Io.Reader.fixed(opt_longnames.?); while (try lr.takeDelimiter(0)) |str| { @@ -415,7 +504,7 @@ const coff = struct { } } - try w.writeByte('\n'); + if (d.element(.newlines)) try w.writeByte('\n'); } opt_expected_kind = null; @@ -426,18 +515,18 @@ const coff = struct { } if (opt_expected_kind) |expected_kind| switch (expected_kind) { - .first_linker => failParse(opts, "missing first linker member", .{}), - .second_linker => failParse(opts, "missing second linker member", .{}), + .first_linker => d.failParse("missing first linker member", .{}), + .second_linker => d.failParse("missing second linker member", .{}), else => {}, }; for (members.items, 0..) |member, member_i| { fr.seekTo(member.offset) catch |err| - failParse(opts, "unable to read member {d} at offset 0x{x}: {t}", .{ member_i, member.offset, err }); + d.failParse("unable to read member {d} at offset 0x{x}: {t}", .{ member_i, member.offset, err }); const raw_header = try r.takeStruct(std.coff.ArchiveMemberHeader, .little); - const header: ArchiveHeader = .fromRaw(opts, &raw_header, opt_longnames); - if (!filterMatches(opts.member_filters, header.name)) continue; + const header: ArchiveHeader = .fromRaw(d, &raw_header, opt_longnames); + if (!filterMatches(d.opts.member_filters, header.name)) continue; const member_sig = try r.peek(4); const machine: std.coff.IMAGE.FILE.MACHINE = @@ -445,17 +534,17 @@ const coff = struct { const sig = std.mem.readInt(u16, member_sig[2..4], .little); const is_imp_lib = machine == std.coff.IMAGE.FILE.MACHINE.UNKNOWN and sig == 0xffff; - if (opts.member_headers or (opts.exports and is_imp_lib)) { - try dumpArchiveHeader(w, &header, member.offset); + if (d.opts.member_headers or (d.opts.exports and is_imp_lib)) { + try dumpArchiveHeader(d, &header, member.offset); if (is_imp_lib) { try w.writeAll("\nImport header:\n"); const imp_header = try r.takeStruct(std.coff.ImportHeader, .little); - try dumpHeader(w, std.coff.ImportHeader, &imp_header, struct { - pub fn sig1(_: *const std.coff.ImportHeader, _: *Io.Writer) !void {} - pub fn sig2(_: *const std.coff.ImportHeader, _: *Io.Writer) !void {} - pub fn types(h: *const std.coff.ImportHeader, cw: *Io.Writer) !void { - try cw.print( + try dumpHeader(d, std.coff.ImportHeader, &imp_header, struct { + pub fn sig1(_: *const DumpContext, _: *const std.coff.ImportHeader) !void {} + pub fn sig2(_: *const DumpContext, _: *const std.coff.ImportHeader) !void {} + pub fn types(id: *const DumpContext, h: *const std.coff.ImportHeader) !void { + try id.w.print( \\{t: >16} import_type \\{t: >16} name_type \\ @@ -490,51 +579,53 @@ const coff = struct { } else { try w.writeAll(" COFF object type\n"); } - try w.writeByte('\n'); + if (d.element(.newlines)) try w.writeByte('\n'); } if (is_imp_lib) continue; - if (opts.section_headers or - opts.file_headers or - opts.relocs or - opts.strings or - opts.symbols) + if (d.opts.section_headers or + d.opts.file_headers or + d.opts.relocs or + d.opts.strings or + d.opts.symbols) { - try w.print("{s}({s}): COFF object\n\n", .{ std.fs.path.basename(opts.input_path), header.name }); - try dumpObject(gpa, opts, false, header.name, fr, w); + if (d.element(.@"file-type")) + try w.print("{s}({s}): COFF object\n\n", .{ std.fs.path.basename(d.opts.input_path), header.name }); + try dumpObject(d, false, header.name); } } } fn dumpObject( - gpa: std.mem.Allocator, - opts: *const Options, + d: *const DumpContext, is_image: bool, obj_name: []const u8, - fr: *Io.File.Reader, - w: *Io.Writer, ) !void { + const gpa = d.gpa; + const fr = d.fr; + const w = d.w; + const file_location = fr.logicalPos(); const r = &fr.interface; const header = r.takeStruct(std.coff.Header, .little) catch |err| - return failParse(opts, "unable to read COFF header: {t}", .{err}); + return d.failParse("unable to read COFF header: {t}", .{err}); - if (opts.file_headers) { - try w.writeAll("COFF Header:\n"); - try dumpHeader(w, std.coff.Header, &header, struct {}); - try w.writeByte('\n'); + if (d.opts.file_headers) { + if (d.element(.@"header-names")) try w.writeAll("COFF Header:\n"); + try dumpHeader(d, std.coff.Header, &header, struct {}); + if (d.element(.newlines)) try w.writeByte('\n'); } switch (header.machine) { - _ => return failParse(opts, "unknown machine type: {x}", .{header.machine}), + _ => return d.failParse("unknown machine type: {x}", .{header.machine}), else => {}, } var known_dirs: [DIRECTORY_ENTRY.len]std.coff.ImageDataDirectory = undefined; const needs_data_dirs = - opts.exports or - opts.imports or - opts.tls; + d.opts.exports or + d.opts.imports or + d.opts.tls; const ImageInfo = struct { data_dirs: []const std.coff.ImageDataDirectory, @@ -543,12 +634,14 @@ const coff = struct { }; const image_info: ?ImageInfo = if (header.size_of_optional_header > 0) image_info: { - if (!opts.file_headers and !needs_data_dirs) { + if (!d.opts.file_headers and !needs_data_dirs) { try fr.seekBy(header.size_of_optional_header); break :image_info null; } - if (opts.file_headers) try w.writeAll("COFF Optional Header:\n"); + if (d.opts.file_headers and d.element(.@"header-names")) + try w.writeAll("COFF Optional Header:\n"); + const magic: std.coff.OptionalHeader.Magic = @enumFromInt(try r.peekInt(u16, .little)); const num_directory_entries, const image_base = switch (magic) { inline .PE32, .@"PE32+" => |v| num_data_dirs: { @@ -558,46 +651,46 @@ const coff = struct { std.coff.OptionalHeader.@"PE32+"; const optional_header = r.takeStruct(OptionalHeader, .little) catch |err| - return failParse(opts, "unable to read optional header: {t}", .{err}); + return d.failParse("unable to read optional header: {t}", .{err}); - if (opts.file_headers) { - try dumpHeader(w, OptionalHeader, &optional_header, struct { - pub fn base_of_code(h: *const std.coff.OptionalHeader, cw: *Io.Writer) !void { + if (d.opts.file_headers) { + try dumpHeader(d, OptionalHeader, &optional_header, struct { + pub fn base_of_code(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void { const base = @as(*const OptionalHeader, @ptrCast(@alignCast(h))).image_base; - try dumpRvaField(cw, @src().fn_name, h.base_of_code, base); + try dumpRvaField(id, @src().fn_name, h.base_of_code, base); } - pub fn address_of_entry_point(h: *const std.coff.OptionalHeader, cw: *Io.Writer) !void { + pub fn address_of_entry_point(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void { const base = @as(*const OptionalHeader, @ptrCast(@alignCast(h))).image_base; - try dumpRvaField(cw, @src().fn_name, h.base_of_code, base); + try dumpRvaField(id, @src().fn_name, h.base_of_code, base); } - pub fn major_linker_version(h: *const std.coff.OptionalHeader, cw: *Io.Writer) !void { - try dumpVersionField(cw, "linker_version", h.major_linker_version, h.minor_linker_version); + pub fn major_linker_version(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void { + try dumpVersionField(id.w, "linker_version", h.major_linker_version, h.minor_linker_version); } - pub fn minor_linker_version(_: *const std.coff.OptionalHeader, _: *Io.Writer) !void {} + pub fn minor_linker_version(_: *const DumpContext, _: *const std.coff.OptionalHeader) !void {} - pub fn major_operating_system_version(h: *const OptionalHeader, cw: *Io.Writer) !void { + pub fn major_operating_system_version(id: *const DumpContext, h: *const OptionalHeader) !void { try dumpVersionField( - cw, + id.w, "operating_system_version", h.major_operating_system_version, h.minor_operating_system_version, ); } - pub fn minor_operating_system_version(_: *const OptionalHeader, _: *Io.Writer) !void {} + pub fn minor_operating_system_version(_: *const DumpContext, _: *const OptionalHeader) !void {} - pub fn major_image_version(h: *const OptionalHeader, cw: *Io.Writer) !void { - try dumpVersionField(cw, "image_version", h.major_image_version, h.minor_image_version); + pub fn major_image_version(id: *const DumpContext, h: *const OptionalHeader) !void { + try dumpVersionField(id.w, "image_version", h.major_image_version, h.minor_image_version); } - pub fn minor_image_version(_: *const OptionalHeader, _: *Io.Writer) !void {} + pub fn minor_image_version(_: *const DumpContext, _: *const OptionalHeader) !void {} - pub fn major_subsystem_version(h: *const OptionalHeader, cw: *Io.Writer) !void { - try dumpVersionField(cw, "subsystem_version", h.major_subsystem_version, h.minor_subsystem_version); + pub fn major_subsystem_version(id: *const DumpContext, h: *const OptionalHeader) !void { + try dumpVersionField(id.w, "subsystem_version", h.major_subsystem_version, h.minor_subsystem_version); } - pub fn minor_subsystem_version(_: *const OptionalHeader, _: *Io.Writer) !void {} + pub fn minor_subsystem_version(_: *const DumpContext, _: *const OptionalHeader) !void {} }); - try w.writeByte('\n'); + if (d.element(.newlines)) try w.writeByte('\n'); } break :num_data_dirs .{ @@ -605,24 +698,26 @@ const coff = struct { optional_header.image_base, }; }, - else => return failParse(opts, "invalid optional header magic number: {x}", .{magic}), + else => return d.failParse("invalid optional header magic number: {x}", .{magic}), }; - if (opts.file_headers) try w.writeAll("Data Directories:\n"); + if (d.opts.file_headers and d.element(.@"header-names")) + try w.writeAll("Data Directories:\n"); + for (0..num_directory_entries) |dir_i| { const dir = r.takeStruct(std.coff.ImageDataDirectory, .little) catch |err| - return failParse(opts, "unable to read data directory {x}: {t}", .{ dir_i, err }); + return d.failParse("unable to read data directory {x}: {t}", .{ dir_i, err }); if (dir_i < known_dirs.len) known_dirs[dir_i] = dir; - if (opts.file_headers) + if (d.opts.file_headers) try w.print( "{x: >16} {x: >8} {t}\n", .{ dir.virtual_address, dir.size, @as(DIRECTORY_ENTRY, @enumFromInt(dir_i)) }, ); } - if (opts.file_headers) try w.writeByte('\n'); + if (d.opts.file_headers and d.element(.newlines)) try w.writeByte('\n'); break :image_info .{ .data_dirs = known_dirs[0..@min(known_dirs.len, num_directory_entries)], @@ -630,32 +725,33 @@ const coff = struct { .image_base = image_base, }; } else if (is_image) { - return failParse(opts, "image did not contain an optional header", .{}); + return d.failParse("image did not contain an optional header", .{}); } else null; // Section names in images don't use the string table, as they must fit inline in the header - const load_string_table = (opts.strings or !is_image) and header.pointer_to_symbol_table > 0; + const load_string_table = (d.opts.strings or !is_image) and header.pointer_to_symbol_table > 0; const string_table = if (load_string_table) string_table: { const pos = fr.logicalPos(); fr.seekTo(file_location + header.pointer_to_symbol_table + header.number_of_symbols * std.coff.Symbol.sizeOf()) catch |err| - return failParse(opts, "unable to seek to string table: {t}", .{err}); + return d.failParse("unable to seek to string table: {t}", .{err}); const string_table_len = r.peekInt(u32, .little) catch |err| - return failParse(opts, "unable to read string table length: {t}", .{err}); + return d.failParse("unable to read string table length: {t}", .{err}); const table = r.readAlloc(gpa, string_table_len) catch |err| - return failParse(opts, "unable to read string table: {t}", .{err}); + return d.failParse("unable to read string table: {t}", .{err}); try fr.seekTo(pos); break :string_table table; } else &.{}; defer gpa.free(string_table); - if (opts.strings) { - try w.print( - \\String Table (0x{x} bytes): - \\ - , .{string_table.len}); + if (d.opts.strings) { + if (d.element(.@"table-header")) + try w.print( + \\String Table (0x{x} bytes): + \\ + , .{string_table.len}); var sr = Io.Reader.fixed(string_table[4..]); while (try sr.takeDelimiter(0)) |str| { @@ -663,7 +759,7 @@ const coff = struct { try w.writeByte('\n'); } - try w.writeByte('\n'); + if (d.element(.newlines)) try w.writeByte('\n'); } var sections: std.ArrayList(Section) = .empty; @@ -671,13 +767,13 @@ const coff = struct { var sections_with_data: u16 = 0; const load_sections = - opts.section_headers or - opts.symbols or - opts.relocs or + d.opts.section_headers or + d.opts.symbols or + d.opts.relocs or needs_data_dirs; if (load_sections) { - if (opts.section_headers) + if (d.opts.section_headers and d.element(.@"table-header")) try w.print( \\Sections in '{s}': \\Num Name RVA Virt Size Data Size & Data & Relocs & Lines # Relocs # Lines Flags @@ -687,37 +783,37 @@ const coff = struct { try sections.resize(gpa, header.number_of_sections); for (sections.items, 0..) |*section, section_i| { section.header = r.takeStruct(std.coff.SectionHeader, .little) catch |err| - return failParse(opts, "unable to read section header {x}: {t}", .{ section_i, err }); + return d.failParse("unable to read section header {x}: {t}", .{ section_i, err }); section.name = headerName(&section.header.name, string_table) catch |err| switch (err) { error.Overflow, error.InvalidCharacter, - => return failParse(opts, "unable to parse section name offset '{s}': {t}", .{ + => return d.failParse("unable to parse section name offset '{s}': {t}", .{ section.name, err, }), - error.OutOfBounds => return failParse(opts, "section name offset '{s}' was out of bounds (>= {x})", .{ + error.OutOfBounds => return d.failParse("section name offset '{s}' was out of bounds (>= {x})", .{ section.name, string_table.len, }), }; sections_with_data += @intFromBool(section.header.size_of_raw_data > 0); - if (opts.section_headers) { - if (!filterMatches(opts.section_filters, section.name)) continue; + if (d.opts.section_headers) { + if (!filterMatches(d.opts.section_filters, section.name)) continue; const raw_name = std.mem.sliceTo(&section.header.name, 0); try w.print( - "{x: >3} {s: <8} {x: >8} {x: >9} {x: >9} {x: >8} {x: >8} {x: >8} {x: >8} {x: >8} {x:0>8} |", + "{x: >3} {s: <8} {f} {f} {f} {f} {f} {f} {f} {f} {x:0>8} |", .{ section_i + 1, raw_name, - section.header.virtual_address, - section.header.virtual_size, - section.header.size_of_raw_data, - section.header.pointer_to_raw_data, - section.header.pointer_to_relocations, - section.header.pointer_to_linenumbers, - section.header.number_of_relocations, - section.header.number_of_linenumbers, + fmtIntField(d, section.header.virtual_address, .{ .kind = .va }), + fmtIntField(d, section.header.virtual_size, .{ .kind = .size, .width = 9 }), + fmtIntField(d, section.header.size_of_raw_data, .{ .kind = .size, .width = 9 }), + fmtIntField(d, section.header.pointer_to_raw_data, .{ .kind = .va }), + fmtIntField(d, section.header.pointer_to_relocations, .{ .kind = .va }), + fmtIntField(d, section.header.pointer_to_linenumbers, .{ .kind = .va }), + fmtIntField(d, section.header.number_of_relocations, .{ .kind = .va }), + fmtIntField(d, section.header.number_of_linenumbers, .{ .kind = .va }), @as(u32, @bitCast(section.header.flags)), }, ); @@ -730,7 +826,7 @@ const coff = struct { } } - if (opts.section_headers) try w.writeByte('\n'); + if (d.opts.section_headers and d.element(.newlines)) try w.writeByte('\n'); } var symbols: std.ArrayList(struct { @@ -738,15 +834,15 @@ const coff = struct { section_number: std.coff.SectionNumber, }) = .empty; defer symbols.deinit(gpa); - if (opts.relocs) + if (d.opts.relocs) try symbols.ensureUnusedCapacity(gpa, header.number_of_symbols); - if (opts.symbols or opts.relocs) { + if (d.opts.symbols or d.opts.relocs) { if (header.pointer_to_symbol_table > 0) { fr.seekTo(file_location + header.pointer_to_symbol_table) catch |err| - return failParse(opts, "unable to seek to symbol table: {t}", .{err}); + return d.failParse("unable to seek to symbol table: {t}", .{err}); - if (opts.symbols) + if (d.opts.symbols and d.element(.@"table-header")) try w.print( \\Symbols in '{s}': \\ Ord Value Sect Type Storage Name @@ -758,7 +854,7 @@ const coff = struct { while (symbol_i < header.number_of_symbols) { var symbol: std.coff.Symbol = undefined; const symbol_bytes = r.take(symbol_size) catch |err| - return failParse(opts, "unable to read symbol {x}: {t}", .{ symbol_i, err }); + return d.failParse("unable to read symbol {x}: {t}", .{ symbol_i, err }); @memcpy(std.mem.asBytes(&symbol)[0..symbol_size], symbol_bytes); if (native_endian != .little) @@ -773,7 +869,7 @@ const coff = struct { const name = std.mem.sliceTo(if (std.mem.eql(u8, symbol.name[0..4], "\x00\x00\x00\x00")) name: { const index = std.mem.readInt(u32, symbol.name[4..], .little); if (index >= string_table.len) - return failParse(opts, "invalid name offset for symbol {x} ({x} >= {x})", .{ + return d.failParse("invalid name offset for symbol {x} ({x} >= {x})", .{ symbol_i, index, string_table.len, @@ -781,16 +877,19 @@ const coff = struct { break :name string_table[index..]; } else &symbol.name, 0); - if (opts.relocs) + if (d.opts.relocs) symbols.appendNTimesAssumeCapacity(.{ .name = name, .section_number = symbol.section_number, }, 1 + symbol.number_of_aux_symbols); - if (!opts.symbols) + if (!d.opts.symbols or !filterMatches(d.opts.symbol_filters, name)) continue; - try w.print("{x: >4} {x:0>8} ", .{ symbol_i, symbol.value }); + try w.print("{f} {x:0>8} ", .{ + fmtIntField(d, @as(u16, @intCast(symbol_i)), .{ .kind = .ord }), + symbol.value, + }); try switch (symbol.section_number) { .UNDEFINED => w.writeAll("UNDEF"), .ABSOLUTE => w.writeAll(" ABS"), @@ -814,8 +913,7 @@ const coff = struct { else => null, }) |suffix| try w.writeAll(suffix) else try w.print("{x}", .{symbol.type.complex_type}); - try w.print("{t: >16} | {s}", .{ symbol.storage_class, name }); - try w.writeByte('\n'); + try w.print("{t: >16} | {s}\n", .{ symbol.storage_class, name }); for (0..symbol.number_of_aux_symbols) |aux_i| { _ = aux_i; @@ -838,8 +936,7 @@ const coff = struct { try w.writeAll("TODO bf / ef aux symbol"); } else if (symbol.storage_class == .WEAK_EXTERNAL and symbol.section_number == .UNDEFINED) { if (symbol.value != 0) - return failParse( - opts, + return d.failParse( "invalid value 0x{x} for weak external symbol 0x{x}", .{ symbol.value, symbol_i }, ); @@ -850,14 +947,11 @@ const coff = struct { std.mem.byteSwapAllFields(std.coff.SectionDefinition, &weak_external); if (weak_external.tag_index >= header.number_of_symbols) - return failParse( - opts, + return d.failParse( "invalid tag_index 0x{x} for weak external symbol 0x{x}", .{ weak_external.tag_index, symbol_i }, ); - // TODO - try w.print(" Weak External [falls back to {x:0>8} via {t}]", .{ weak_external.tag_index, weak_external.flag, @@ -914,8 +1008,8 @@ const coff = struct { continue; } - try w.print(" [size {x:0>8} chksum {x:0>8} relocs {x:0>4} lines {x:0>4}]", .{ - section_def.length, + try w.print(" [size {f} chksum {x:0>8} relocs {x:0>4} lines {x:0>4}]", .{ + fmtIntField(d, section_def.length, .{ .kind = .size, .zero_fill = true }), section_def.checksum, section_def.number_of_relocations, section_def.number_of_linenumbers, @@ -930,19 +1024,19 @@ const coff = struct { try w.writeAll(")"); }, } - } else {} + } try w.writeByte('\n'); } } - if (opts.symbols) try w.writeByte('\n'); - } else if (opts.symbols) { + if (d.opts.symbols and d.element(.newlines)) try w.writeByte('\n'); + } else if (d.opts.symbols) { try w.writeAll("No symbol table found\n"); } } - if (opts.relocs) { + if (d.opts.relocs) { const relocation_size = std.coff.Relocation.sizeOf(); for (sections.items, 0..) |section, section_i| { @@ -955,7 +1049,7 @@ const coff = struct { , .{ section_i + 1, section.name, obj_name }); fr.seekTo(file_location + section.header.pointer_to_relocations) catch |err| - return failParse(opts, "unable to seek to section {x} relocation table: {t}", .{ section_i + 1, err }); + return d.failParse("unable to seek to section {x} relocation table: {t}", .{ section_i + 1, err }); for (0..section.header.number_of_relocations) |reloc_i| { var reloc: std.coff.Relocation = undefined; @@ -963,7 +1057,13 @@ const coff = struct { if (native_endian != .little) std.mem.byteSwapAllFields(std.coff.Relocation, &reloc); - try w.print("{x:0>8} ", .{reloc.virtual_address}); + const sym = &symbols.items[reloc.symbol_table_index]; + if (!filterMatches(d.opts.symbol_filters, sym.name)) + continue; + + try w.print("{f} ", .{ + fmtIntField(d, reloc.virtual_address, .{ .kind = .va, .zero_fill = true }), + }); switch (header.machine) { _ => unreachable, inline else => |m| switch (m.RelocationType()) { @@ -976,20 +1076,18 @@ const coff = struct { } if (reloc.symbol_table_index >= symbols.items.len) - return failParse( - opts, + return d.failParse( "reloc {x} in section {x} has out-of-bounds symbol index {x}", .{ reloc_i, section_i + 1, reloc.symbol_table_index }, ); - const sym = &symbols.items[reloc.symbol_table_index]; - try w.print("{x: >8} {f} | {s}\n", .{ - reloc.symbol_table_index, + try w.print("{f} {f} | {s}\n", .{ + fmtIntField(d, reloc.symbol_table_index, .{ .kind = .ord }), fmtSectionNumber(sym.section_number), sym.name, }); } - try w.writeByte('\n'); + if (d.element(.newlines)) try w.writeByte('\n'); } } @@ -1026,44 +1124,40 @@ const coff = struct { } else &.{}; defer gpa.free(rva_index); - if (opts.exports) { - if (try seekToDataDirectory(opts, fr, w, rva_index, sections.items, image_info.?.data_dirs, .EXPORT)) |section_index| { + if (d.opts.exports) { + if (try seekToDataDirectory(d, rva_index, sections.items, image_info.?.data_dirs, .EXPORT)) |section_index| { const export_dir = r.takeStruct(std.coff.ExportDirectoryTable, .little) catch |err| - return failParse(opts, "unable to read export directory: {t}", .{err}); + return d.failParse("unable to read export directory: {t}", .{err}); try w.print("Export directory:\n", .{}); - try dumpHeader(w, std.coff.ExportDirectoryTable, &export_dir, struct { - pub fn major_version(h: *const std.coff.ExportDirectoryTable, cw: *Io.Writer) !void { - try dumpVersionField(cw, "version", h.major_version, h.minor_version); + try dumpHeader(d, std.coff.ExportDirectoryTable, &export_dir, struct { + pub fn major_version(id: *const DumpContext, h: *const std.coff.ExportDirectoryTable) !void { + try dumpVersionField(id.w, "version", h.major_version, h.minor_version); } - pub fn minor_version(_: *const std.coff.ExportDirectoryTable, _: *Io.Writer) !void {} + pub fn minor_version(_: *const DumpContext, _: *const std.coff.ExportDirectoryTable) !void {} }); const section = sections.items[section_index]; const name_loc = section.rvaFileOffset(export_dir.name_rva) catch - return failParse( - opts, + return d.failParse( "export name rva 0x{x} was not within the export section", .{export_dir.name_rva}, ); const eat_loc = section.rvaFileOffset(export_dir.export_address_table_rva) catch - return failParse( - opts, + return d.failParse( "export address table rva 0x{x} was not within the export section", .{export_dir.export_address_table_rva}, ); const name_pointer_loc = section.rvaFileOffset(export_dir.name_pointer_table_rva) catch - return failParse( - opts, + return d.failParse( "export name pointer table rva 0x{x} was not within the export section", .{export_dir.name_pointer_table_rva}, ); const ord_loc = section.rvaFileOffset(export_dir.ordinal_table_rva) catch - return failParse( - opts, + return d.failParse( "export ordinal table rva 0x{x} was not within the export section", .{export_dir.ordinal_table_rva}, ); @@ -1077,12 +1171,13 @@ const coff = struct { defer gpa.free(dir_slice); const dll_name = std.mem.sliceTo(dir_slice[name_loc - dir_loc ..], 0); - try w.print( - \\ - \\Exports from {s}: - \\ Ord Hint RVA Name - \\ - , .{dll_name}); + if (d.element(.@"table-header")) + try w.print( + \\ + \\Exports from {s}: + \\ Ord Hint RVA Name + \\ + , .{dll_name}); const name_pointers = dir_slice[name_pointer_loc - dir_loc ..][0 .. export_dir.number_of_names * @sizeOf(u32)]; const ords = dir_slice[ord_loc - dir_loc ..][0 .. export_dir.number_of_names * @sizeOf(u16)]; @@ -1090,18 +1185,24 @@ const coff = struct { const name_rva_to_offset = dir.virtual_address + @sizeOf(std.coff.ExportDirectoryTable); for (0..export_dir.number_of_names) |name_i| { const name_rva = std.mem.readInt(u32, name_pointers[name_i * @sizeOf(u32) ..][0..@sizeOf(u32)], .little); + const name = std.mem.sliceTo(dir_slice[name_rva - name_rva_to_offset ..], 0); + if (!filterMatches(d.opts.symbol_filters, name)) + continue; + const ord = std.mem.readInt(u16, ords[name_i * @sizeOf(u16) ..][0..@sizeOf(u16)], .little); const addr = std.mem.readInt(u32, addrs[@as(u32, ord) * @sizeOf(u32) ..][0..@sizeOf(u32)], .little); - try w.print("{x: >4} {x: >4} ", .{ export_dir.ordinal_base + ord, name_i }); + try w.print("{f} {f} ", .{ + fmtIntField(d, @as(u16, @intCast(export_dir.ordinal_base + ord)), .{ .kind = .ord }), + fmtIntField(d, @as(u16, @intCast(name_i)), .{ .kind = .ord }), + }); const is_forwarder = addr >= dir.virtual_address and addr < dir_end_rva; if (is_forwarder) { try w.writeAll("forwards"); } else { - try w.print("{x: >8}", .{addr}); + try w.print("{f}", .{fmtIntField(d, addr, .{ .kind = .rva })}); } - const name = std.mem.sliceTo(dir_slice[name_rva - name_rva_to_offset ..], 0); try w.print(" | {s}", .{name}); if (is_forwarder) try w.print(" -> {s}", .{std.mem.sliceTo(dir_slice[addr - name_rva_to_offset ..], 0)}); @@ -1110,11 +1211,9 @@ const coff = struct { } } - if (opts.imports) { + if (d.opts.imports) { if (try seekToDataDirectory( - opts, - fr, - w, + d, rva_index, sections.items, image_info.?.data_dirs, @@ -1125,8 +1224,7 @@ const coff = struct { defer directory_entries.deinit(gpa); while (true) { const entry = r.takeStruct(Entry, .little) catch |err| - return failParse( - opts, + return d.failParse( "unable to read import directory entry {x}: {t}", .{ directory_entries.items.len, err }, ); @@ -1141,8 +1239,7 @@ const coff = struct { sections.items, entry.name_rva, ) orelse - return failParse( - opts, + return d.failParse( "import directory entry name rva 0x{x} was not found in any section", .{entry.name_rva}, ); @@ -1151,8 +1248,7 @@ const coff = struct { entry.name_rva, ) catch unreachable; fr.seekTo(name_loc) catch |err| - return failParse( - opts, + return d.failParse( "unable to seek to import directory entry name at 0x{x}: {t}", .{ name_loc, err }, ); @@ -1160,7 +1256,7 @@ const coff = struct { const dll_name = (try r.takeDelimiter(0)).?; try w.print("Import table entry for {s}:\n", .{dll_name}); - try dumpHeader(w, Entry, &entry, struct {}); + try dumpHeader(d, Entry, &entry, struct {}); try w.print( \\ @@ -1173,8 +1269,7 @@ const coff = struct { sections.items, entry.import_lookup_table_rva, ) orelse - return failParse( - opts, + return d.failParse( "import directory entry ilt rva 0x{x} was not found in any section", .{entry.import_lookup_table_rva}, ); @@ -1183,8 +1278,7 @@ const coff = struct { entry.import_lookup_table_rva, ) catch unreachable; fr.seekTo(ilt_loc) catch |err| - return failParse( - opts, + return d.failParse( "unable to seek to import directory ilt at 0x{x}: {t}", .{ ilt_loc, err }, ); @@ -1199,8 +1293,7 @@ const coff = struct { defer ilt_entries.deinit(gpa); while (true) { const table_entry = r.takeStruct(TableEntry, .little) catch |err| - return failParse( - opts, + return d.failParse( "unable to read ilt entry {s}:{x}: {t}", .{ dll_name, ilt_entries.items.len, err }, ); @@ -1217,8 +1310,7 @@ const coff = struct { sections.items, ilt_entry.payload.hint_name_rva, ) orelse - return failParse( - opts, + return d.failParse( "import directory ilt entry 0x{x}'s hint rva 0x{x} was not found in any section", .{ ilt_entry_i, ilt_entry.payload.hint_name_rva }, ); @@ -1227,22 +1319,19 @@ const coff = struct { ilt_entry.payload.hint_name_rva, ) catch unreachable; fr.seekTo(hint_loc) catch |err| - return failParse( - opts, + return d.failParse( "unable to seek to ilt entry 0x{x}'s hint at 0x{x}: {t}", .{ ilt_entry_i, hint_loc, err }, ); const hint = r.takeInt(u16, .little) catch |err| - return failParse( - opts, + return d.failParse( "unable to read import directory ilt entry 0x{x}'s hint: {t}", .{ ilt_entry_i, err }, ); const name = r.takeDelimiter(0) catch |err| - return failParse( - opts, + return d.failParse( "unable to read import directory ilt entry 0x{x}'s name: {t}", .{ ilt_entry_i, err }, ); @@ -1250,18 +1339,16 @@ const coff = struct { try w.print(" {x: >4} | {s}\n", .{ hint, name.? }); } } - try w.writeByte('\n'); + if (d.element(.newlines)) try w.writeByte('\n'); }, } } } } - if (opts.tls) { + if (d.opts.tls) { if (try seekToDataDirectory( - opts, - fr, - w, + d, rva_index, sections.items, image_info.?.data_dirs, @@ -1272,10 +1359,10 @@ const coff = struct { inline else => |m| { const TlsDirectoryEntry = std.coff.TlsDirectoryEntry(m); const tls_entry = r.takeStruct(TlsDirectoryEntry, .little) catch |err| - return failParse(opts, "unable to read tls directory: {t}", .{err}); + return d.failParse("unable to read tls directory: {t}", .{err}); try w.writeAll("TLS Directory:\n"); - try dumpHeader(w, TlsDirectoryEntry, &tls_entry, struct {}); + try dumpHeader(d, TlsDirectoryEntry, &tls_entry, struct {}); try w.writeAll(" | "); if (tls_entry.characteristics.alignment == .NONE) { @@ -1301,8 +1388,7 @@ const coff = struct { sections.items, callbacks_rva, ) orelse - return failParse( - opts, + return d.failParse( "tls callbacks rva 0x{x} was not found in any section", .{callbacks_rva}, ); @@ -1311,24 +1397,22 @@ const coff = struct { .rvaFileOffset(callbacks_rva) catch unreachable; fr.seekTo(callbacks_loc) catch |err| - return failParse( - opts, + return d.failParse( "unable to seek to tls callbacks array at offset 0x{x}: {t}", .{ callbacks_loc, err }, ); while (true) { const callback_va = r.takeInt(@FieldType(TlsDirectoryEntry, "callbacks_va"), .little) catch |err| - return failParse( - opts, + return d.failParse( "unable to read tls callbacks array: {t}", .{err}, ); - try w.print("{x: >16} \n", .{callback_va}); + try w.print("{f}\n", .{fmtIntField(d, callback_va, .{ .kind = .va })}); if (callback_va == 0) break; } - try w.writeByte('\n'); + if (d.element(.newlines)) try w.writeByte('\n'); }, } } @@ -1336,9 +1420,7 @@ const coff = struct { } fn seekToDataDirectory( - opts: *const Options, - fr: *Io.File.Reader, - w: *Io.Writer, + d: *const DumpContext, rva_index: []const u16, sections: []const Section, data_dirs: []const std.coff.ImageDataDirectory, @@ -1349,16 +1431,14 @@ const coff = struct { if (rva == 0) break :blk; const section_index = sectionContainingRva(rva_index, sections, rva) orelse - return failParse( - opts, + return d.failParse( "{t} directory rva 0x{x} was not found in any section", .{ entry, rva }, ); const file_offset = sections[section_index].rvaFileOffset(rva) catch unreachable; - fr.seekTo(file_offset) catch |err| - return failParse( - opts, + d.fr.seekTo(file_offset) catch |err| + return d.failParse( "unable to seek to {t} directory at offset 0x{x}: {t}", .{ entry, file_offset, err }, ); @@ -1366,7 +1446,7 @@ const coff = struct { return section_index; } - try w.print("{t} directory was not present in optional header\n", .{entry}); + try d.w.print("{t} directory was not present in optional header\n", .{entry}); return null; } @@ -1382,19 +1462,18 @@ const coff = struct { fn order(ctx: @This(), section_index: u16) std.math.Order { const h = &ctx.sections[section_index].header; - const start = h.virtual_address; - if (ctx.rva < start) return .lt; + if (ctx.rva < h.virtual_address) return .lt; const end = h.virtual_address + h.size_of_raw_data; if (ctx.rva >= end) return .gt; return .eq; } }; - const index = std.sort.binarySearch(u16, indices, Context{ + const indices_index = std.sort.binarySearch(u16, indices, Context{ .rva = rva, .sections = sections, }, Context.order) orelse return null; - return @intCast(index); + return @intCast(indices[indices_index]); } fn headerName(raw: *const [8]u8, string_table: []const u8) ![]const u8 { @@ -1427,6 +1506,40 @@ const coff = struct { }; } + const FormatIntField = struct { + val: ?u64, + width: usize, + zero_fill: bool, + }; + + fn fmtIntField( + d: *const DumpContext, + val: anytype, + params: struct { + kind: ?FieldKind = null, + width: ?usize = null, + zero_fill: bool = false, + }, + ) std.fmt.Alt(FormatIntField, intFieldString) { + return .{ + .data = .{ + .val = if (d.redacted(params.kind)) null else val, + .width = params.width orelse @typeInfo(@TypeOf(val)).int.bits / 4, + .zero_fill = params.zero_fill, + }, + }; + } + + fn intFieldString(field: FormatIntField, w: *std.Io.Writer) std.Io.Writer.Error!void { + if (field.val) |val| { + try w.printInt(val, 16, .lower, .{ + .width = field.width, + .alignment = .right, + .fill = if (field.zero_fill) '0' else ' ', + }); + } else try w.splatByteAll('x', field.width); + } + fn dumpFlags(w: *Io.Writer, comptime fmt: []const u8, comptime T: type, flags: *const T, cols: u32) !void { const s = @typeInfo(T).@"struct"; inline for (s.fields) |flag_field| { @@ -1437,33 +1550,53 @@ const coff = struct { } } - fn dumpArchiveHeader(w: *Io.Writer, header: *const ArchiveHeader, pos: u32) !void { - try w.print("Archive member at offset 0x{x}: '{s}'\n", .{ pos, header.name }); - try dumpHeader(w, ArchiveHeader, header, struct { - pub fn name(_: *const ArchiveHeader, _: *Io.Writer) !void {} - pub fn file_mode(h: *const ArchiveHeader, cw: *Io.Writer) !void { - try cw.print("{o: >16} file_mode\n", .{h.file_mode}); + fn dumpArchiveHeader(d: *const DumpContext, header: *const ArchiveHeader, pos: u32) !void { + try d.w.print("Archive member at offset 0x{x}: '{s}'\n", .{ pos, header.name }); + try dumpHeader(d, ArchiveHeader, header, struct { + pub fn name(_: *const DumpContext, _: *const ArchiveHeader) !void {} + pub fn file_mode(id: *const DumpContext, h: *const ArchiveHeader) !void { + try id.w.print("{o: >16} file_mode\n", .{h.file_mode}); } }); } - fn dumpHeader(w: *Io.Writer, comptime T: type, header: *const T, Custom: type) !void { + fn fieldKind(name: []const u8) ?FieldKind { + if (std.mem.endsWith(u8, name, "_rva")) + return .rva; + if (std.mem.endsWith(u8, name, "_va") or + std.mem.endsWith(u8, name, "_address") or + std.mem.startsWith(u8, name, "pointer_")) + return .va; + if (std.mem.startsWith(u8, name, "number_")) + return .size; + return null; + } + + fn dumpHeader( + d: *const DumpContext, + comptime T: type, + header: *const T, + Custom: type, + ) !void { inline for (@typeInfo(T).@"struct".fields) |field| { const val = &@field(header, field.name); if (@hasDecl(Custom, field.name)) { - try @field(Custom, field.name)(header, w); + try @field(Custom, field.name)(d, header); } else { switch (@typeInfo(field.type)) { - .int => try w.print("{x: >16} {s}\n", .{ val.*, field.name }), - .@"enum" => try w.print("{x: >16} {s} ({t})\n", .{ val.*, field.name, val.* }), + .int => try d.w.print("{f} {s}\n", .{ fmtIntField(d, val.*, .{ + .kind = comptime fieldKind(field.name), + .width = 16, + }), field.name }), + .@"enum" => try d.w.print("{x: >16} {s} ({t})\n", .{ val.*, field.name, val.* }), .@"struct" => |s| { switch (s.layout) { .auto, .@"extern", - => try dumpHeader(w, field.type, val, Custom), + => try dumpHeader(d, field.type, val, Custom), .@"packed" => { - try w.print("{x: >16} {s}\n", .{ @as(s.backing_integer.?, @bitCast(val.*)), field.name }); - try dumpFlags(w, "| {s}\n", field.type, val, 15); + try d.w.print("{x: >16} {s}\n", .{ @as(s.backing_integer.?, @bitCast(val.*)), field.name }); + try dumpFlags(d.w, "| {s}\n", field.type, val, 15); }, } }, @@ -1477,8 +1610,12 @@ const coff = struct { try w.print("{d: >13}.{x:0<2} {s}\n", .{ major, minor, name }); } - fn dumpRvaField(w: *Io.Writer, name: []const u8, rva: u64, base: u64) !void { - try w.print("{x: >16} {s} ({x})\n", .{ rva, name, base + rva }); + fn dumpRvaField(d: *const DumpContext, name: []const u8, rva: u64, base: u64) !void { + try d.w.print("{f} {s} ({f})\n", .{ + fmtIntField(d, rva, .{ .kind = .rva }), + name, + fmtIntField(d, base + rva, .{ .kind = .va }), + }); } }; @@ -1492,18 +1629,32 @@ const usage = \\Usage: zig objdump [options] file \\ \\Options: - \\ -h, --help Print this help and exit - \\ --all-headers Alias for --file-headers --member-headers --section-headers --relocs --symbols - \\ --file-headers Display file-format specific headers - \\ --imports Display imported symbols - \\ --exports Display exported symbols - \\ --linker-member[=1|2|longnames] (Coff) Display contents of the specified linker member (default 2) - \\ --member-headers Display archive member headers - \\ --only-member=[name] Only consider archive members names that contain [name]. Can be specified multiple times. - \\ --only-section=[name] Only consider section names that contain [name]. Can be specified multiple times. - \\ --relocs Display relocations - \\ --section-headers Display section headers - \\ --strings Display string tables - \\ --symbols Display symbol tables - \\ --tls Display TLS information + \\ -h, --help Print this help and exit + \\ --all-headers Alias for --file-headers --member-headers --section-headers --relocs --symbols + \\ --file-headers Display file-format specific headers + \\ --imports Display imported symbols + \\ --exports Display exported symbols + \\ --linker-member[=1|2|longnames] (Coff) Display contents of the specified archive linker member (default 2) + \\ --member-headers Display archive member headers + \\ --redact=[kind] Redact the specified field kind. Intended for snapshot testing. + \\ rva Relative virtual addresses + \\ va Virtual addresses and file offsets + \\ ord Symbol ordinals / hints + \\ size Sizes and lengths + \\ all All of the above + \\ --omit-element=[kind] Omit specific parts of the output. Intended for snapshot testing. + \\ file-type File type summary + \\ table-headers Table headers with column names + \\ header-names Name that precedes a header block + \\ newlines Newlines between output sections + \\ all All of the above + \\ --only-member=[name] Only consider archive members names that contain [name]. Can be specified multiple times. + \\ --only-section=[name] Only consider section names that contain [name]. Can be specified multiple times. + \\ --only-symbol=[name] Only consider symbol names that contain [name]. Can be specified multiple times. + \\ --relocs Display relocations + \\ -s, --snapshot Alias for --redact=all --omit-format=all + \\ --section-headers Display section headers + \\ --strings Display string tables + \\ --symbols Display symbol tables + \\ --tls Display TLS information ; diff --git a/lib/std/Build/Configuration.zig b/lib/std/Build/Configuration.zig @@ -585,6 +585,8 @@ pub const Step = extern struct { expect_stderr_match: Storage.FlagLengthPrefixedList(.flags2, .expect_stderr_match, Bytes), expect_stdout_match: Storage.FlagLengthPrefixedList(.flags2, .expect_stdout_match, Bytes), expect_term_value: Storage.FlagOptional(.flags2, .expect_term, u32), + expect_stdout_snapshot: Storage.FlagOptional(.flags2, .expect_stdout_snapshot, LazyPath.Index), + expect_stderr_snapshot: Storage.FlagOptional(.flags2, .expect_stderr_snapshot, LazyPath.Index), pub const CapturedStream = extern struct { generated_file: GeneratedFileIndex, @@ -683,7 +685,9 @@ pub const Step = extern struct { expect_stdout_match: bool, expect_term: bool, expect_term_status: ExpectTermStatus, - _: u25 = 0, + expect_stdout_snapshot: bool, + expect_stderr_snapshot: bool, + _: u23 = 0, }; }; diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig @@ -129,6 +129,8 @@ pub const StdIo = union(enum) { expect_stdout_exact: []const u8, expect_stdout_match: []const u8, expect_term: process.Child.Term, + expect_stderr_snapshot: std.Build.LazyPath, + expect_stdout_snapshot: std.Build.LazyPath, }; }; @@ -632,6 +634,13 @@ pub fn addCheck(run: *Run, new_check: StdIo.Check) void { .check => |*checks| checks.append(b.allocator, new_check) catch @panic("OOM"), else => @panic("illegal call to addCheck: conflicting helper method calls. Suggest to directly set stdio field of Run instead"), } + + switch (new_check) { + .expect_stderr_snapshot, + .expect_stdout_snapshot, + => |file| run.addFileInput(file), + else => {}, + } } pub fn captureStdErr(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPath { diff --git a/test/link.zig b/test/link.zig @@ -1,17 +1,30 @@ -pub fn addCases(cases: @import("tests.zig").LinkContext) void { - if (cases.addTestStep("static-lib-exports")) |name| { - const lib = cases.addStaticLibrary(.{ +pub fn addCases(ctx: *@import("tests.zig").LinkContext) void { + if (ctx.includeTest("exports-static")) |prefix| { + const lib = ctx.addLibrary(.static, .{ .name = "lib", - .zig_source_bytes = - \\export fn foo() void {} - \\var bar: u32 = 1234; - \\comptime { @export(&bar, .{ .name = "bar", .linkage = .strong }); } - \\const baz: u64 = 5678; - \\comptime { @export(&baz, .{ .name = "baz", .linkage = .strong }); } - , + .zig_source_file = ctx.sourcePath("exports.zig"), }); - cases.verifyObjdump(name, lib, &.{"--symbols"}, .{ .os = true }); + ctx.verifyObjdump(prefix, lib, &.{ + "-s", + "--symbols", + "--only-symbol=foo", + }, .{}); } + + if (ctx.includeTest("exports-dynamic")) |prefix| { + const lib = ctx.addLibrary(.dynamic, .{ + .name = "lib", + .zig_source_file = ctx.sourcePath("exports.zig"), + }); + ctx.verifyObjdump(prefix, lib, &.{ + "-s", + "--exports", + "--only-symbol=foo", + }, .{}); + } + + + } const std = @import("std"); diff --git a/test/link/exports.zig b/test/link/exports.zig @@ -0,0 +1,9 @@ +export fn foo_fn() void {} +var foo_var: u32 = 1234; +comptime { + @export(&foo_var, .{ .name = "foo_var", .linkage = .strong }); +} +const foo_const: u64 = 5678; +comptime { + @export(&foo_const, .{ .name = "foo_const", .linkage = .strong }); +} diff --git a/test/link/snapshots/exports-dynamic.lib.dmp b/test/link/snapshots/exports-dynamic.lib.dmp @@ -0,0 +1,14 @@ +Export directory: + 0 flags + 0 time_date_stamp + 0.00 version +xxxxxxxxxxxxxxxx name_rva + 1 ordinal_base +xxxxxxxxxxxxxxxx number_of_entries +xxxxxxxxxxxxxxxx number_of_names +xxxxxxxxxxxxxxxx export_address_table_rva +xxxxxxxxxxxxxxxx name_pointer_table_rva +xxxxxxxxxxxxxxxx ordinal_table_rva +xxxx xxxx xxxxxxxx | foo_const +xxxx xxxx xxxxxxxx | foo_fn +xxxx xxxx xxxxxxxx | foo_var diff --git a/test/link/snapshots/exports-static.lib.dmp b/test/link/snapshots/exports-static.lib.dmp @@ -0,0 +1,3 @@ +xxxx 00000000 4 NULL() EXTERNAL | foo_fn +xxxx 00000000 2 NULL EXTERNAL | foo_var +xxxx 00000008 3 NULL EXTERNAL | foo_const diff --git a/test/src/Link.zig b/test/src/Link.zig @@ -5,21 +5,29 @@ target: std.Build.ResolvedTarget, use_llvm: bool, use_lld: bool, link_libc: bool, -suffix: []const u8, test_filters: []const []const u8, +update_step: ?*Step.UpdateSourceFiles, +updated_snapshots: std.StringArrayHashMapUnmanaged(void), max_rss: usize, -pub fn addTestStep(self: *const Link, prefix: []const u8) ?[]const u8 { +pub fn includeTest(self: *const Link, prefix: []const u8) ?[]const u8 { if (for (self.test_filters) |filter| { if (std.mem.containsAtLeast(u8, prefix, 1, filter)) break false; } else self.test_filters.len > 0) return null; + return prefix; +} - return std.fmt.allocPrint(self.b.allocator, "test-{s}", .{prefix}) catch @panic("OOM"); +pub fn sourcePath(self: *const Link, sub_path: []const u8) std.Build.LazyPath { + return self.b.path(self.b.pathJoin(&.{ "test/link", sub_path })); } -pub fn addStaticLibrary(self: *const Link, overlay: OverlayOptions) *Step.Compile { +pub fn addLibrary( + self: *const Link, + linkage: std.builtin.LinkMode, + overlay: OverlayOptions, +) *Step.Compile { return self.b.addLibrary(.{ - .linkage = .static, + .linkage = linkage, .name = overlay.name, .root_module = self.createModule(overlay), .use_llvm = self.use_llvm, @@ -27,7 +35,6 @@ pub fn addStaticLibrary(self: *const Link, overlay: OverlayOptions) *Step.Compil }); } -// TODO: Use std.meta.FieldEnum on TargetQuery? const SnapshotScope = packed struct { arch: bool = false, os: bool = false, @@ -38,33 +45,44 @@ const SnapshotScope = packed struct { link_libc: bool = false, }; +/// Verify the results of a `zig objdump` call against a snapshot, which +/// contains the expected output. Snapshots alias between all build +/// configurations by default, but by specifying fields in `scope`, +/// unique snapshot names are generated for each value of that field. pub fn verifyObjdump( - self: *const Link, - name: []const u8, + self: *Link, + prefix: []const u8, compile: *Step.Compile, args: []const []const u8, scope: SnapshotScope, ) void { - const snapshot_name = self.snapshotName(name, compile.name, scope) catch @panic("OOM"); + const snapshot_name = self.snapshotName(prefix, compile.name, scope) catch @panic("OOM"); + const snapshot_sub_path = self.b.pathJoin(&.{ "test/link/snapshots/", snapshot_name }); + + // Many tests may read the same snapshot, so only use the first one to update. + // If there are differences in output, they will show up on the next test run. + if (self.update_step != null) { + const gop = self.updated_snapshots.getOrPut(self.b.allocator, snapshot_sub_path) catch @panic("OOM"); + if (gop.found_existing) return; + } + const run_step = Step.Run.create(self.b, self.b.fmt("objdump {s}", .{snapshot_name})); run_step.addArgs(&.{ self.b.graph.zig_exe, "objdump" }); run_step.addArtifactArg(compile); run_step.addArgs(args); run_step.addCheck(.{ .expect_term = .{ .exited = 0 } }); - const actual_path = run_step.captureStdOut(.{ .trim_whitespace = .none }); - const expected_path = self.b.path(self.b.pathJoin(&.{ "test/link/snapshots/", snapshot_name })); + if (self.update_step) |update_step| { + // Workaround for the build system not realizing objdump itself has changed + run_step.has_side_effects = true; - const check_step = self.b.addCheckFile(actual_path, .{ - .expected_file = .{ - .file = expected_path, - .if_missing = .fail, - // TODO: Option to do UpdateSourceFiles if not matching / missing? - // TODO: Option to output to <name>-<self.suffix>.actual.dmp file? - }, - }); + const snapshot_update_path = run_step.captureStdOut(.{}); + update_step.addCopyFileToSource(snapshot_update_path, snapshot_sub_path); + } else { + run_step.addCheck(.{ .snapshot = .{ .file = self.b.path(snapshot_sub_path) } }); + } - self.step.dependOn(&check_step.step); + self.step.dependOn(&run_step.step); } fn snapshotName( @@ -81,9 +99,9 @@ fn snapshotName( if (scope.os) try w.print("-{t}", .{self.target.result.os.tag}); if (scope.abi) try w.print("-{t}", .{self.target.result.abi}); if (scope.optimize) try w.print("-{t}", .{self.optimize}); - if (scope.use_llvm and self.use_llvm) try w.writeAll("-llvm"); - if (scope.use_lld and self.use_lld) try w.writeAll("-lld"); - if (scope.link_libc and self.link_libc) try w.writeAll("-libc"); + if (scope.use_llvm) try w.writeAll(if (self.use_llvm) "-llvm" else "-no-llvm"); + if (scope.use_lld) try w.writeAll(if (self.use_lld) "-lld" else "-no-lld"); + if (scope.link_libc) try w.writeAll(if (self.link_libc) "-libc" else "-no-libc"); try w.writeAll(".dmp"); return try snapshot_name.toOwnedSlice(); @@ -95,7 +113,7 @@ fn createModule(self: *const Link, overlay: OverlayOptions) *Build.Module { const mod = self.b.createModule(.{ .target = self.target, .optimize = self.optimize, - .root_source_file = rsf: { + .root_source_file = overlay.zig_source_file orelse rsf: { const bytes = overlay.zig_source_bytes orelse break :rsf null; const name = self.b.fmt("{s}.zig", .{overlay.name}); break :rsf write_files.add(name, bytes); @@ -148,6 +166,7 @@ const OverlayOptions = struct { objcpp_source_bytes: ?[]const u8 = null, objcpp_source_flags: []const []const u8 = &.{}, zig_source_bytes: ?[]const u8 = null, + zig_source_file: ?std.Build.LazyPath = null, pic: ?bool = null, strip: ?bool = null, }; diff --git a/test/tests.zig b/test/tests.zig @@ -3148,6 +3148,11 @@ const LinkTestOptions = struct { pub fn addLinkTests(b: *std.Build, options: LinkTestOptions) *Step { const step = b.step("test-link", "Run the linker tests"); + const update_snapshots = b.option( + bool, + "link-snapshot-update", + "Update linker test snapshots in-place instead of testing against them", + ) orelse false; for (link_targets) |link_target| { if (options.skip_non_native and !link_target.target.isNative()) continue; @@ -3168,24 +3173,34 @@ pub fn addLinkTests(b: *std.Build, options: LinkTestOptions) *Step { if (options.skip_llvm and would_use_llvm) continue; if (link_target.link_libc and target.abi == .msvc and b.graph.host.result.os.tag != .windows) continue; - link.addCases(.{ + const opt_update_step = if (update_snapshots) update: { + const update_step = Step.UpdateSourceFiles.create(b); + step.dependOn(&update_step.step); + break :update update_step; + } else null; + + var context: LinkContext = .{ .b = b, .step = step, .optimize = optimize_mode, .target = resolved_target, - .suffix = std.fmt.allocPrint(b.allocator, "{s}-{t}{s}{s}{s}", .{ - target.zigTriple(b.allocator) catch @panic("OOM"), - optimize_mode, - if (link_target.use_llvm) "-llvm" else "", - if (link_target.use_lld) "-lld" else "", - if (link_target.link_libc) "-libc" else "", - }) catch @panic("OOM"), + // .suffix = std.fmt.allocPrint(b.allocator, "{s}-{t}{s}{s}{s}", .{ + // target.zigTriple(b.allocator) catch @panic("OOM"), + // optimize_mode, + // if (link_target.use_llvm) "-llvm" else "", + // if (link_target.use_lld) "-lld" else "", + // if (link_target.link_libc) "-libc" else "", + // }) catch @panic("OOM"), .use_llvm = link_target.use_llvm, .use_lld = link_target.use_lld, .link_libc = link_target.link_libc, .test_filters = options.test_filters, + .update_step = opt_update_step, + .updated_snapshots = .empty, .max_rss = options.max_rss, - }); + }; + + link.addCases(&context); } } return step;