From a7531ba928938029f2381b8ec5845c2797528b12 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Thu, 28 May 2026 12:10:05 +0100 Subject: [PATCH] link: deduplicate static archive inputs This has the benefit of preventing redundant work in linker implementations, but it also helps to prevent incomplete linker implementations (currently Elf2) from emitting "multiple definitions of symbol" errors when the same static library is passed on the command line multiple times. --- src/link.zig | 81 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/src/link.zig b/src/link.zig index 728d2d4f245d83902ef319881b7db128fbf1e85f..8759d2810e55da1e53094fa429efdbdcc2825912 100644 --- a/src/link.zig +++ b/src/link.zig @@ -1853,6 +1853,9 @@ pub fn resolveInputs( var ld_script_bytes: std.ArrayList(u8) = .empty; defer ld_script_bytes.deinit(gpa); + var archive_dedup: ArchiveDedupMap = .empty; + defer archive_dedup.deinit(gpa); + var failed_libs: std.ArrayList(struct { name: []const u8, strategy: UnresolvedInput.SearchStrategy, @@ -1892,6 +1895,7 @@ pub fn resolveInputs( resolved_inputs, &checked_paths, &ld_script_bytes, + &archive_dedup, lib_directory, name_query, target, @@ -1919,6 +1923,7 @@ pub fn resolveInputs( resolved_inputs, &checked_paths, &ld_script_bytes, + &archive_dedup, lib_directory, name_query, target, @@ -1947,6 +1952,7 @@ pub fn resolveInputs( resolved_inputs, &checked_paths, &ld_script_bytes, + &archive_dedup, lib_directory, name_query, target, @@ -1966,6 +1972,7 @@ pub fn resolveInputs( resolved_inputs, &checked_paths, &ld_script_bytes, + &archive_dedup, lib_directory, name_query, target, @@ -1997,6 +2004,7 @@ pub fn resolveInputs( unresolved_inputs, resolved_inputs, &ld_script_bytes, + &archive_dedup, target, .{ .path = Path.initCwd(an.name), @@ -2015,6 +2023,7 @@ pub fn resolveInputs( unresolved_inputs, resolved_inputs, &ld_script_bytes, + &archive_dedup, target, .{ .path = .{ @@ -2043,6 +2052,7 @@ pub fn resolveInputs( unresolved_inputs, resolved_inputs, &ld_script_bytes, + &archive_dedup, target, pq, color, @@ -2088,6 +2098,8 @@ fn resolveLibInput( checked_paths: *std.ArrayList(u8), /// Allocated via `gpa`. ld_script_bytes: *std.ArrayList(u8), + /// Allocated via `gpa`. + archive_dedup: *ArchiveDedupMap, lib_directory: Directory, name_query: UnresolvedInput.NameQuery, target: *const std.Target, @@ -2095,6 +2107,7 @@ fn resolveLibInput( color: std.zig.Color, ) Allocator.Error!ResolveLibInputResult { try resolved_inputs.ensureUnusedCapacity(gpa, 1); + try archive_dedup.ensureUnusedCapacity(gpa, 1); const lib_name = name_query.name; @@ -2110,7 +2123,7 @@ fn resolveLibInput( else => |e| fatal("unable to search for tbd library '{f}': {s}", .{ test_path, @errorName(e) }), }; errdefer file.close(io); - return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query); + return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query); } { @@ -2125,7 +2138,7 @@ fn resolveLibInput( }), }; try checked_paths.print(gpa, "\n {f}", .{test_path}); - switch (try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, target, .{ + switch (try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, .{ .path = test_path, .query = name_query.query, }, link_mode, color)) { @@ -2149,7 +2162,7 @@ fn resolveLibInput( }), }; errdefer file.close(io); - return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query); + return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query); } // In the case of MinGW, the main check will be .lib but we also need to @@ -2165,26 +2178,61 @@ fn resolveLibInput( else => |e| fatal("unable to search for static library '{f}': {s}", .{ test_path, @errorName(e) }), }; errdefer file.close(io); - return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query); + return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query); } return .no_match; } +/// Deduplicates static archive link inputs based on their path. This is done for efficiency, so +/// that linker implementations do not need to open and scan the archive just to determine that they +/// need not extract any objects. At the time of writing, it also helps avoid "multiple definitions +/// of symbol" errors in incomplete linker implementations. +/// +/// Key is index into `resolved_inputs` of an `Input.archive`. +/// +/// Accessed through `ArchiveDedupAdapter`. +/// +const ArchiveDedupMap = std.array_hash_map.Custom(u32, void, void, true); +/// Adapter for accessing `ArchiveDedupMap` with an effective key type of `Path`. +const ArchiveDedupAdapter = struct { + resolved_inputs: []const Input, + pub fn hash(ctx: ArchiveDedupAdapter, path: Path) u32 { + _ = ctx; + return Path.TableAdapter.hash(.{}, path); + } + pub fn eql(ctx: ArchiveDedupAdapter, a_path: Path, b_input_index: u32, _: usize) bool { + const b_path = ctx.resolved_inputs[b_input_index].archive.path; + return a_path.eql(b_path); + } +}; + fn finishResolveLibInput( + io: Io, resolved_inputs: *std.ArrayList(Input), + archive_dedup: *ArchiveDedupMap, path: Path, file: Io.File, link_mode: std.lang.LinkMode, query: UnresolvedInput.Query, ) ResolveLibInputResult { switch (link_mode) { - .static => resolved_inputs.appendAssumeCapacity(.{ .archive = .{ - .path = path, - .file = file, - .must_link = query.must_link, - .hidden = query.hidden, - } }), + .static => { + const ctx: ArchiveDedupAdapter = .{ .resolved_inputs = resolved_inputs.items }; + const gop = archive_dedup.getOrPutAssumeCapacityAdapted(path, ctx); + if (gop.found_existing) { + // Ignore duplicate archive input + file.close(io); + return .ok; + } + gop.key_ptr.* = @intCast(resolved_inputs.items.len); + resolved_inputs.appendAssumeCapacity(.{ .archive = .{ + .path = path, + .file = file, + .must_link = query.must_link, + .hidden = query.hidden, + } }); + }, .dynamic => resolved_inputs.appendAssumeCapacity(.{ .dso = .{ .path = path, .file = file, @@ -2206,13 +2254,15 @@ fn resolvePathInput( resolved_inputs: *std.ArrayList(Input), /// Allocated via `gpa`. ld_script_bytes: *std.ArrayList(u8), + /// Allocated via `gpa`. + archive_dedup: *ArchiveDedupMap, target: *const std.Target, pq: UnresolvedInput.PathQuery, color: std.zig.Color, ) Allocator.Error!?ResolveLibInputResult { switch (Compilation.classifyFileExt(pq.path.sub_path)) { - .static_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, target, pq, .static, color), - .shared_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, target, pq, .dynamic, color), + .static_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, pq, .static, color), + .shared_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, pq, .dynamic, color), .object => { var file = pq.path.root_dir.handle.openFile(io, pq.path.sub_path, .{}) catch |err| fatal("failed to open object {f}: {s}", .{ pq.path, @errorName(err) }); @@ -2249,12 +2299,15 @@ fn resolvePathInputLib( resolved_inputs: *std.ArrayList(Input), /// Allocated via `gpa`. ld_script_bytes: *std.ArrayList(u8), + /// Allocated via `gpa`. + archive_dedup: *ArchiveDedupMap, target: *const std.Target, pq: UnresolvedInput.PathQuery, link_mode: std.lang.LinkMode, color: std.zig.Color, ) Allocator.Error!ResolveLibInputResult { try resolved_inputs.ensureUnusedCapacity(gpa, 1); + try archive_dedup.ensureUnusedCapacity(gpa, 1); const test_path: Path = pq.path; // In the case of shared libraries, they might actually be "linker scripts" @@ -2279,7 +2332,7 @@ fn resolvePathInputLib( mem.startsWith(u8, buf, std.elf.ARMAG_THIN)) { // Appears to be an ELF or archive file. - return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, pq.query); + return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, pq.query); } const stat = file.stat(io) catch |err| fatal("failed to stat {f}: {t}", .{ test_path, err }); @@ -2349,7 +2402,7 @@ fn resolvePathInputLib( }), }; errdefer file.close(io); - return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, pq.query); + return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, pq.query); } pub fn openObject(io: Io, path: Path, must_link: bool, hidden: bool) !Input.Object { -- 2.54.0