From ecd95943a9a174284a5e90f208df4d508aafd098 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 20 Sep 2026 18:53:30 -0700 Subject: [PATCH 1/4] link frontend: different semantics for colon prefixed -l args --- src/link.zig | 61 +++++++++++++++++++++++++++------------------------- src/main.zig | 28 +++++++++++++++--------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/link.zig b/src/link.zig index 8dc5988ee05b247bede01e8e303d1ae9429a9e2c..10c93ce3ad75a6e9796c4a4a9a8edb2104322ad5 100644 --- a/src/link.zig +++ b/src/link.zig @@ -995,11 +995,7 @@ pub const File = struct { .{}, ) catch |err| { const diags = &base.comp.link_diags; - return diags.fail("failed to copy '{f}' to '{f}': {t}", .{ - std.fmt.alt(@as(Path, cached_pp_file_path), .formatEscapeChar), - std.fmt.alt(@as(Path, emit), .formatEscapeChar), - err, - }); + return diags.fail("failed to copy {qf} to {qf}: {t}", .{ cached_pp_file_path, emit, err }); }; return; } @@ -1740,7 +1736,7 @@ pub fn doZcuTask(comp: *Compilation, tid: Zcu.PerThread.Id, task: ZcuTask) void if (comp.bin_file) |lf| { lf.updateLineNumber(pt, line_update.inst, line_update.line) catch |err| switch (err) { error.OutOfMemory => diags.setAllocFailure(), - else => |e| log.err("update line number failed: {s}", .{@errorName(e)}), + else => |e| log.err("update line number failed: {t}", .{e}), }; } } @@ -1753,7 +1749,7 @@ pub fn doZcuTask(comp: *Compilation, tid: Zcu.PerThread.Id, task: ZcuTask) void if (comp.bin_file) |lf| { lf.lostTracking(pt, ti) catch |err| switch (err) { error.OutOfMemory => diags.setAllocFailure(), - else => |e| log.err("lost tracking failed: {s}", .{@errorName(e)}), + else => |e| log.err("lost tracking failed: {t}", .{e}), }; } } @@ -1845,6 +1841,14 @@ pub const UnresolvedInput = union(enum) { must_link: bool = false, hidden: bool = false, allow_so_scripts: bool = false, + // Corresponds to GNU ld `-l :path/to/filename`: + // * No extra rpaths. + // * NEEDED entry should be exactly the string + // after the colon. No file system paths prepended. + // * The DSO still must be found at compile/link + // time and its entries used to resolve symbols. + // CGo compilation depends on this. + name_done: bool = false, preferred_mode: std.lang.LinkMode, search_strategy: SearchStrategy, @@ -2201,8 +2205,8 @@ pub fn resolveInputs( if (failed_libs.items.len > 0) { for (failed_libs.items) |f| { const searched_paths = if (f.checked_paths.len == 0) " none" else f.checked_paths; - std.log.err("unable to find {s} system library '{s}' using strategy '{s}'. searched paths:{s}", .{ - @tagName(f.preferred_mode), f.name, @tagName(f.strategy), searched_paths, + std.log.err("unable to find {t} system library {q} using strategy {t}. searched paths:{s}", .{ + f.preferred_mode, f.name, f.strategy, searched_paths, }); } std.process.exit(1); @@ -2237,7 +2241,7 @@ fn resolveLibInput( const lib_name = name_query.name; - if (target.os.tag.isDarwin() and link_mode == .dynamic) tbd: { + if (target.os.tag.isDarwin() and link_mode == .dynamic and !name_query.query.name_done) tbd: { // Prefer .tbd over .dylib. const test_path: Path = .{ .root_dir = lib_directory, @@ -2246,7 +2250,7 @@ fn resolveLibInput( try checked_paths.print(gpa, "\n {f}", .{test_path}); var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { error.FileNotFound => break :tbd, - else => |e| fatal("unable to search for tbd library '{f}': {s}", .{ test_path, @errorName(e) }), + else => |e| fatal("searching for tbd library {qf}: {t}", .{ test_path, e }), }; errdefer file.close(io); return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query); @@ -2255,8 +2259,9 @@ fn resolveLibInput( { const test_path: Path = .{ .root_dir = lib_directory, - .sub_path = try std.fmt.allocPrint(arena, "{s}{s}{s}", .{ - target.libPrefix(), lib_name, + .sub_path = if (name_query.query.name_done) lib_name else try std.fmt.allocPrint(arena, "{s}{s}{s}", .{ + target.libPrefix(), + lib_name, switch (link_mode) { .static => target.staticLibSuffix(), .dynamic => target.dynamicLibSuffix(), @@ -2275,7 +2280,7 @@ fn resolveLibInput( // In the case of Darwin, the main check will be .dylib, so here we // additionally check for .so files. - if (target.os.tag.isDarwin() and link_mode == .dynamic) so: { + if (target.os.tag.isDarwin() and link_mode == .dynamic and !name_query.query.name_done) so: { const test_path: Path = .{ .root_dir = lib_directory, .sub_path = try std.fmt.allocPrint(arena, "lib{s}.so", .{lib_name}), @@ -2283,9 +2288,7 @@ fn resolveLibInput( try checked_paths.print(gpa, "\n {f}", .{test_path}); var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { error.FileNotFound => break :so, - else => |e| fatal("unable to search for so library '{f}': {s}", .{ - test_path, @errorName(e), - }), + else => |e| fatal("unable to search for so library {qf}: {t}", .{ test_path, e }), }; errdefer file.close(io); return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query); @@ -2293,7 +2296,7 @@ fn resolveLibInput( // In the case of MinGW, the main check will be .lib but we also need to // look for `libfoo.a`. - if (target.isMinGW() and link_mode == .static) mingw: { + if (target.isMinGW() and link_mode == .static and !name_query.query.name_done) mingw: { const test_path: Path = .{ .root_dir = lib_directory, .sub_path = try std.fmt.allocPrint(arena, "lib{s}.a", .{lib_name}), @@ -2301,7 +2304,7 @@ fn resolveLibInput( try checked_paths.print(gpa, "\n {f}", .{test_path}); var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { error.FileNotFound => break :mingw, - else => |e| fatal("unable to search for static library '{f}': {s}", .{ test_path, @errorName(e) }), + else => |e| fatal("unable to search for static library {qf}: {t}", .{ test_path, e }), }; errdefer file.close(io); return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query); @@ -2310,12 +2313,14 @@ fn resolveLibInput( // In the case of OpenBSD, dynamic libraries are always versioned, without // unversioned symlinks. OpenBSD patches LLD to select the highest-versioned // shared library, and this code is intended to match that upstream behavior. - if (target.isOpenBSDLibC() and link_mode == .dynamic) versioned: { + if (target.isOpenBSDLibC() and link_mode == .dynamic and !name_query.query.name_done) versioned: { const prefix = try std.fmt.allocPrint(arena, "lib{s}.so.", .{lib_name}); var dir = lib_directory.handle.openDir(io, ".", .{ .iterate = true }) catch |err| switch (err) { error.NotDir, error.FileNotFound => break :versioned, - else => |e| fatal("unable to search for shared library '{s}.*': {s}", .{ prefix, @errorName(e) }), + else => |e| fatal("unable to search for shared library \"{f}.*\": {t}", .{ + std.zig.fmtString(prefix), e, + }), }; defer dir.close(io); @@ -2325,7 +2330,7 @@ fn resolveLibInput( var iter = dir.iterate(); while (iter.next(io) catch |err| { - fatal("unable to scan library directory '{s}'", .{@errorName(err)}); + fatal("scanning library directory: {t}", .{err}); }) |entry| { if (entry.kind != .file) continue; if (!std.mem.startsWith(u8, entry.name, prefix)) continue; @@ -2445,7 +2450,7 @@ fn resolvePathInput( .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) }); + fatal("failed to open object {f}: {t}", .{ pq.path, err }); errdefer file.close(io); try resolved_inputs.append(gpa, .{ .object = .{ .path = pq.path, @@ -2457,7 +2462,7 @@ fn resolvePathInput( }, .res => { var file = pq.path.root_dir.handle.openFile(io, pq.path.sub_path, .{}) catch |err| - fatal("failed to open windows resource {f}: {s}", .{ pq.path, @errorName(err) }); + fatal("failed to open windows resource {f}: {t}", .{ pq.path, err }); errdefer file.close(io); try resolved_inputs.append(gpa, .{ .res = .{ .path = pq.path, @@ -2498,14 +2503,14 @@ fn resolvePathInputLib( }) { var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { error.FileNotFound => return .no_match, - else => |e| fatal("unable to search for {t} library '{f}': {t}", .{ + else => |e| fatal("unable to search for {t} library {qf}: {t}", .{ link_mode, std.fmt.alt(test_path, .formatEscapeChar), e, }), }; errdefer file.close(io); try ld_script_bytes.resize(gpa, @max(std.elf.MAGIC.len, std.elf.ARMAG.len)); const n = file.readPositionalAll(io, ld_script_bytes.items, 0) catch |err| - fatal("failed to read '{f}': {t}", .{ std.fmt.alt(test_path, .formatEscapeChar), err }); + fatal("failed to read {qf}: {t}", .{ test_path, err }); const buf = ld_script_bytes.items[0..n]; if (mem.startsWith(u8, buf, std.elf.MAGIC) or mem.startsWith(u8, buf, std.elf.ARMAG) or @@ -2577,9 +2582,7 @@ fn resolvePathInputLib( var file = test_path.root_dir.handle.openFile(io, test_path.sub_path, .{}) catch |err| switch (err) { error.FileNotFound => return .no_match, - else => |e| fatal("unable to search for {s} library {f}: {s}", .{ - @tagName(link_mode), test_path, @errorName(e), - }), + else => |e| fatal("unable to search for {t} library {f}: {t}", .{ link_mode, test_path, e }), }; errdefer file.close(io); return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, pq.query); diff --git a/src/main.zig b/src/main.zig index 5323ea936fa88c1a611ef02d69f5855495e92b73..bc2354b3a8c3755e7efb1d4ed1afe3e77a38cf2e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -70,10 +70,6 @@ var stdin_buffer: [4096]u8 align(std.heap.page_size_min) = undefined; /// This can be global since stdout is a singleton. var stdout_buffer: [4096]u8 align(std.heap.page_size_min) = undefined; -/// Shaming all the locations that inappropriately use an O(N) search algorithm. -/// Please delete this and fix the compilation errors! -pub const @"bad O(N)" = void; - const normal_usage = \\Usage: zig [command] [options] \\ @@ -2083,12 +2079,24 @@ fn buildOutputType( // We don't know whether this library is part of libc or libc++ until // we resolve the target, so we simply append to the list for now. if (mem.startsWith(u8, it.only_arg, ":")) { - // -l :path/to/filename is used when callers need - // more control over what's in the resulting - // binary: no extra rpaths and DSO filename exactly - // as provided. CGo compilation depends on this. - try create_module.cli_link_inputs.append(arena, .{ .dso_exact = .{ - .name = it.only_arg, + // -l :path/to/filename indicates: + // * No extra rpaths. + // * NEEDED entry should be exactly the string + // after the colon. No file system paths prepended. + // * The DSO still must be found at compile/link + // time and its entries used to resolve symbols. + // CGo compilation depends on this. + try create_module.cli_link_inputs.append(arena, .{ .name_query = .{ + .name = it.only_arg[1..], + .query = .{ + .must_link = must_link, + .needed = needed, + .weak = false, + .preferred_mode = lib_preferred_mode, + .search_strategy = lib_search_strategy, + .allow_so_scripts = allow_so_scripts, + .name_done = true, + }, } }); } else { const compiler_rt_classification = target_util.classifyCompilerRtLibName(it.only_arg); -- 2.54.0 From cf33f1003fd02abaf91078725d9c972dd51fc5e1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 21 Sep 2026 00:22:22 -0700 Subject: [PATCH 2/4] link frontend: remove dso_exact, add exact_name to dso linker still wants the path to the shared object, but now also accepts an exact name to put into the NEEDED entry. closes #31823 --- src/Compilation.zig | 5 ++-- src/link.zig | 48 +++++++++++++++------------------- src/link/Coff.zig | 3 +-- src/link/Elf.zig | 4 +-- src/link/Elf2.zig | 31 +++++++++------------- src/link/Lld.zig | 36 +++++++++++++++---------- src/link/MachO.zig | 34 +++++++++++++++--------- src/link/MachO/relocatable.zig | 6 ++--- src/link/Wasm.zig | 2 -- src/main.zig | 4 +-- 10 files changed, 87 insertions(+), 86 deletions(-) diff --git a/src/Compilation.zig b/src/Compilation.zig index e7da1d5a790a8ac1c6a5e3f8edaab6341b7b77aa..b07db94185836ffcffb95cbf7a3286c947439820 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -2891,12 +2891,13 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) UpdateE } } - for (comp.link_inputs) |input| if (input.path()) |path| { + for (comp.link_inputs) |input| { + const path = input.path(); try comp.appendFileSystemInput(try .fromUnresolved(arena, comp.dirs, &.{ path.root_dir.path orelse ".", path.sub_path, })); - }; + } // For compiling Win32 resources, we rely on the cache hash system to avoid duplicating work. // Add a Job for each Win32 resource file. diff --git a/src/link.zig b/src/link.zig index 10c93ce3ad75a6e9796c4a4a9a8edb2104322ad5..e913efa77ae4c3e8113d07ff6cf81b79e098bdb8 100644 --- a/src/link.zig +++ b/src/link.zig @@ -1506,7 +1506,6 @@ pub fn doPrelinkTask(comp: *Compilation, task: PrelinkTask) void { .object => |obj| diags.addParseError(obj.path, "failed to parse object: {t}", .{e}), .archive => |obj| diags.addParseError(obj.path, "failed to parse archive: {t}", .{e}), .res => |res| diags.addParseError(res.path, "failed to parse Windows resource: {t}", .{e}), - .dso_exact => diags.addError("failed to handle dso_exact: {t}", .{e}), }, }; prog_node.completeOne(); @@ -1821,8 +1820,6 @@ pub const UnresolvedInput = union(enum) { /// Strings that come from GNU ld scripts. Is it a filename? Is it a path? /// Who knows! Fuck around and find out. ambiguous_name: NameQuery, - /// Put exactly this string in the dynamic section, no rpath. - dso_exact: Input.DsoExact, pub const NameQuery = struct { name: []const u8, @@ -1875,7 +1872,6 @@ pub const Input = union(enum) { /// May not be a GNU ld script. Those are resolved when converting from /// `UnresolvedInput` to `Input` values. dso: Dso, - dso_exact: DsoExact, pub const Object = struct { path: Path, @@ -1895,6 +1891,9 @@ pub const Input = union(enum) { needed: bool, weak: bool, reexport: bool, + /// Does not include any ":" prefix. Indicates that this string, exactly, should go into the NEEDED + /// entry, without affecting any rpaths. + exact_name: ?[]const u8, }; pub const DsoExact = struct { @@ -1903,21 +1902,17 @@ pub const Input = union(enum) { name: []const u8, }; - /// Returns `null` in the case of `dso_exact`. - pub fn path(input: Input) ?Path { + pub fn path(input: Input) Path { return switch (input) { .object, .archive => |obj| obj.path, inline .res, .dso => |x| x.path, - .dso_exact => null, }; } - /// Returns `null` in the case of `dso_exact`. - pub fn pathAndFile(input: Input) ?struct { Path, Io.File } { + pub fn pathAndFile(input: Input) struct { Path, Io.File } { return switch (input) { .object, .archive => |obj| .{ obj.path, obj.file }, inline .res, .dso => |x| .{ x.path, x.file }, - .dso_exact => null, }; } @@ -1925,7 +1920,6 @@ pub const Input = union(enum) { return switch (input) { .object, .archive => |obj| obj.path.basename(), inline .res, .dso => |x| x.path.basename(), - .dso_exact => "dso_exact", }; } }; @@ -1956,9 +1950,7 @@ pub fn hashInputs(man: *Cache.Manifest, link_inputs: []const Input) !void { man.hash.add(dso.needed); man.hash.add(dso.weak); man.hash.add(dso.reexport); - }, - .dso_exact => |dso_exact| { - man.hash.addBytes(dso_exact.name); + man.hash.addOptionalBytes(dso.exact_name); }, } } @@ -2194,10 +2186,6 @@ pub fn resolveInputs( } continue; }, - .dso_exact => |dso_exact| { - try resolved_inputs.append(gpa, .{ .dso_exact = dso_exact }); - continue; - }, } comptime unreachable; } @@ -2418,13 +2406,18 @@ fn finishResolveLibInput( .hidden = query.hidden, } }); }, - .dynamic => resolved_inputs.appendAssumeCapacity(.{ .dso = .{ - .path = path, - .file = file, - .needed = query.needed, - .weak = query.weak, - .reexport = query.reexport, - } }), + .dynamic => resolved_inputs.appendAssumeCapacity(.{ + .dso = .{ + .path = path, + .file = file, + .needed = query.needed, + .weak = query.weak, + .reexport = query.reexport, + // Here we assume that `Path.sub_path` is usable as `exact_name`, however the code after this point + // has no business making that assumption. + .exact_name = if (query.name_done) path.sub_path else null, + }, + }), } return .ok; } @@ -2608,6 +2601,7 @@ pub fn openDso(io: Io, path: Path, needed: bool, weak: bool, reexport: bool) !In .needed = needed, .weak = weak, .reexport = reexport, + .exact_name = null, }; } @@ -2639,7 +2633,7 @@ pub fn anyObjectInputs(inputs: []const Input) bool { pub fn countObjectInputs(inputs: []const Input) usize { var count: usize = 0; for (inputs) |input| switch (input) { - .dso, .dso_exact => continue, + .dso => continue, .res, .object, .archive => count += 1, }; return count; @@ -2649,7 +2643,7 @@ pub fn countObjectInputs(inputs: []const Input) usize { pub fn firstObjectInput(inputs: []const Input) ?Input.Object { for (inputs) |input| switch (input) { .object, .archive => |obj| return obj, - .res, .dso, .dso_exact => continue, + .res, .dso => continue, }; return null; } diff --git a/src/link/Coff.zig b/src/link/Coff.zig index d8934d79d89cff328b413aaec7ef173be957adff..250dbd388658148ff196567923b5e0de593f72eb 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -3905,7 +3905,7 @@ pub fn loadInput(coff: *Coff, input: link.Input) link.Error!void { const comp = coff.base.comp; const io = comp.io; - const path = input.path() orelse unreachable; + const path = input.path(); const gop = try coff.inputs.getOrPut(comp.gpa, path); if (gop.found_existing) return; errdefer _ = coff.inputs.swapRemove(path); @@ -3935,7 +3935,6 @@ pub fn loadInput(coff: *Coff, input: link.Input) link.Error!void { coff.loadDll(dso.path, &fr) catch |err| return coff.failLoadInput(err, &fr, dso.path); }, - .dso_exact => unreachable, } } diff --git a/src/link/Elf.zig b/src/link/Elf.zig index f04d7582bd6dac07669861f0d920c86efa6d6d1c..4c9a91d25ebcac10fd8186f33b2e22d5ad6ed44f 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -722,7 +722,6 @@ pub fn loadInput(self: *Elf, input: link.Input) !void { const argv = &self.dump_argv_list; switch (input) { .res => unreachable, - .dso_exact => |dso_exact| try argv.appendSlice(gpa, &.{ "-l", dso_exact.name }), .object, .archive => |obj| try argv.append(gpa, try obj.path.toString(comp.arena)), .dso => |dso| try argv.append(gpa, try dso.path.toString(comp.arena)), } @@ -730,7 +729,6 @@ pub fn loadInput(self: *Elf, input: link.Input) !void { switch (input) { .res => unreachable, - .dso_exact => @panic("TODO"), .object => |obj| try parseObject(self, obj), .archive => |obj| if (self.base.isStaticLib()) { // Ignore static library inputs when generating a static library. @@ -1120,6 +1118,8 @@ fn parseDso( const handle = dso.file; + if (dso.exact_name != null) @panic("TODO"); + const stat: Stat = .init(try handle.stat(io)); var header = try SharedObject.parseHeader(gpa, io, diags, dso.path, handle, stat, target); defer header.deinit(gpa); diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 569df037df4e861707959ff914ea74e3bb2d0b08..155f5aeb18a74f6a55543eeb8dd53fd25f240af9 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -6228,36 +6228,29 @@ fn loadInputInner(elf: *Elf, input: link.Input) (Error || error{BadMagic})!void }, .res => unreachable, .dso => |dso| { + if (dso.exact_name) |exact_name| { + // This means the path field is still valid, but the NEEDED entry must be set to + // exactly the `exact_name` string. + std.debug.panic("load dso_exact {q}", .{exact_name}); + if (elf.shndx.dynamic != .UNDEF) { + try elf.needed.put(elf.base.comp.gpa, try elf.string(.dynstr, exact_name), {}); + } + } try elf.needed.ensureUnusedCapacity(elf.base.comp.gpa, 1); var fr = dso.file.reader(io, &buf); elf.loadDso(dso.path, &fr) catch |err| switch (err) { else => |e| return e, - error.EndOfStream => return diags.failParse( - dso.path, - "unexpected eof", - .{}, - ), + error.EndOfStream => return diags.failParse(dso.path, "unexpected eof", .{}), error.AccessDenied, error.Unexpected, error.Unseekable => |e| return diags.fail( - "failed to read \"{f}\": {t}", - .{ dso.path.fmtEscapeString(), e }, + "failed to read {qf}: {t}", + .{ dso.path, e }, ), error.ReadFailed => switch (fr.err.?) { error.Canceled => |e| return e, - else => |e| return diags.fail( - "failed to read \"{f}\": {t}", - .{ dso.path.fmtEscapeString(), e }, - ), + else => |e| return diags.fail("failed to read {qf}: {t}", .{ dso.path, e }), }, }; }, - .dso_exact => |dso_exact| { - log.debug("load dso_exact '{f}'", .{std.zig.fmtString(dso_exact.name)}); - if (elf.shndx.dynamic != .UNDEF) { - try elf.needed.put(elf.base.comp.gpa, try elf.string(.dynstr, dso_exact.name), {}); - } - // TODO: we need to get a resolved file path from the frontend, because we need to read - // the shared object to discover symbol types. - }, } } fn loadArchive(elf: *Elf, path: std.Build.Cache.Path, fr: *Io.File.Reader) (LoadParseInputError || error{BadMagic})!void { diff --git a/src/link/Lld.zig b/src/link/Lld.zig index f715ea6147f2cb584adca502e9cfb958f9e24724..8ed1f43086fe355ddf5481287c4d1ca41f2c78a4 100644 --- a/src/link/Lld.zig +++ b/src/link/Lld.zig @@ -309,9 +309,9 @@ fn linkAsArchive(lld: *Lld, arena: Allocator) link.Error!void { try object_files.ensureUnusedCapacity(arena, comp.link_inputs.len); for (comp.link_inputs) |input| switch (input) { - .dso, .dso_exact, .archive => {}, // static archives should not contain shared libraries or other static archives + .dso, .archive => {}, // static archives should not contain shared libraries or other static archives .res, .object => { - const path = try input.path().?.toStringZ(arena); + const path = try input.path().toStringZ(arena); object_files.appendAssumeCapacity(path); }, }; @@ -551,7 +551,6 @@ fn coffLink(lld: *Lld, arena: Allocator) !void { try argv.ensureUnusedCapacity(comp.link_inputs.len); for (comp.link_inputs) |link_input| switch (link_input) { - .dso_exact => unreachable, // not applicable to PE/COFF inline .dso, .res => |x| { argv.appendAssumeCapacity(try x.path.toString(arena)); }, @@ -1114,10 +1113,6 @@ fn elfLink(lld: *Lld, arena: Allocator) !void { } try argv.append(try obj.path.toString(arena)); }, - .dso_exact => |dso_exact| { - assert(dso_exact.name[0] == ':'); - try argv.appendSlice(&.{ "-l", dso_exact.name }); - }, }; if (whole_archive) { @@ -1161,7 +1156,7 @@ fn elfLink(lld: *Lld, arena: Allocator) !void { for (base.comp.link_inputs) |link_input| switch (link_input) { .res => unreachable, // Windows-only - .object, .archive, .dso_exact => continue, + .object, .archive => continue, .dso => |dso| { const lib_as_needed = !dso.needed; switch ((@as(u2, @intFromBool(lib_as_needed)) << 1) | @intFromBool(as_needed)) { @@ -1176,11 +1171,25 @@ fn elfLink(lld: *Lld, arena: Allocator) !void { }, } - // By this time, we depend on these libs being dynamically linked - // libraries and not static libraries (the check for that needs to be earlier), - // but they could be full paths to .so files, in which case we - // want to avoid prepending "-l". - argv.appendAssumeCapacity(try dso.path.toString(arena)); + if (dso.exact_name) |exact_name| { + // LLD does not have a way to specify full path to the DSO while also specifying + // the exact value for NEEDED, so we have to use a combination of -L and -l args. + assert(mem.endsWith(u8, dso.path.sub_path, exact_name)); + const truncated_path: Cache.Path = .{ + .root_dir = dso.path.root_dir, + .sub_path = dso.path.sub_path[0 .. dso.path.sub_path.len - exact_name.len], + }; + try argv.ensureUnusedCapacity(3); + argv.appendAssumeCapacity("-L"); + argv.appendAssumeCapacity(try truncated_path.toString(arena)); + argv.appendAssumeCapacity(try arena.print("-l:{s}", .{exact_name})); + } else { + // By this time, we depend on these libs being dynamically linked + // libraries and not static libraries (the check for that needs to be earlier), + // but they could be full paths to .so files, in which case we + // want to avoid prepending "-l". + argv.appendAssumeCapacity(try dso.path.toString(arena)); + } }, }; @@ -1588,7 +1597,6 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void { .dso => |dso| { try argv.append(try dso.path.toString(arena)); }, - .dso_exact => unreachable, .res => unreachable, }; if (whole_archive) { diff --git a/src/link/MachO.zig b/src/link/MachO.zig index c32301b826482026993bfb7e8763357cd57dbf06..4df893a7e02e0942dd51897dd072c62646be25fb 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -372,7 +372,6 @@ pub fn flush( for (comp.link_inputs) |link_input| switch (link_input) { .dso => continue, // handled below .object, .archive => positionals.appendAssumeCapacity(link_input), - .dso_exact => @panic("TODO"), .res => unreachable, }; @@ -406,7 +405,7 @@ pub fn flush( for (positionals.items) |link_input| { self.classifyInputFile(link_input) catch |err| - diags.addParseError(link_input.path().?, "failed to read input file: {s}", .{@errorName(err)}); + diags.addParseError(link_input.path(), "failed to read input file: {t}", .{err}); } var system_libs = std.array_list.Managed(SystemLib).init(gpa); @@ -447,11 +446,11 @@ pub fn flush( }; for (comp.link_inputs) |link_input| switch (link_input) { - .object, .archive, .dso_exact => continue, + .object, .archive => continue, .res => unreachable, .dso => { self.classifyInputFile(link_input) catch |err| - diags.addParseError(link_input.path().?, "failed to parse input file: {s}", .{@errorName(err)}); + diags.addParseError(link_input.path(), "failed to parse input file: {t}", .{err}); }, }; @@ -655,8 +654,13 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { for (comp.link_inputs) |link_input| switch (link_input) { .object, .archive => |obj| try argv.append(try obj.path.toString(arena)), .res => |res| try argv.append(try res.path.toString(arena)), - .dso => |dso| try argv.append(try dso.path.toString(arena)), - .dso_exact => |dso_exact| try argv.appendSlice(&.{ "-l", dso_exact.name }), + .dso => |dso| { + if (dso.exact_name) |exact_name| { + try argv.appendSlice(&.{ "-l", try arena.print(":{s}", .{exact_name}) }); + } else { + try argv.append(try dso.path.toString(arena)); + } + }, }; for (comp.c_objects.items) |c_object| { @@ -748,7 +752,6 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { if (obj.must_link) try argv.append("-force_load"); // TODO: verify this try argv.append(try obj.path.toString(arena)); }, - .dso_exact => |dso_exact| try argv.appendSlice(&.{ "-l", dso_exact.name }), }; for (comp.c_objects.items) |c_object| { @@ -775,15 +778,21 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { } for (comp.link_inputs) |link_input| switch (link_input) { - .object, .archive, .dso_exact => continue, // handled above + .object, .archive => continue, // handled above .res => unreachable, // windows only .dso => |dso| { + try argv.ensureUnusedCapacity(2); if (dso.needed) { - try argv.appendSlice(&.{ "-needed-l", try dso.path.toString(arena) }); + argv.appendAssumeCapacity("-needed-l"); } else if (dso.weak) { - try argv.appendSlice(&.{ "-weak-l", try dso.path.toString(arena) }); + argv.appendAssumeCapacity("-weak-l"); } else { - try argv.appendSlice(&.{ "-l", try dso.path.toString(arena) }); + argv.appendAssumeCapacity("-l"); + } + if (dso.exact_name) |exact_name| { + argv.appendAssumeCapacity(try arena.print(":{s}", .{exact_name})); + } else { + argv.appendAssumeCapacity(try dso.path.toString(arena)); } }, }; @@ -870,7 +879,7 @@ pub fn classifyInputFile(self: *MachO, input: link.Input) !void { const comp = self.base.comp; const io = comp.io; - const path, const file = input.pathAndFile().?; + const path, const file = input.pathAndFile(); // TODO don't classify now, it's too late. The input file has already been classified log.debug("classifying input file {f}", .{path}); @@ -4478,7 +4487,6 @@ const SystemLib = struct { fn fromLinkInput(link_input: link.Input) SystemLib { return switch (link_input) { - .dso_exact => unreachable, .res => unreachable, .object, .archive => |obj| .{ .path = obj.path, diff --git a/src/link/MachO/relocatable.zig b/src/link/MachO/relocatable.zig index 793cfe5f0c2659a7df45a6ea69dfccc0f101db08..38420414382928237d93b16b5c8648e4c91fb6f5 100644 --- a/src/link/MachO/relocatable.zig +++ b/src/link/MachO/relocatable.zig @@ -19,7 +19,7 @@ pub fn flushObject(macho_file: *MachO, comp: *Compilation, module_obj_path: ?Pat // Instead of invoking a full-blown `-r` mode on the input which sadly will strip all // debug info segments/sections (this is apparently by design by Apple), we copy // the *only* input file over. - const path = positionals.items[0].path().?; + const path = positionals.items[0].path(); const in_file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| return diags.fail("failed to open {f}: {s}", .{ path, @errorName(err) }); const stat = in_file.stat(io) catch |err| @@ -31,7 +31,7 @@ pub fn flushObject(macho_file: *MachO, comp: *Compilation, module_obj_path: ?Pat for (positionals.items) |link_input| { macho_file.classifyInputFile(link_input) catch |err| - diags.addParseError(link_input.path().?, "failed to read input file: {s}", .{@errorName(err)}); + diags.addParseError(link_input.path(), "failed to read input file: {t}", .{err}); } if (diags.hasErrors()) return error.AlreadyReported; @@ -102,7 +102,7 @@ pub fn flushStaticLib(macho_file: *MachO, comp: *Compilation, module_obj_path: ? for (positionals.items) |link_input| { macho_file.classifyInputFile(link_input) catch |err| - diags.addParseError(link_input.path().?, "failed to read input file: {s}", .{@errorName(err)}); + diags.addParseError(link_input.path(), "failed to read input file: {t}", .{err}); } if (diags.hasErrors()) return error.AlreadyReported; diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 4532b75a647013c1f15f19aebceae5a094b510ab..30868f14272601c048c01e495e239d9819c32a85 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -3788,7 +3788,6 @@ pub fn loadInput(wasm: *Wasm, input: link.Input) !void { const argv = &wasm.dump_argv_list; switch (input) { .res => unreachable, - .dso_exact => unreachable, .dso => unreachable, .object, .archive => |obj| { try argv.append(gpa, try obj.path.toString(comp.arena)); @@ -3798,7 +3797,6 @@ pub fn loadInput(wasm: *Wasm, input: link.Input) !void { switch (input) { .res => unreachable, - .dso_exact => unreachable, .dso => unreachable, .object => |obj| try parseObject(wasm, obj), .archive => |obj| try parseArchive(wasm, obj), diff --git a/src/main.zig b/src/main.zig index bc2354b3a8c3755e7efb1d4ed1afe3e77a38cf2e..095a563ca2b605924aa3c41eeb9285fedf582897 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4277,10 +4277,10 @@ fn createModule( &create_module.link_inputs, create_module.lib_directories.items, color, - ) catch |err| fatal("failed to resolve link inputs: {s}", .{@errorName(err)}); + ) catch |err| fatal("failed to resolve link inputs: {t}", .{err}); if (!create_module.opts.any_dyn_libs) for (create_module.link_inputs.items) |item| switch (item) { - .dso, .dso_exact => { + .dso => { create_module.opts.any_dyn_libs = true; break; }, -- 2.54.0 From 0a1843452e6d39d4a2adc9c7feefce274a5d30c6 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Mon, 21 Sep 2026 21:03:55 +0100 Subject: [PATCH 3/4] link: clean up and unify shared library input rules For determining the `DT_NEEDED` value: * the `DT_SONAME` always takes precedence * otherwise: * `-l` arguments use the resolved path's basename * raw `/path/to/libfoo.so` arguments use the full path All ELF linkers, including LLD, now implement exactly those semantics. The operand to `-l` is not allowed to contain path separators, because the behavior of this in other linkers is confusing and likely not supported. (This is not the case for `-l :path/to/libfoo.so`.) --- src/link.zig | 140 +++++++++++++++++++++++++--------- src/link/Elf.zig | 13 +++- src/link/Elf/SharedObject.zig | 4 +- src/link/Elf2.zig | 22 +++--- src/link/Lld.zig | 35 +++++---- src/link/MachO.zig | 12 +-- src/main.zig | 17 ++++- 7 files changed, 161 insertions(+), 82 deletions(-) diff --git a/src/link.zig b/src/link.zig index e913efa77ae4c3e8113d07ff6cf81b79e098bdb8..6b2d2eecdf7f644ddaa94b71a701de983173fda5 100644 --- a/src/link.zig +++ b/src/link.zig @@ -1819,11 +1819,14 @@ pub const UnresolvedInput = union(enum) { path_query: PathQuery, /// Strings that come from GNU ld scripts. Is it a filename? Is it a path? /// Who knows! Fuck around and find out. - ambiguous_name: NameQuery, + ambiguous_name: AmbiguousNameQuery, pub const NameQuery = struct { name: []const u8, query: Query, + // Corresponds to GNU ld `-l :path/to/filename`, meaning that `name` is a path relative to + // the library search path rather than just a library name. + name_done: bool, }; pub const PathQuery = struct { @@ -1831,6 +1834,11 @@ pub const UnresolvedInput = union(enum) { query: Query, }; + pub const AmbiguousNameQuery = struct { + name: []const u8, + query: Query, + }; + pub const Query = struct { needed: bool = false, weak: bool = false, @@ -1838,14 +1846,6 @@ pub const UnresolvedInput = union(enum) { must_link: bool = false, hidden: bool = false, allow_so_scripts: bool = false, - // Corresponds to GNU ld `-l :path/to/filename`: - // * No extra rpaths. - // * NEEDED entry should be exactly the string - // after the colon. No file system paths prepended. - // * The DSO still must be found at compile/link - // time and its entries used to resolve symbols. - // CGo compilation depends on this. - name_done: bool = false, preferred_mode: std.lang.LinkMode, search_strategy: SearchStrategy, @@ -1891,15 +1891,9 @@ pub const Input = union(enum) { needed: bool, weak: bool, reexport: bool, - /// Does not include any ":" prefix. Indicates that this string, exactly, should go into the NEEDED - /// entry, without affecting any rpaths. - exact_name: ?[]const u8, - }; + fallback_soname: FallbackSoname, - pub const DsoExact = struct { - /// Includes the ":" prefix. This is intended to be put into the DSO - /// section verbatim with no corresponding rpaths. - name: []const u8, + pub const FallbackSoname = enum { basename, full_path }; }; pub fn path(input: Input) Path { @@ -1950,7 +1944,7 @@ pub fn hashInputs(man: *Cache.Manifest, link_inputs: []const Input) !void { man.hash.add(dso.needed); man.hash.add(dso.weak); man.hash.add(dso.reexport); - man.hash.addOptionalBytes(dso.exact_name); + man.hash.add(dso.fallback_soname); }, } } @@ -2001,7 +1995,7 @@ pub fn resolveInputs( .name_query => |name_query| { const query = name_query.query; - // Checked in the first pass above while looking for libc libraries. + // Checked in the first pass in `main.zig` while looking for libc libraries. assert(!fs.path.isAbsolute(name_query.name)); checked_paths.clearRetainingCapacity(); @@ -2229,7 +2223,7 @@ fn resolveLibInput( const lib_name = name_query.name; - if (target.os.tag.isDarwin() and link_mode == .dynamic and !name_query.query.name_done) tbd: { + if (target.os.tag.isDarwin() and link_mode == .dynamic and !name_query.name_done) tbd: { // Prefer .tbd over .dylib. const test_path: Path = .{ .root_dir = lib_directory, @@ -2241,13 +2235,22 @@ fn resolveLibInput( else => |e| fatal("searching for tbd library {qf}: {t}", .{ test_path, e }), }; errdefer file.close(io); - return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query); + return finishResolveLibInput( + io, + resolved_inputs, + archive_dedup, + test_path, + file, + link_mode, + name_query.query, + .basename, + ); } { const test_path: Path = .{ .root_dir = lib_directory, - .sub_path = if (name_query.query.name_done) lib_name else try std.fmt.allocPrint(arena, "{s}{s}{s}", .{ + .sub_path = if (name_query.name_done) lib_name else try std.fmt.allocPrint(arena, "{s}{s}{s}", .{ target.libPrefix(), lib_name, switch (link_mode) { @@ -2260,7 +2263,7 @@ fn resolveLibInput( 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)) { + }, link_mode, color, .basename)) { .no_match => {}, .ok => return .ok, } @@ -2268,7 +2271,7 @@ fn resolveLibInput( // In the case of Darwin, the main check will be .dylib, so here we // additionally check for .so files. - if (target.os.tag.isDarwin() and link_mode == .dynamic and !name_query.query.name_done) so: { + if (target.os.tag.isDarwin() and link_mode == .dynamic and !name_query.name_done) so: { const test_path: Path = .{ .root_dir = lib_directory, .sub_path = try std.fmt.allocPrint(arena, "lib{s}.so", .{lib_name}), @@ -2279,12 +2282,21 @@ fn resolveLibInput( else => |e| fatal("unable to search for so library {qf}: {t}", .{ test_path, e }), }; errdefer file.close(io); - return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query); + return finishResolveLibInput( + io, + resolved_inputs, + archive_dedup, + test_path, + file, + link_mode, + name_query.query, + .basename, + ); } // In the case of MinGW, the main check will be .lib but we also need to // look for `libfoo.a`. - if (target.isMinGW() and link_mode == .static and !name_query.query.name_done) mingw: { + if (target.isMinGW() and link_mode == .static and !name_query.name_done) mingw: { const test_path: Path = .{ .root_dir = lib_directory, .sub_path = try std.fmt.allocPrint(arena, "lib{s}.a", .{lib_name}), @@ -2295,13 +2307,22 @@ fn resolveLibInput( else => |e| fatal("unable to search for static library {qf}: {t}", .{ test_path, e }), }; errdefer file.close(io); - return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, name_query.query); + return finishResolveLibInput( + io, + resolved_inputs, + archive_dedup, + test_path, + file, + link_mode, + name_query.query, + .basename, + ); } // In the case of OpenBSD, dynamic libraries are always versioned, without // unversioned symlinks. OpenBSD patches LLD to select the highest-versioned // shared library, and this code is intended to match that upstream behavior. - if (target.isOpenBSDLibC() and link_mode == .dynamic and !name_query.query.name_done) versioned: { + if (target.isOpenBSDLibC() and link_mode == .dynamic and !name_query.name_done) versioned: { const prefix = try std.fmt.allocPrint(arena, "lib{s}.so.", .{lib_name}); var dir = lib_directory.handle.openDir(io, ".", .{ .iterate = true }) catch |err| switch (err) { @@ -2347,7 +2368,7 @@ fn resolveLibInput( 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)) { + }, link_mode, color, .basename)) { .no_match => {}, .ok => return .ok, } @@ -2388,6 +2409,7 @@ fn finishResolveLibInput( file: Io.File, link_mode: std.lang.LinkMode, query: UnresolvedInput.Query, + fallback_soname: Input.Dso.FallbackSoname, ) ResolveLibInputResult { switch (link_mode) { .static => { @@ -2413,9 +2435,7 @@ fn finishResolveLibInput( .needed = query.needed, .weak = query.weak, .reexport = query.reexport, - // Here we assume that `Path.sub_path` is usable as `exact_name`, however the code after this point - // has no business making that assumption. - .exact_name = if (query.name_done) path.sub_path else null, + .fallback_soname = fallback_soname, }, }), } @@ -2439,8 +2459,34 @@ fn resolvePathInput( 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, 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), + .static_library => return try resolvePathInputLib( + gpa, + arena, + io, + unresolved_inputs, + resolved_inputs, + ld_script_bytes, + archive_dedup, + target, + pq, + .static, + color, + .full_path, + ), + .shared_library => return try resolvePathInputLib( + gpa, + arena, + io, + unresolved_inputs, + resolved_inputs, + ld_script_bytes, + archive_dedup, + target, + pq, + .dynamic, + color, + .full_path, + ), .object => { var file = pq.path.root_dir.handle.openFile(io, pq.path.sub_path, .{}) catch |err| fatal("failed to open object {f}: {t}", .{ pq.path, err }); @@ -2483,6 +2529,7 @@ fn resolvePathInputLib( pq: UnresolvedInput.PathQuery, link_mode: std.lang.LinkMode, color: std.zig.Color, + fallback_soname: Input.Dso.FallbackSoname, ) Allocator.Error!ResolveLibInputResult { try resolved_inputs.ensureUnusedCapacity(gpa, 1); try archive_dedup.ensureUnusedCapacity(gpa, 1); @@ -2510,7 +2557,16 @@ fn resolvePathInputLib( mem.startsWith(u8, buf, std.elf.ARMAG_THIN)) { // Appears to be an ELF or archive file. - return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, pq.query); + return finishResolveLibInput( + io, + resolved_inputs, + archive_dedup, + test_path, + file, + link_mode, + pq.query, + fallback_soname, + ); } const stat = file.stat(io) catch |err| fatal("failed to stat {f}: {t}", .{ test_path, err }); @@ -2561,6 +2617,7 @@ fn resolvePathInputLib( unresolved_inputs.appendAssumeCapacity(.{ .name_query = .{ .name = try arena.dupe(u8, arg.path["-l".len..]), .query = query, + .name_done = false, } }); } else { unresolved_inputs.appendAssumeCapacity(.{ .ambiguous_name = .{ @@ -2578,7 +2635,16 @@ fn resolvePathInputLib( else => |e| fatal("unable to search for {t} library {f}: {t}", .{ link_mode, test_path, e }), }; errdefer file.close(io); - return finishResolveLibInput(io, resolved_inputs, archive_dedup, test_path, file, link_mode, pq.query); + return finishResolveLibInput( + io, + resolved_inputs, + archive_dedup, + test_path, + file, + link_mode, + pq.query, + fallback_soname, + ); } pub fn openObject(io: Io, path: Path, must_link: bool, hidden: bool) !Input.Object { @@ -2601,7 +2667,7 @@ pub fn openDso(io: Io, path: Path, needed: bool, weak: bool, reexport: bool) !In .needed = needed, .weak = weak, .reexport = reexport, - .exact_name = null, + .fallback_soname = .full_path, }; } diff --git a/src/link/Elf.zig b/src/link/Elf.zig index 4c9a91d25ebcac10fd8186f33b2e22d5ad6ed44f..b0e188390f24c3000d382aae4b76646fd6306644 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -735,7 +735,7 @@ pub fn loadInput(self: *Elf, input: link.Input) !void { } else { try parseArchive(gpa, io, diags, &self.file_handles, &self.files, target, debug_fmt_strip, default_sym_version, &self.objects, obj); }, - .dso => |dso| try parseDso(gpa, io, diags, dso, &self.shared_objects, &self.files, target), + .dso => |dso| try parseDso(gpa, comp.arena, io, diags, dso, &self.shared_objects, &self.files, target), } } @@ -1106,6 +1106,7 @@ fn parseArchive( fn parseDso( gpa: Allocator, + arena: Allocator, io: Io, diags: *Diags, dso: link.Input.Dso, @@ -1118,13 +1119,16 @@ fn parseDso( const handle = dso.file; - if (dso.exact_name != null) @panic("TODO"); - const stat: Stat = .init(try handle.stat(io)); var header = try SharedObject.parseHeader(gpa, io, diags, dso.path, handle, stat, target); defer header.deinit(gpa); - const soname = header.soname() orelse dso.path.basename(); + const fallback_soname: []const u8 = switch (dso.fallback_soname) { + .full_path => try dso.path.toString(arena), + .basename => fs.path.basename(dso.path.sub_path), + }; + + const soname = header.soname() orelse fallback_soname; const gop = try shared_objects.getOrPut(gpa, soname); if (gop.found_existing) return; @@ -1156,6 +1160,7 @@ fn parseDso( .symbols_extra = .empty, .symbols_resolver = .empty, .output_symtab_ctx = .{}, + .fallback_soname = fallback_soname, }, }); const so = fileLookup(files.*, index, null).?.shared_object; diff --git a/src/link/Elf/SharedObject.zig b/src/link/Elf/SharedObject.zig index 3030a45bff3806d93b1af418d6eaa49a7a649558..05d886e5fcaa177a8959003d0a0aa581f543f980 100644 --- a/src/link/Elf/SharedObject.zig +++ b/src/link/Elf/SharedObject.zig @@ -31,6 +31,8 @@ alive: bool, output_symtab_ctx: Elf.SymtabCtx, +fallback_soname: []const u8, + pub fn deinit(so: *SharedObject, gpa: Allocator) void { gpa.free(so.path.sub_path); so.parsed.deinit(gpa); @@ -427,7 +429,7 @@ pub fn asFile(self: *SharedObject) File { } pub fn soname(self: *SharedObject) []const u8 { - return self.parsed.soname() orelse self.path.basename(); + return self.parsed.soname() orelse self.fallback_soname; } pub fn initSymbolAliases(self: *SharedObject, elf_file: *Elf) !void { diff --git a/src/link/Elf2.zig b/src/link/Elf2.zig index 155f5aeb18a74f6a55543eeb8dd53fd25f240af9..53bd6d47970c35a93d6410298077767065de69a9 100644 --- a/src/link/Elf2.zig +++ b/src/link/Elf2.zig @@ -6228,17 +6228,9 @@ fn loadInputInner(elf: *Elf, input: link.Input) (Error || error{BadMagic})!void }, .res => unreachable, .dso => |dso| { - if (dso.exact_name) |exact_name| { - // This means the path field is still valid, but the NEEDED entry must be set to - // exactly the `exact_name` string. - std.debug.panic("load dso_exact {q}", .{exact_name}); - if (elf.shndx.dynamic != .UNDEF) { - try elf.needed.put(elf.base.comp.gpa, try elf.string(.dynstr, exact_name), {}); - } - } try elf.needed.ensureUnusedCapacity(elf.base.comp.gpa, 1); var fr = dso.file.reader(io, &buf); - elf.loadDso(dso.path, &fr) catch |err| switch (err) { + elf.loadDso(dso.path, dso.fallback_soname, &fr) catch |err| switch (err) { else => |e| return e, error.EndOfStream => return diags.failParse(dso.path, "unexpected eof", .{}), error.AccessDenied, error.Unexpected, error.Unseekable => |e| return diags.fail( @@ -6860,7 +6852,12 @@ fn populateArchiveMemberName(elf: *Elf, member_ar_hdr: *std.elf.ar_hdr, member_n @memcpy(dest_slice[0 .. dest_slice.len - 2], member_name); @memcpy(dest_slice[dest_slice.len - 2 ..], "/\n"); // yes, the terminator is weird } -fn loadDso(elf: *Elf, path: std.Build.Cache.Path, fr: *Io.File.Reader) (LoadParseInputError || error{BadMagic})!void { +fn loadDso( + elf: *Elf, + path: std.Build.Cache.Path, + fallback_soname: link.Input.Dso.FallbackSoname, + fr: *Io.File.Reader, +) (LoadParseInputError || error{BadMagic})!void { const comp = elf.base.comp; const gpa = comp.gpa; const diags = &comp.link_diags; @@ -6956,7 +6953,10 @@ fn loadDso(elf: *Elf, path: std.Build.Cache.Path, fr: *Io.File.Reader) (LoadPars } break std.mem.sliceTo(dynstr[@intCast(val)..], 0); } - } else std.fs.path.basename(path.sub_path); + } else switch (fallback_soname) { + .basename => std.fs.path.basename(path.sub_path), + .full_path => try path.toString(comp.arena), + }; try elf.needed.put(gpa, try elf.string(.dynstr, soname), {}); // Scan the symbol table and populate `elf.dso_globals`. diff --git a/src/link/Lld.zig b/src/link/Lld.zig index 8ed1f43086fe355ddf5481287c4d1ca41f2c78a4..c56557dfb02336a36ab88ec32170b705a64d5e53 100644 --- a/src/link/Lld.zig +++ b/src/link/Lld.zig @@ -1154,6 +1154,13 @@ fn elfLink(lld: *Lld, arena: Allocator) !void { argv.appendAssumeCapacity("--as-needed"); var as_needed = true; + // When we have a DSO input, in order to trick LLD into putting the basename in its + // `DT_NEEDED` entry while still allowing us to tell it the exact path to the shared + // object, we pass it on the CLI as "-l:/absolute/path/to/libfoo.so". This will treat + // the given path not actually as an absolute path, but as relative to the library + // search path, so the root directory must therefore be the only library search path. + try argv.append("-L/"); + for (base.comp.link_inputs) |link_input| switch (link_input) { .res => unreachable, // Windows-only .object, .archive => continue, @@ -1171,24 +1178,16 @@ fn elfLink(lld: *Lld, arena: Allocator) !void { }, } - if (dso.exact_name) |exact_name| { - // LLD does not have a way to specify full path to the DSO while also specifying - // the exact value for NEEDED, so we have to use a combination of -L and -l args. - assert(mem.endsWith(u8, dso.path.sub_path, exact_name)); - const truncated_path: Cache.Path = .{ - .root_dir = dso.path.root_dir, - .sub_path = dso.path.sub_path[0 .. dso.path.sub_path.len - exact_name.len], - }; - try argv.ensureUnusedCapacity(3); - argv.appendAssumeCapacity("-L"); - argv.appendAssumeCapacity(try truncated_path.toString(arena)); - argv.appendAssumeCapacity(try arena.print("-l:{s}", .{exact_name})); - } else { - // By this time, we depend on these libs being dynamically linked - // libraries and not static libraries (the check for that needs to be earlier), - // but they could be full paths to .so files, in which case we - // want to avoid prepending "-l". - argv.appendAssumeCapacity(try dso.path.toString(arena)); + // By this time, we depend on these libs being dynamically linked libraries and + // not static libraries (the check for that needs to be earlier), but they could + // be full file paths, in which case we don't want to use the "-l:" strategy. + switch (dso.fallback_soname) { + .basename => try argv.append(try arena.print("-l:{s}", .{try fs.path.resolve(arena, &.{ + comp.dirs.cwd, + dso.path.root_dir.path orelse ".", + dso.path.sub_path, + })})), + .full_path => try argv.append(try dso.path.toString(arena)), } }, }; diff --git a/src/link/MachO.zig b/src/link/MachO.zig index 4df893a7e02e0942dd51897dd072c62646be25fb..a1baa45e2786cb4183ee5100fbdff11e0f76eb7b 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -655,11 +655,7 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { .object, .archive => |obj| try argv.append(try obj.path.toString(arena)), .res => |res| try argv.append(try res.path.toString(arena)), .dso => |dso| { - if (dso.exact_name) |exact_name| { - try argv.appendSlice(&.{ "-l", try arena.print(":{s}", .{exact_name}) }); - } else { - try argv.append(try dso.path.toString(arena)); - } + try argv.append(try dso.path.toString(arena)); }, }; @@ -789,11 +785,7 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void { } else { argv.appendAssumeCapacity("-l"); } - if (dso.exact_name) |exact_name| { - argv.appendAssumeCapacity(try arena.print(":{s}", .{exact_name})); - } else { - argv.appendAssumeCapacity(try dso.path.toString(arena)); - } + argv.appendAssumeCapacity(try dso.path.toString(arena)); }, }; diff --git a/src/main.zig b/src/main.zig index 095a563ca2b605924aa3c41eeb9285fedf582897..e8f142f772868ef619333afa756be482b48808bb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1351,6 +1351,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }); } else if (mem.eql(u8, arg, "--needed-library") or mem.eql(u8, arg, "-needed-l") or @@ -1366,6 +1367,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }); } else if (mem.eql(u8, arg, "-weak_library") or mem.eql(u8, arg, "-weak-l")) { try create_module.cli_link_inputs.append(arena, .{ .name_query = .{ @@ -1377,6 +1379,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }); } else if (mem.eql(u8, arg, "-D")) { try cc_argv.appendSlice(arena, &.{ arg, args_iter.nextOrFatal() }); @@ -1839,6 +1842,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }); } else if (mem.cutPrefix(u8, arg, "-needed-l")) |name| { try create_module.cli_link_inputs.append(arena, .{ .name_query = .{ @@ -1850,6 +1854,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }); } else if (mem.cutPrefix(u8, arg, "-weak-l")) |name| { try create_module.cli_link_inputs.append(arena, .{ .name_query = .{ @@ -1861,6 +1866,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }); } else if (mem.startsWith(u8, arg, "-D")) { try cc_argv.append(arena, arg); @@ -2095,8 +2101,8 @@ fn buildOutputType( .preferred_mode = lib_preferred_mode, .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, - .name_done = true, }, + .name_done = true, } }); } else { const compiler_rt_classification = target_util.classifyCompilerRtLibName(it.only_arg); @@ -2122,6 +2128,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }); } } @@ -2552,6 +2559,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }), .weak_framework => try create_module.frameworks.put(arena, it.only_arg, .{ .weak = true }), .headerpad_max_install_names => headerpad_max_install_names = true, @@ -2921,6 +2929,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }); } else if (mem.cutPrefix(u8, arg, "-weak-l")) |rest| { try create_module.cli_link_inputs.append(arena, .{ .name_query = .{ @@ -2932,6 +2941,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }); } else if (mem.eql(u8, arg, "-weak_library")) { try create_module.cli_link_inputs.append(arena, .{ .name_query = .{ @@ -2943,6 +2953,7 @@ fn buildOutputType( .search_strategy = lib_search_strategy, .allow_so_scripts = allow_so_scripts, }, + .name_done = false, } }); } else if (mem.eql(u8, arg, "-compatibility_version")) { const compat_version = linker_args_it.nextOrFatal(); @@ -4183,6 +4194,10 @@ fn createModule( fatal("cannot use absolute path as a system library: {s}", .{lib_name}); } + if (!nq.name_done and std.mem.findScalar(u8, nq.name, '/') != null) { + fatal("cannot use path separator in system library name: {s}", .{lib_name}); + } + unresolved_link_inputs.appendAssumeCapacity(cli_link_input); any_name_queries_remaining = true; }, -- 2.54.0 From 77d47494c7ccbe045b713900b32a85c859f78ed0 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 21 Sep 2026 13:11:29 -0700 Subject: [PATCH 4/4] CLI: fix help text for --debug-link-snapshot --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index e8f142f772868ef619333afa756be482b48808bb..1bd12d4a4f7464034824f4a14360360e00a39527 100644 --- a/src/main.zig +++ b/src/main.zig @@ -803,7 +803,7 @@ const compile_usage = \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features \\ --debug-log [scope] Enable printing debug/info log messages for scope \\ --debug-compile-errors Crash with helpful diagnostics at the first compile error - \\ --debug-link-snapshot Enable dumping of the linker's state in JSON format + \\ --debug-link-snapshot Dump linker state and output file information for troubleshooting \\ --debug-rt[=mode] Build compiler runtime libraries with [mode] optimization \\ (debug if [=mode] is omitted) \\ --debug-incremental Enable incremental compilation debug features -- 2.54.0