From 4165096f7d2ed1810bd5dca7e5cf696b8eb6d24e Mon Sep 17 00:00:00 2001 From: Alexander Khabarov Date: Tue, 25 Feb 2025 14:56:36 +0000 Subject: [PATCH 1/3] lib: add support for omitting soname This commit adds the `soname` option for `Build.Module` and `OverlayOptions`. This controls whether `-fsoname` or `-fno-soname` is passed to the linker. The `SoName` type is moved to `std.zig` to make it accessible in other modules. This is needed to implement the new test in https://github.com/ziglang/zig/pull/19818 --- lib/std/Build/Module.zig | 11 +++++++++++ lib/std/zig.zig | 6 ++++++ src/main.zig | 9 ++------- test/link/link.zig | 2 ++ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/std/Build/Module.zig b/lib/std/Build/Module.zig index 0bc77b0741db..ba6367cb2244 100644 --- a/lib/std/Build/Module.zig +++ b/lib/std/Build/Module.zig @@ -18,6 +18,7 @@ frameworks: std.StringArrayHashMapUnmanaged(LinkFrameworkOptions), link_objects: std.ArrayListUnmanaged(LinkObject), strip: ?bool, +soname: ?std.zig.SoName, unwind_tables: ?std.builtin.UnwindTables, single_threaded: ?bool, stack_protector: ?bool, @@ -251,6 +252,7 @@ pub const CreateOptions = struct { link_libcpp: ?bool = null, single_threaded: ?bool = null, strip: ?bool = null, + soname: ?std.zig.SoName = null, unwind_tables: ?std.builtin.UnwindTables = null, dwarf_format: ?std.dwarf.Format = null, code_model: std.builtin.CodeModel = .default, @@ -300,6 +302,7 @@ pub fn init( .frameworks = .{}, .link_objects = .{}, .strip = options.strip, + .soname = options.soname, .unwind_tables = options.unwind_tables, .single_threaded = options.single_threaded, .stack_protector = options.stack_protector, @@ -566,6 +569,14 @@ pub fn appendZigProcessFlags( try addFlag(zig_args, m.pic, "-fPIC", "-fno-PIC"); try addFlag(zig_args, m.red_zone, "-mred-zone", "-mno-red-zone"); + if (m.soname) |soname| { + try zig_args.append(switch (soname) { + .no => "-fno-soname", + .yes_default_value => "-fsoname", + .yes => |value| b.fmt("-fsoname={s}", .{value}), + }); + } + if (m.dwarf_format) |dwarf_format| { try zig_args.append(switch (dwarf_format) { .@"32" => "-gdwarf32", diff --git a/lib/std/zig.zig b/lib/std/zig.zig index 9033164b244e..53ec71815550 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -315,6 +315,12 @@ pub const BuildId = union(enum) { pub const LtoMode = enum { none, full, thin }; +pub const SoName = union(enum) { + no, + yes_default_value, + yes: []const u8, +}; + /// Renders a `std.Target.Cpu` value into a textual representation that can be parsed /// via the `-mcpu` flag passed to the Zig compiler. /// Appends the result to `buffer`. diff --git a/src/main.zig b/src/main.zig index 2605b089eba0..f30806b5b67a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -16,6 +16,7 @@ const native_os = builtin.os.tag; const Cache = std.Build.Cache; const Path = std.Build.Cache.Path; const Directory = std.Build.Cache.Directory; +const SoName = std.zig.SoName; const EnvVar = std.zig.EnvVar; const LibCInstallation = std.zig.LibCInstallation; const AstGen = std.zig.AstGen; @@ -688,12 +689,6 @@ const usage_build_generic = \\ ; -const SOName = union(enum) { - no, - yes_default_value, - yes: []const u8, -}; - const EmitBin = union(enum) { no, yes_default_path, @@ -862,7 +857,7 @@ fn buildOutputType( var target_arch_os_abi: ?[]const u8 = null; var target_mcpu: ?[]const u8 = null; var emit_h: Emit = .no; - var soname: SOName = undefined; + var soname: SoName = undefined; var want_compiler_rt: ?bool = null; var want_ubsan_rt: ?bool = null; var linker_script: ?[]const u8 = null; diff --git a/test/link/link.zig b/test/link/link.zig index 59198e6c8eb1..231529048ef4 100644 --- a/test/link/link.zig +++ b/test/link/link.zig @@ -44,6 +44,7 @@ const OverlayOptions = struct { zig_source_bytes: ?[]const u8 = null, pic: ?bool = null, strip: ?bool = null, + soname: ?std.zig.SoName = null, }; pub fn addExecutable(b: *std.Build, base: Options, overlay: OverlayOptions) *Compile { @@ -97,6 +98,7 @@ fn createModule(b: *Build, base: Options, overlay: OverlayOptions) *Build.Module }, .pic = overlay.pic, .strip = if (base.strip) |s| s else overlay.strip, + .soname = overlay.soname, }); if (overlay.objcpp_source_bytes) |bytes| { From 99261d93cb912cf397f54d387848782edde44617 Mon Sep 17 00:00:00 2001 From: Alexander Khabarov Date: Tue, 25 Feb 2025 15:03:21 +0000 Subject: [PATCH 2/3] test: check that `zig cc` passes `-l`/`-L` like Clang/GCC for ELF This test follows the example from https://github.com/ziglang/zig/issues/19699. --- test/link/elf.zig | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/link/elf.zig b/test/link/elf.zig index 92b857a565c5..9e04caa21efc 100644 --- a/test/link/elf.zig +++ b/test/link/elf.zig @@ -82,6 +82,8 @@ pub fn testAll(b: *Build, build_opts: BuildOptions) *Step { // glibc tests elf_step.dependOn(testAsNeeded(b, .{ .target = gnu_target })); + elf_step.dependOn(testLibraryPathsCompatibility(b, .{ .target = gnu_target, .use_lld = true })); + elf_step.dependOn(testLibraryPathsCompatibility(b, .{ .target = gnu_target, .use_lld = false })); // https://github.com/ziglang/zig/issues/17430 // elf_step.dependOn(testCanonicalPlt(b, .{ .target = gnu_target })); elf_step.dependOn(testCommentString(b, .{ .target = gnu_target })); @@ -309,6 +311,53 @@ fn testAsNeeded(b: *Build, opts: Options) *Step { return test_step; } +fn testLibraryPathsCompatibility(b: *Build, opts: Options) *Step { + const test_step = addTestStep(b, "library-paths-compatibility", opts); + + const main_o = addObject(b, opts, .{ + .name = "main", + .c_source_bytes = + \\#include + \\int foo(); + \\int main() { + \\ printf("%d\n", foo()); + \\ return 0; + \\} + \\ + , + }); + main_o.linkLibC(); + + const libfoo = addSharedLibrary(b, opts, .{ .name = "foo", .soname = .no }); + addCSourceBytes(libfoo, "int foo() { return 42; }", &.{}); + + { + const scripts = WriteFile.create(b); + const path = scripts.addCopyFile(libfoo.getEmittedBin(), "foo/libfoo.so"); + + const exe = addExecutable(b, opts, .{ .name = "test" }); + exe.addObject(main_o); + + exe.addLibraryPath(.{ .generated = .{ .file = &scripts.generated_directory, .sub_path = "foo" } }); + exe.addRPath(path.dirname()); + + exe.linkSystemLibrary2("foo", .{ .needed = false }); + exe.linkLibC(); + + const run = addRunArtifact(exe); + run.expectStdOutEqual("42\n"); + test_step.dependOn(&run.step); + + const check = exe.checkObject(); + check.checkInDynamicSection(); + check.checkExact("NEEDED libfoo.so"); + check.checkNotPresent("NEEDED foo/libfoo.so"); + test_step.dependOn(&check.step); + } + + return test_step; +} + fn testCanonicalPlt(b: *Build, opts: Options) *Step { const test_step = addTestStep(b, "canonical-plt", opts); From 31be45eb62897b39eaff05411408bcc3c780850f Mon Sep 17 00:00:00 2001 From: Alexander Khabarov Date: Tue, 25 Feb 2025 15:04:19 +0000 Subject: [PATCH 3/3] link: make `zig cc` pass `-l`/`-L` like Clang/GCC for ELF --- src/link.zig | 30 +++++++++++++++++++++--------- src/link/Elf.zig | 24 +++++++++++++++++------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/link.zig b/src/link.zig index f436106aabae..8fa28d7db924 100644 --- a/src/link.zig +++ b/src/link.zig @@ -1818,6 +1818,8 @@ pub const Input = union(enum) { needed: bool, weak: bool, reexport: bool, + name: ?[]const u8, + lib_directory: Directory, }; pub const DsoExact = struct { @@ -1870,6 +1872,8 @@ 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.name); + man.hash.addOptionalBytes(dso.lib_directory.path); }, .dso_exact => |dso_exact| { man.hash.addBytes(dso_exact.name); @@ -2145,7 +2149,7 @@ fn resolveLibInput( else => |e| fatal("unable to search for tbd library '{}': {s}", .{ test_path, @errorName(e) }), }; errdefer file.close(); - return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query); + return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query, name_query.name, lib_directory); } { @@ -2159,10 +2163,10 @@ fn resolveLibInput( }), }; try checked_paths.writer(gpa).print("\n {}", .{test_path}); - switch (try resolvePathInputLib(gpa, arena, unresolved_inputs, resolved_inputs, ld_script_bytes, target, .{ + switch (try resolvePathInputLib(gpa, arena, unresolved_inputs, resolved_inputs, ld_script_bytes, lib_directory, target, .{ .path = test_path, .query = name_query.query, - }, link_mode, color)) { + }, link_mode, color, name_query.name)) { .no_match => {}, .ok => return .ok, } @@ -2183,7 +2187,7 @@ fn resolveLibInput( }), }; errdefer file.close(); - return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query); + return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query, name_query.name, lib_directory); } // In the case of MinGW, the main check will be .lib but we also need to @@ -2199,7 +2203,7 @@ fn resolveLibInput( else => |e| fatal("unable to search for static library '{}': {s}", .{ test_path, @errorName(e) }), }; errdefer file.close(); - return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query); + return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, name_query.query, name_query.name, lib_directory); } return .no_match; @@ -2211,6 +2215,8 @@ fn finishResolveLibInput( file: std.fs.File, link_mode: std.builtin.LinkMode, query: UnresolvedInput.Query, + name: ?[]const u8, + lib_directory: Directory, ) ResolveLibInputResult { switch (link_mode) { .static => resolved_inputs.appendAssumeCapacity(.{ .archive = .{ @@ -2225,6 +2231,8 @@ fn finishResolveLibInput( .needed = query.needed, .weak = query.weak, .reexport = query.reexport, + .name = name, + .lib_directory = lib_directory, } }), } return .ok; @@ -2244,8 +2252,8 @@ fn resolvePathInput( color: std.zig.Color, ) Allocator.Error!?ResolveLibInputResult { switch (Compilation.classifyFileExt(pq.path.sub_path)) { - .static_library => return try resolvePathInputLib(gpa, arena, unresolved_inputs, resolved_inputs, ld_script_bytes, target, pq, .static, color), - .shared_library => return try resolvePathInputLib(gpa, arena, unresolved_inputs, resolved_inputs, ld_script_bytes, target, pq, .dynamic, color), + .static_library => return try resolvePathInputLib(gpa, arena, unresolved_inputs, resolved_inputs, ld_script_bytes, Directory.cwd(), target, pq, .static, color, null), + .shared_library => return try resolvePathInputLib(gpa, arena, unresolved_inputs, resolved_inputs, ld_script_bytes, Directory.cwd(), target, pq, .dynamic, color, null), .object => { var file = pq.path.root_dir.handle.openFile(pq.path.sub_path, .{}) catch |err| fatal("failed to open object {}: {s}", .{ pq.path, @errorName(err) }); @@ -2281,10 +2289,12 @@ fn resolvePathInputLib( resolved_inputs: *std.ArrayListUnmanaged(Input), /// Allocated via `gpa`. ld_script_bytes: *std.ArrayListUnmanaged(u8), + lib_directory: Directory, target: std.Target, pq: UnresolvedInput.PathQuery, link_mode: std.builtin.LinkMode, color: std.zig.Color, + name: ?[]const u8, ) Allocator.Error!ResolveLibInputResult { try resolved_inputs.ensureUnusedCapacity(gpa, 1); @@ -2309,7 +2319,7 @@ fn resolvePathInputLib( if (n != ld_script_bytes.items.len) break :elf_file; if (!mem.eql(u8, ld_script_bytes.items[0..4], "\x7fELF")) break :elf_file; // Appears to be an ELF file. - return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, pq.query); + return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, pq.query, name, lib_directory); } const stat = file.stat() catch |err| fatal("failed to stat {}: {s}", .{ test_path, @errorName(err) }); @@ -2375,7 +2385,7 @@ fn resolvePathInputLib( }), }; errdefer file.close(); - return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, pq.query); + return finishResolveLibInput(resolved_inputs, test_path, file, link_mode, pq.query, name, lib_directory); } pub fn openObject(path: Path, must_link: bool, hidden: bool) !Input.Object { @@ -2398,6 +2408,8 @@ pub fn openDso(path: Path, needed: bool, weak: bool, reexport: bool) !Input.Dso .needed = needed, .weak = weak, .reexport = reexport, + .name = null, + .lib_directory = Directory.cwd(), }; } diff --git a/src/link/Elf.zig b/src/link/Elf.zig index a338ec722d0a..0e9961dba9f3 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -1980,9 +1980,12 @@ fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: s // Shared libraries. if (is_exe_or_dyn_lib) { - // Worst-case, we need an --as-needed argument for every lib, as well - // as one before and one after. - try argv.ensureUnusedCapacity(2 * self.base.comp.link_inputs.len + 2); + // Worst-case, we need: + // * an --as-needed/--no-as-needed argument for every lib + // * -L, lib directory, -l/-weak-l, library name for every lib + // * one --as-needed before and one --as-needed after + try argv.ensureUnusedCapacity(5 * self.base.comp.link_inputs.len + 2); + argv.appendAssumeCapacity("--as-needed"); var as_needed = true; @@ -2004,10 +2007,17 @@ fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: s } // 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)); + // libraries and not static libraries (the check for that needs to be earlier). + if (dso.name) |name| { + if (dso.lib_directory.path) |path| { + argv.appendAssumeCapacity("-L"); + argv.appendAssumeCapacity(path); + } + argv.appendAssumeCapacity(if (dso.weak) "-weak-l" else "-l"); + argv.appendAssumeCapacity(name); + } else { + argv.appendAssumeCapacity(try dso.path.toString(arena)); + } }, };