authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-05-28 12:10:05+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-05-28 12:16:58+01:00
loga7531ba928938029f2381b8ec5845c2797528b12
treef18eb67cdf316a22dbbbc726b8f53b44bee2a573
parentc698e55754748fa9e97fed4fe279c604d3b381cd
signaturelock-open Commit is signed but in an unrecognized format.

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.

1 files changed, 67 insertions(+), 14 deletions(-)

src/link.zig+67-14
......@@ -1853,6 +1853,9 @@ pub fn resolveInputs(
18531853 var ld_script_bytes: std.ArrayList(u8) = .empty;
18541854 defer ld_script_bytes.deinit(gpa);
18551855
1856 var archive_dedup: ArchiveDedupMap = .empty;
1857 defer archive_dedup.deinit(gpa);
1858
18561859 var failed_libs: std.ArrayList(struct {
18571860 name: []const u8,
18581861 strategy: UnresolvedInput.SearchStrategy,
......@@ -1892,6 +1895,7 @@ pub fn resolveInputs(
18921895 resolved_inputs,
18931896 &checked_paths,
18941897 &ld_script_bytes,
1898 &archive_dedup,
18951899 lib_directory,
18961900 name_query,
18971901 target,
......@@ -1919,6 +1923,7 @@ pub fn resolveInputs(
19191923 resolved_inputs,
19201924 &checked_paths,
19211925 &ld_script_bytes,
1926 &archive_dedup,
19221927 lib_directory,
19231928 name_query,
19241929 target,
......@@ -1947,6 +1952,7 @@ pub fn resolveInputs(
19471952 resolved_inputs,
19481953 &checked_paths,
19491954 &ld_script_bytes,
1955 &archive_dedup,
19501956 lib_directory,
19511957 name_query,
19521958 target,
......@@ -1966,6 +1972,7 @@ pub fn resolveInputs(
19661972 resolved_inputs,
19671973 &checked_paths,
19681974 &ld_script_bytes,
1975 &archive_dedup,
19691976 lib_directory,
19701977 name_query,
19711978 target,
......@@ -1997,6 +2004,7 @@ pub fn resolveInputs(
19972004 unresolved_inputs,
19982005 resolved_inputs,
19992006 &ld_script_bytes,
2007 &archive_dedup,
20002008 target,
20012009 .{
20022010 .path = Path.initCwd(an.name),
......@@ -2015,6 +2023,7 @@ pub fn resolveInputs(
20152023 unresolved_inputs,
20162024 resolved_inputs,
20172025 &ld_script_bytes,
2026 &archive_dedup,
20182027 target,
20192028 .{
20202029 .path = .{
......@@ -2043,6 +2052,7 @@ pub fn resolveInputs(
20432052 unresolved_inputs,
20442053 resolved_inputs,
20452054 &ld_script_bytes,
2055 &archive_dedup,
20462056 target,
20472057 pq,
20482058 color,
......@@ -2088,6 +2098,8 @@ fn resolveLibInput(
20882098 checked_paths: *std.ArrayList(u8),
20892099 /// Allocated via `gpa`.
20902100 ld_script_bytes: *std.ArrayList(u8),
2101 /// Allocated via `gpa`.
2102 archive_dedup: *ArchiveDedupMap,
20912103 lib_directory: Directory,
20922104 name_query: UnresolvedInput.NameQuery,
20932105 target: *const std.Target,
......@@ -2095,6 +2107,7 @@ fn resolveLibInput(
20952107 color: std.zig.Color,
20962108) Allocator.Error!ResolveLibInputResult {
20972109 try resolved_inputs.ensureUnusedCapacity(gpa, 1);
2110 try archive_dedup.ensureUnusedCapacity(gpa, 1);
20982111
20992112 const lib_name = name_query.name;
21002113
......@@ -2110,7 +2123,7 @@ fn resolveLibInput(
21102123 else => |e| fatal("unable to search for tbd library '{f}': {s}", .{ test_path, @errorName(e) }),
21112124 };
21122125 errdefer file.close(io);
2113 return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query);
2126 return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query);
21142127 }
21152128
21162129 {
......@@ -2125,7 +2138,7 @@ fn resolveLibInput(
21252138 }),
21262139 };
21272140 try checked_paths.print(gpa, "\n {f}", .{test_path});
2128 switch (try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, target, .{
2141 switch (try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, .{
21292142 .path = test_path,
21302143 .query = name_query.query,
21312144 }, link_mode, color)) {
......@@ -2149,7 +2162,7 @@ fn resolveLibInput(
21492162 }),
21502163 };
21512164 errdefer file.close(io);
2152 return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query);
2165 return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query);
21532166 }
21542167
21552168 // In the case of MinGW, the main check will be .lib but we also need to
......@@ -2165,26 +2178,61 @@ fn resolveLibInput(
21652178 else => |e| fatal("unable to search for static library '{f}': {s}", .{ test_path, @errorName(e) }),
21662179 };
21672180 errdefer file.close(io);
2168 return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query);
2181 return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query);
21692182 }
21702183
21712184 return .no_match;
21722185}
21732186
2187/// Deduplicates static archive link inputs based on their path. This is done for efficiency, so
2188/// that linker implementations do not need to open and scan the archive just to determine that they
2189/// need not extract any objects. At the time of writing, it also helps avoid "multiple definitions
2190/// of symbol" errors in incomplete linker implementations.
2191///
2192/// Key is index into `resolved_inputs` of an `Input.archive`.
2193///
2194/// Accessed through `ArchiveDedupAdapter`.
2195///
2196const ArchiveDedupMap = std.array_hash_map.Custom(u32, void, void, true);
2197/// Adapter for accessing `ArchiveDedupMap` with an effective key type of `Path`.
2198const ArchiveDedupAdapter = struct {
2199 resolved_inputs: []const Input,
2200 pub fn hash(ctx: ArchiveDedupAdapter, path: Path) u32 {
2201 _ = ctx;
2202 return Path.TableAdapter.hash(.{}, path);
2203 }
2204 pub fn eql(ctx: ArchiveDedupAdapter, a_path: Path, b_input_index: u32, _: usize) bool {
2205 const b_path = ctx.resolved_inputs[b_input_index].archive.path;
2206 return a_path.eql(b_path);
2207 }
2208};
2209
21742210fn finishResolveLibInput(
2211 io: Io,
21752212 resolved_inputs: *std.ArrayList(Input),
2213 archive_dedup: *ArchiveDedupMap,
21762214 path: Path,
21772215 file: Io.File,
21782216 link_mode: std.lang.LinkMode,
21792217 query: UnresolvedInput.Query,
21802218) ResolveLibInputResult {
21812219 switch (link_mode) {
2182 .static => resolved_inputs.appendAssumeCapacity(.{ .archive = .{
2183 .path = path,
2184 .file = file,
2185 .must_link = query.must_link,
2186 .hidden = query.hidden,
2187 } }),
2220 .static => {
2221 const ctx: ArchiveDedupAdapter = .{ .resolved_inputs = resolved_inputs.items };
2222 const gop = archive_dedup.getOrPutAssumeCapacityAdapted(path, ctx);
2223 if (gop.found_existing) {
2224 // Ignore duplicate archive input
2225 file.close(io);
2226 return .ok;
2227 }
2228 gop.key_ptr.* = @intCast(resolved_inputs.items.len);
2229 resolved_inputs.appendAssumeCapacity(.{ .archive = .{
2230 .path = path,
2231 .file = file,
2232 .must_link = query.must_link,
2233 .hidden = query.hidden,
2234 } });
2235 },
21882236 .dynamic => resolved_inputs.appendAssumeCapacity(.{ .dso = .{
21892237 .path = path,
21902238 .file = file,
......@@ -2206,13 +2254,15 @@ fn resolvePathInput(
22062254 resolved_inputs: *std.ArrayList(Input),
22072255 /// Allocated via `gpa`.
22082256 ld_script_bytes: *std.ArrayList(u8),
2257 /// Allocated via `gpa`.
2258 archive_dedup: *ArchiveDedupMap,
22092259 target: *const std.Target,
22102260 pq: UnresolvedInput.PathQuery,
22112261 color: std.zig.Color,
22122262) Allocator.Error!?ResolveLibInputResult {
22132263 switch (Compilation.classifyFileExt(pq.path.sub_path)) {
2214 .static_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, target, pq, .static, color),
2215 .shared_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, target, pq, .dynamic, color),
2264 .static_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, pq, .static, color),
2265 .shared_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, pq, .dynamic, color),
22162266 .object => {
22172267 var file = pq.path.root_dir.handle.openFile(io, pq.path.sub_path, .{}) catch |err|
22182268 fatal("failed to open object {f}: {s}", .{ pq.path, @errorName(err) });
......@@ -2249,12 +2299,15 @@ fn resolvePathInputLib(
22492299 resolved_inputs: *std.ArrayList(Input),
22502300 /// Allocated via `gpa`.
22512301 ld_script_bytes: *std.ArrayList(u8),
2302 /// Allocated via `gpa`.
2303 archive_dedup: *ArchiveDedupMap,
22522304 target: *const std.Target,
22532305 pq: UnresolvedInput.PathQuery,
22542306 link_mode: std.lang.LinkMode,
22552307 color: std.zig.Color,
22562308) Allocator.Error!ResolveLibInputResult {
22572309 try resolved_inputs.ensureUnusedCapacity(gpa, 1);
2310 try archive_dedup.ensureUnusedCapacity(gpa, 1);
22582311
22592312 const test_path: Path = pq.path;
22602313 // In the case of shared libraries, they might actually be "linker scripts"
......@@ -2279,7 +2332,7 @@ fn resolvePathInputLib(
22792332 mem.startsWith(u8, buf, std.elf.ARMAG_THIN))
22802333 {
22812334 // Appears to be an ELF or archive file.
2282 return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, pq.query);
2335 return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, pq.query);
22832336 }
22842337 const stat = file.stat(io) catch |err|
22852338 fatal("failed to stat {f}: {t}", .{ test_path, err });
......@@ -2349,7 +2402,7 @@ fn resolvePathInputLib(
23492402 }),
23502403 };
23512404 errdefer file.close(io);
2352 return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, pq.query);
2405 return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, pq.query);
23532406}
23542407
23552408pub fn openObject(io: Io, path: Path, must_link: bool, hidden: bool) !Input.Object {