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(...@@ -1853,6 +1853,9 @@ pub fn resolveInputs(
1853 var ld_script_bytes: std.ArrayList(u8) = .empty;1853 var ld_script_bytes: std.ArrayList(u8) = .empty;
1854 defer ld_script_bytes.deinit(gpa);1854 defer ld_script_bytes.deinit(gpa);
18551855
1856 var archive_dedup: ArchiveDedupMap = .empty;
1857 defer archive_dedup.deinit(gpa);
1858
1856 var failed_libs: std.ArrayList(struct {1859 var failed_libs: std.ArrayList(struct {
1857 name: []const u8,1860 name: []const u8,
1858 strategy: UnresolvedInput.SearchStrategy,1861 strategy: UnresolvedInput.SearchStrategy,
...@@ -1892,6 +1895,7 @@ pub fn resolveInputs(...@@ -1892,6 +1895,7 @@ pub fn resolveInputs(
1892 resolved_inputs,1895 resolved_inputs,
1893 &checked_paths,1896 &checked_paths,
1894 &ld_script_bytes,1897 &ld_script_bytes,
1898 &archive_dedup,
1895 lib_directory,1899 lib_directory,
1896 name_query,1900 name_query,
1897 target,1901 target,
...@@ -1919,6 +1923,7 @@ pub fn resolveInputs(...@@ -1919,6 +1923,7 @@ pub fn resolveInputs(
1919 resolved_inputs,1923 resolved_inputs,
1920 &checked_paths,1924 &checked_paths,
1921 &ld_script_bytes,1925 &ld_script_bytes,
1926 &archive_dedup,
1922 lib_directory,1927 lib_directory,
1923 name_query,1928 name_query,
1924 target,1929 target,
...@@ -1947,6 +1952,7 @@ pub fn resolveInputs(...@@ -1947,6 +1952,7 @@ pub fn resolveInputs(
1947 resolved_inputs,1952 resolved_inputs,
1948 &checked_paths,1953 &checked_paths,
1949 &ld_script_bytes,1954 &ld_script_bytes,
1955 &archive_dedup,
1950 lib_directory,1956 lib_directory,
1951 name_query,1957 name_query,
1952 target,1958 target,
...@@ -1966,6 +1972,7 @@ pub fn resolveInputs(...@@ -1966,6 +1972,7 @@ pub fn resolveInputs(
1966 resolved_inputs,1972 resolved_inputs,
1967 &checked_paths,1973 &checked_paths,
1968 &ld_script_bytes,1974 &ld_script_bytes,
1975 &archive_dedup,
1969 lib_directory,1976 lib_directory,
1970 name_query,1977 name_query,
1971 target,1978 target,
...@@ -1997,6 +2004,7 @@ pub fn resolveInputs(...@@ -1997,6 +2004,7 @@ pub fn resolveInputs(
1997 unresolved_inputs,2004 unresolved_inputs,
1998 resolved_inputs,2005 resolved_inputs,
1999 &ld_script_bytes,2006 &ld_script_bytes,
2007 &archive_dedup,
2000 target,2008 target,
2001 .{2009 .{
2002 .path = Path.initCwd(an.name),2010 .path = Path.initCwd(an.name),
...@@ -2015,6 +2023,7 @@ pub fn resolveInputs(...@@ -2015,6 +2023,7 @@ pub fn resolveInputs(
2015 unresolved_inputs,2023 unresolved_inputs,
2016 resolved_inputs,2024 resolved_inputs,
2017 &ld_script_bytes,2025 &ld_script_bytes,
2026 &archive_dedup,
2018 target,2027 target,
2019 .{2028 .{
2020 .path = .{2029 .path = .{
...@@ -2043,6 +2052,7 @@ pub fn resolveInputs(...@@ -2043,6 +2052,7 @@ pub fn resolveInputs(
2043 unresolved_inputs,2052 unresolved_inputs,
2044 resolved_inputs,2053 resolved_inputs,
2045 &ld_script_bytes,2054 &ld_script_bytes,
2055 &archive_dedup,
2046 target,2056 target,
2047 pq,2057 pq,
2048 color,2058 color,
...@@ -2088,6 +2098,8 @@ fn resolveLibInput(...@@ -2088,6 +2098,8 @@ fn resolveLibInput(
2088 checked_paths: *std.ArrayList(u8),2098 checked_paths: *std.ArrayList(u8),
2089 /// Allocated via `gpa`.2099 /// Allocated via `gpa`.
2090 ld_script_bytes: *std.ArrayList(u8),2100 ld_script_bytes: *std.ArrayList(u8),
2101 /// Allocated via `gpa`.
2102 archive_dedup: *ArchiveDedupMap,
2091 lib_directory: Directory,2103 lib_directory: Directory,
2092 name_query: UnresolvedInput.NameQuery,2104 name_query: UnresolvedInput.NameQuery,
2093 target: *const std.Target,2105 target: *const std.Target,
...@@ -2095,6 +2107,7 @@ fn resolveLibInput(...@@ -2095,6 +2107,7 @@ fn resolveLibInput(
2095 color: std.zig.Color,2107 color: std.zig.Color,
2096) Allocator.Error!ResolveLibInputResult {2108) Allocator.Error!ResolveLibInputResult {
2097 try resolved_inputs.ensureUnusedCapacity(gpa, 1);2109 try resolved_inputs.ensureUnusedCapacity(gpa, 1);
2110 try archive_dedup.ensureUnusedCapacity(gpa, 1);
20982111
2099 const lib_name = name_query.name;2112 const lib_name = name_query.name;
21002113
...@@ -2110,7 +2123,7 @@ fn resolveLibInput(...@@ -2110,7 +2123,7 @@ fn resolveLibInput(
2110 else => |e| fatal("unable to search for tbd library '{f}': {s}", .{ test_path, @errorName(e) }),2123 else => |e| fatal("unable to search for tbd library '{f}': {s}", .{ test_path, @errorName(e) }),
2111 };2124 };
2112 errdefer file.close(io);2125 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);
2114 }2127 }
21152128
2116 {2129 {
...@@ -2125,7 +2138,7 @@ fn resolveLibInput(...@@ -2125,7 +2138,7 @@ fn resolveLibInput(
2125 }),2138 }),
2126 };2139 };
2127 try checked_paths.print(gpa, "\n {f}", .{test_path});2140 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, .{
2129 .path = test_path,2142 .path = test_path,
2130 .query = name_query.query,2143 .query = name_query.query,
2131 }, link_mode, color)) {2144 }, link_mode, color)) {
...@@ -2149,7 +2162,7 @@ fn resolveLibInput(...@@ -2149,7 +2162,7 @@ fn resolveLibInput(
2149 }),2162 }),
2150 };2163 };
2151 errdefer file.close(io);2164 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);
2153 }2166 }
21542167
2155 // In the case of MinGW, the main check will be .lib but we also need to2168 // In the case of MinGW, the main check will be .lib but we also need to
...@@ -2165,26 +2178,61 @@ fn resolveLibInput(...@@ -2165,26 +2178,61 @@ fn resolveLibInput(
2165 else => |e| fatal("unable to search for static library '{f}': {s}", .{ test_path, @errorName(e) }),2178 else => |e| fatal("unable to search for static library '{f}': {s}", .{ test_path, @errorName(e) }),
2166 };2179 };
2167 errdefer file.close(io);2180 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);
2169 }2182 }
21702183
2171 return .no_match;2184 return .no_match;
2172}2185}
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
2174fn finishResolveLibInput(2210fn finishResolveLibInput(
2211 io: Io,
2175 resolved_inputs: *std.ArrayList(Input),2212 resolved_inputs: *std.ArrayList(Input),
2213 archive_dedup: *ArchiveDedupMap,
2176 path: Path,2214 path: Path,
2177 file: Io.File,2215 file: Io.File,
2178 link_mode: std.lang.LinkMode,2216 link_mode: std.lang.LinkMode,
2179 query: UnresolvedInput.Query,2217 query: UnresolvedInput.Query,
2180) ResolveLibInputResult {2218) ResolveLibInputResult {
2181 switch (link_mode) {2219 switch (link_mode) {
2182 .static => resolved_inputs.appendAssumeCapacity(.{ .archive = .{2220 .static => {
2183 .path = path,2221 const ctx: ArchiveDedupAdapter = .{ .resolved_inputs = resolved_inputs.items };
2184 .file = file,2222 const gop = archive_dedup.getOrPutAssumeCapacityAdapted(path, ctx);
2185 .must_link = query.must_link,2223 if (gop.found_existing) {
2186 .hidden = query.hidden,2224 // Ignore duplicate archive input
2187 } }),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 },
2188 .dynamic => resolved_inputs.appendAssumeCapacity(.{ .dso = .{2236 .dynamic => resolved_inputs.appendAssumeCapacity(.{ .dso = .{
2189 .path = path,2237 .path = path,
2190 .file = file,2238 .file = file,
...@@ -2206,13 +2254,15 @@ fn resolvePathInput(...@@ -2206,13 +2254,15 @@ fn resolvePathInput(
2206 resolved_inputs: *std.ArrayList(Input),2254 resolved_inputs: *std.ArrayList(Input),
2207 /// Allocated via `gpa`.2255 /// Allocated via `gpa`.
2208 ld_script_bytes: *std.ArrayList(u8),2256 ld_script_bytes: *std.ArrayList(u8),
2257 /// Allocated via `gpa`.
2258 archive_dedup: *ArchiveDedupMap,
2209 target: *const std.Target,2259 target: *const std.Target,
2210 pq: UnresolvedInput.PathQuery,2260 pq: UnresolvedInput.PathQuery,
2211 color: std.zig.Color,2261 color: std.zig.Color,
2212) Allocator.Error!?ResolveLibInputResult {2262) Allocator.Error!?ResolveLibInputResult {
2213 switch (Compilation.classifyFileExt(pq.path.sub_path)) {2263 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),2264 .static_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, pq, .static, color),
2215 .shared_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, target, pq, .dynamic, color),2265 .shared_library => return try resolvePathInputLib(gpa, arena, io, unresolved_inputs, resolved_inputs, ld_script_bytes, archive_dedup, target, pq, .dynamic, color),
2216 .object => {2266 .object => {
2217 var file = pq.path.root_dir.handle.openFile(io, pq.path.sub_path, .{}) catch |err|2267 var file = pq.path.root_dir.handle.openFile(io, pq.path.sub_path, .{}) catch |err|
2218 fatal("failed to open object {f}: {s}", .{ pq.path, @errorName(err) });2268 fatal("failed to open object {f}: {s}", .{ pq.path, @errorName(err) });
...@@ -2249,12 +2299,15 @@ fn resolvePathInputLib(...@@ -2249,12 +2299,15 @@ fn resolvePathInputLib(
2249 resolved_inputs: *std.ArrayList(Input),2299 resolved_inputs: *std.ArrayList(Input),
2250 /// Allocated via `gpa`.2300 /// Allocated via `gpa`.
2251 ld_script_bytes: *std.ArrayList(u8),2301 ld_script_bytes: *std.ArrayList(u8),
2302 /// Allocated via `gpa`.
2303 archive_dedup: *ArchiveDedupMap,
2252 target: *const std.Target,2304 target: *const std.Target,
2253 pq: UnresolvedInput.PathQuery,2305 pq: UnresolvedInput.PathQuery,
2254 link_mode: std.lang.LinkMode,2306 link_mode: std.lang.LinkMode,
2255 color: std.zig.Color,2307 color: std.zig.Color,
2256) Allocator.Error!ResolveLibInputResult {2308) Allocator.Error!ResolveLibInputResult {
2257 try resolved_inputs.ensureUnusedCapacity(gpa, 1);2309 try resolved_inputs.ensureUnusedCapacity(gpa, 1);
2310 try archive_dedup.ensureUnusedCapacity(gpa, 1);
22582311
2259 const test_path: Path = pq.path;2312 const test_path: Path = pq.path;
2260 // In the case of shared libraries, they might actually be "linker scripts"2313 // In the case of shared libraries, they might actually be "linker scripts"
...@@ -2279,7 +2332,7 @@ fn resolvePathInputLib(...@@ -2279,7 +2332,7 @@ fn resolvePathInputLib(
2279 mem.startsWith(u8, buf, std.elf.ARMAG_THIN))2332 mem.startsWith(u8, buf, std.elf.ARMAG_THIN))
2280 {2333 {
2281 // Appears to be an ELF or archive file.2334 // 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);
2283 }2336 }
2284 const stat = file.stat(io) catch |err|2337 const stat = file.stat(io) catch |err|
2285 fatal("failed to stat {f}: {t}", .{ test_path, err });2338 fatal("failed to stat {f}: {t}", .{ test_path, err });
...@@ -2349,7 +2402,7 @@ fn resolvePathInputLib(...@@ -2349,7 +2402,7 @@ fn resolvePathInputLib(
2349 }),2402 }),
2350 };2403 };
2351 errdefer file.close(io);2404 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);
2353}2406}
23542407
2355pub fn openObject(io: Io, path: Path, must_link: bool, hidden: bool) !Input.Object {2408pub fn openObject(io: Io, path: Path, must_link: bool, hidden: bool) !Input.Object {