From efc5c97bff87d4c28ae9642fe69d9bc2c7e9eeb7 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Mon, 27 Jun 2022 09:24:18 +0200 Subject: [PATCH 1/2] macho: implement -dead_strip_dylibs linker flag --- lib/std/build.zig | 6 +++ src/Compilation.zig | 8 +++- src/link.zig | 3 ++ src/link/Coff.zig | 2 +- src/link/Elf.zig | 2 +- src/link/MachO.zig | 14 ++++++- src/link/Wasm.zig | 2 +- src/main.zig | 7 ++++ test/link.zig | 2 +- test/link/macho/dead_strip_dylibs/build.zig | 46 +++++++++++++++++++++ test/link/macho/dead_strip_dylibs/main.c | 10 +++++ test/link/macho/frameworks/build.zig | 32 -------------- test/link/macho/frameworks/main.c | 7 ---- 13 files changed, 94 insertions(+), 47 deletions(-) create mode 100644 test/link/macho/dead_strip_dylibs/build.zig create mode 100644 test/link/macho/dead_strip_dylibs/main.c delete mode 100644 test/link/macho/frameworks/build.zig delete mode 100644 test/link/macho/frameworks/main.c diff --git a/lib/std/build.zig b/lib/std/build.zig index 968ee043bff4..da5ffb33bde3 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -1601,6 +1601,9 @@ pub const LibExeObjStep = struct { /// and start of `__TEXT,__text` section to a value fitting all paths expanded to MAXPATHLEN. headerpad_max_install_names: bool = false, + /// (Darwin) Remove dylibs that are unreachable by the entry point or exported symbols. + dead_strip_dylibs: bool = false, + /// Position Independent Code force_pic: ?bool = null, @@ -2676,6 +2679,9 @@ pub const LibExeObjStep = struct { if (self.headerpad_max_install_names) { try zig_args.append("-headerpad_max_install_names"); } + if (self.dead_strip_dylibs) { + try zig_args.append("-dead_strip_dylibs"); + } if (self.bundle_compiler_rt) |x| { if (x) { diff --git a/src/Compilation.zig b/src/Compilation.zig index 652938d74110..fffa777f22e5 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -911,6 +911,8 @@ pub const InitOptions = struct { headerpad_size: ?u32 = null, /// (Darwin) set enough space as if all paths were MATPATHLEN headerpad_max_install_names: bool = false, + /// (Darwin) remove dylibs that are unreachable by the entry point or exported symbols + dead_strip_dylibs: bool = false, }; fn addPackageTableToCacheHash( @@ -1754,6 +1756,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { .search_strategy = options.search_strategy, .headerpad_size = options.headerpad_size, .headerpad_max_install_names = options.headerpad_max_install_names, + .dead_strip_dylibs = options.dead_strip_dylibs, }); errdefer bin_file.destroy(); comp.* = .{ @@ -2369,7 +2372,7 @@ fn prepareWholeEmitSubPath(arena: Allocator, opt_emit: ?EmitLoc) error{OutOfMemo /// to remind the programmer to update multiple related pieces of code that /// are in different locations. Bump this number when adding or deleting /// anything from the link cache manifest. -pub const link_hash_implementation_version = 6; +pub const link_hash_implementation_version = 7; fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifest) !void { const gpa = comp.gpa; @@ -2379,7 +2382,7 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - comptime assert(link_hash_implementation_version == 6); + comptime assert(link_hash_implementation_version == 7); if (comp.bin_file.options.module) |mod| { const main_zig_file = try mod.main_pkg.root_src_directory.join(arena, &[_][]const u8{ @@ -2488,6 +2491,7 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes man.hash.addOptional(comp.bin_file.options.search_strategy); man.hash.addOptional(comp.bin_file.options.headerpad_size); man.hash.add(comp.bin_file.options.headerpad_max_install_names); + man.hash.add(comp.bin_file.options.dead_strip_dylibs); // COFF specific stuff man.hash.addOptional(comp.bin_file.options.subsystem); diff --git a/src/link.zig b/src/link.zig index 21d54d531c10..18e10dc74c12 100644 --- a/src/link.zig +++ b/src/link.zig @@ -199,6 +199,9 @@ pub const Options = struct { /// (Darwin) set enough space as if all paths were MATPATHLEN headerpad_max_install_names: bool = false, + /// (Darwin) remove dylibs that are unreachable by the entry point or exported symbols + dead_strip_dylibs: bool = false, + pub fn effectiveOutputMode(options: Options) std.builtin.OutputMode { return if (options.use_lld) .Obj else options.output_mode; } diff --git a/src/link/Coff.zig b/src/link/Coff.zig index 77059a7fd98e..0e7d7c89eefc 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -969,7 +969,7 @@ fn linkWithLLD(self: *Coff, comp: *Compilation, prog_node: *std.Progress.Node) ! man = comp.cache_parent.obtain(); self.base.releaseLock(); - comptime assert(Compilation.link_hash_implementation_version == 6); + comptime assert(Compilation.link_hash_implementation_version == 7); for (self.base.options.objects) |obj| { _ = try man.addFile(obj.path, null); diff --git a/src/link/Elf.zig b/src/link/Elf.zig index 79545d1e1a18..faeb7c9d2770 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -1298,7 +1298,7 @@ fn linkWithLLD(self: *Elf, comp: *Compilation, prog_node: *std.Progress.Node) !v // We are about to obtain this lock, so here we give other processes a chance first. self.base.releaseLock(); - comptime assert(Compilation.link_hash_implementation_version == 6); + comptime assert(Compilation.link_hash_implementation_version == 7); try man.addOptionalFile(self.base.options.linker_script); try man.addOptionalFile(self.base.options.version_script); diff --git a/src/link/MachO.zig b/src/link/MachO.zig index 4ddee493b88f..bd354ba1ce66 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -541,7 +541,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No // We are about to obtain this lock, so here we give other processes a chance first. self.base.releaseLock(); - comptime assert(Compilation.link_hash_implementation_version == 6); + comptime assert(Compilation.link_hash_implementation_version == 7); for (self.base.options.objects) |obj| { _ = try man.addFile(obj.path, null); @@ -558,6 +558,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No man.hash.addOptional(self.base.options.search_strategy); man.hash.addOptional(self.base.options.headerpad_size); man.hash.add(self.base.options.headerpad_max_install_names); + man.hash.add(self.base.options.dead_strip_dylibs); man.hash.addListOfBytes(self.base.options.lib_dirs); man.hash.addListOfBytes(self.base.options.framework_dirs); man.hash.addListOfBytes(self.base.options.frameworks); @@ -987,6 +988,10 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No try argv.append("-headerpad_max_install_names"); } + if (self.base.options.dead_strip_dylibs) { + try argv.append("-dead_strip_dylibs"); + } + if (self.base.options.entry) |entry| { try argv.append("-e"); try argv.append(entry); @@ -1425,7 +1430,12 @@ pub fn parseDylib(self: *MachO, path: []const u8, opts: DylibCreateOpts) ParseDy try self.dylibs.append(self.base.allocator, dylib); try self.dylibs_map.putNoClobber(self.base.allocator, dylib.id.?.name, dylib_id); - if (!(opts.is_dependent or self.referenced_dylibs.contains(dylib_id))) { + const should_link_dylib_even_if_unreachable = blk: { + if (self.base.options.dead_strip_dylibs) break :blk false; + break :blk !(opts.is_dependent or self.referenced_dylibs.contains(dylib_id)); + }; + + if (should_link_dylib_even_if_unreachable) { try self.addLoadDylibLC(dylib_id); try self.referenced_dylibs.putNoClobber(self.base.allocator, dylib_id, {}); } diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index b074799771d8..467aa8962138 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -2546,7 +2546,7 @@ fn linkWithLLD(self: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) ! // We are about to obtain this lock, so here we give other processes a chance first. self.base.releaseLock(); - comptime assert(Compilation.link_hash_implementation_version == 6); + comptime assert(Compilation.link_hash_implementation_version == 7); for (self.base.options.objects) |obj| { _ = try man.addFile(obj.path, null); diff --git a/src/main.zig b/src/main.zig index d63e2353600d..8f9d3e5a13f0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -452,6 +452,7 @@ const usage_build_generic = \\ -search_dylibs_first (Darwin) search `libx.dylib` in each dir in library search paths, then `libx.a` \\ -headerpad [value] (Darwin) set minimum space for future expansion of the load commands in hexadecimal notation \\ -headerpad_max_install_names (Darwin) set enough space as if all paths were MAXPATHLEN + \\ -dead_strip_dylibs (Darwin) remove dylibs that are unreachable by the entry point or exported symbols \\ --import-memory (WebAssembly) import memory from the environment \\ --import-table (WebAssembly) import function table from the host environment \\ --export-table (WebAssembly) export function table to the host environment @@ -703,6 +704,7 @@ fn buildOutputType( var search_strategy: ?link.File.MachO.SearchStrategy = null; var headerpad_size: ?u32 = null; var headerpad_max_install_names: bool = false; + var dead_strip_dylibs: bool = false; // e.g. -m3dnow or -mno-outline-atomics. They correspond to std.Target llvm cpu feature names. // This array is populated by zig cc frontend and then has to be converted to zig-style @@ -937,6 +939,8 @@ fn buildOutputType( }; } else if (mem.eql(u8, arg, "-headerpad_max_install_names")) { headerpad_max_install_names = true; + } else if (mem.eql(u8, arg, "-dead_strip_dylibs")) { + dead_strip_dylibs = true; } else if (mem.eql(u8, arg, "-T") or mem.eql(u8, arg, "--script")) { linker_script = args_iter.next() orelse { fatal("expected parameter after {s}", .{arg}); @@ -1700,6 +1704,8 @@ fn buildOutputType( }; } else if (mem.eql(u8, arg, "-headerpad_max_install_names")) { headerpad_max_install_names = true; + } else if (mem.eql(u8, arg, "-dead_strip_dylibs")) { + dead_strip_dylibs = true; } else if (mem.eql(u8, arg, "--gc-sections")) { linker_gc_sections = true; } else if (mem.eql(u8, arg, "--no-gc-sections")) { @@ -2821,6 +2827,7 @@ fn buildOutputType( .search_strategy = search_strategy, .headerpad_size = headerpad_size, .headerpad_max_install_names = headerpad_max_install_names, + .dead_strip_dylibs = dead_strip_dylibs, }) catch |err| switch (err) { error.LibCUnavailable => { const target = target_info.target; diff --git a/test/link.zig b/test/link.zig index 0c301d6bcbe2..51aef4c496cb 100644 --- a/test/link.zig +++ b/test/link.zig @@ -40,7 +40,7 @@ pub fn addCases(cases: *tests.StandaloneContext) void { .build_modes = true, }); - cases.addBuildFile("test/link/macho/frameworks/build.zig", .{ + cases.addBuildFile("test/link/macho/dead_strip_dylibs/build.zig", .{ .build_modes = true, .requires_macos_sdk = true, }); diff --git a/test/link/macho/dead_strip_dylibs/build.zig b/test/link/macho/dead_strip_dylibs/build.zig new file mode 100644 index 000000000000..2e4b69c22920 --- /dev/null +++ b/test/link/macho/dead_strip_dylibs/build.zig @@ -0,0 +1,46 @@ +const std = @import("std"); +const Builder = std.build.Builder; +const LibExeObjectStep = std.build.LibExeObjStep; + +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + + const test_step = b.step("test", "Test the program"); + + { + // Without -dead_strip_dylibs we expect `-la` to include liba.dylib in the final executable + const exe = createScenario(b, mode); + + const check = exe.checkObject(.macho); + check.checkStart("cmd LOAD_DYLIB"); + check.checkNext("name {*}Cocoa"); + + check.checkStart("cmd LOAD_DYLIB"); + check.checkNext("name {*}libobjc{*}.dylib"); + + test_step.dependOn(&check.step); + + const run_cmd = exe.run(); + test_step.dependOn(&run_cmd.step); + } + + { + // With -dead_strip_dylibs, we should include liba.dylib as it's unreachable + const exe = createScenario(b, mode); + exe.dead_strip_dylibs = true; + + const run_cmd = exe.run(); + run_cmd.expected_exit_code = @bitCast(u8, @as(i8, -2)); // should fail + test_step.dependOn(&run_cmd.step); + } +} + +fn createScenario(b: *Builder, mode: std.builtin.Mode) *LibExeObjectStep { + const exe = b.addExecutable("test", null); + b.default_step.dependOn(&exe.step); + exe.addCSourceFile("main.c", &[0][]const u8{}); + exe.setBuildMode(mode); + exe.linkLibC(); + exe.linkFramework("Cocoa"); + return exe; +} diff --git a/test/link/macho/dead_strip_dylibs/main.c b/test/link/macho/dead_strip_dylibs/main.c new file mode 100644 index 000000000000..836d1b1cc1b2 --- /dev/null +++ b/test/link/macho/dead_strip_dylibs/main.c @@ -0,0 +1,10 @@ +#include + +int main() { + if (objc_getClass("NSObject") == 0) { + return -1; + } + if (objc_getClass("NSApplication") == 0) { + return -2; + } +} diff --git a/test/link/macho/frameworks/build.zig b/test/link/macho/frameworks/build.zig deleted file mode 100644 index 7086606f30e5..000000000000 --- a/test/link/macho/frameworks/build.zig +++ /dev/null @@ -1,32 +0,0 @@ -const std = @import("std"); -const Builder = std.build.Builder; - -pub fn build(b: *Builder) void { - const mode = b.standardReleaseOptions(); - - const test_step = b.step("test", "Test the program"); - - const exe = b.addExecutable("test", null); - b.default_step.dependOn(&exe.step); - exe.addCSourceFile("main.c", &[0][]const u8{}); - exe.setBuildMode(mode); - exe.linkLibC(); - exe.linkFramework("Cocoa"); - - const check = exe.checkObject(.macho); - check.checkStart("cmd LOAD_DYLIB"); - check.checkNext("name {*}Cocoa"); - - switch (mode) { - .Debug, .ReleaseSafe => { - check.checkStart("cmd LOAD_DYLIB"); - check.checkNext("name {*}libobjc{*}.dylib"); - }, - else => {}, - } - - test_step.dependOn(&check.step); - - const run_cmd = exe.run(); - test_step.dependOn(&run_cmd.step); -} diff --git a/test/link/macho/frameworks/main.c b/test/link/macho/frameworks/main.c deleted file mode 100644 index b9dab990b2ff..000000000000 --- a/test/link/macho/frameworks/main.c +++ /dev/null @@ -1,7 +0,0 @@ -#include -#include - -int main() { - assert(objc_getClass("NSObject") > 0); - assert(objc_getClass("NSApplication") > 0); -} From 0dd28920daa5127ffe5a3691343fa519f7547cfd Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Mon, 27 Jun 2022 19:48:10 +0200 Subject: [PATCH 2/2] macho: implement and handle `-needed-*` and `-needed_*` family of flags MachO linker now handles `-needed-l`, `-needed_library=` and `-needed_framework=`. While on macOS `-l` is equivalent to `-needed-l`, and `-framework` to `-needed_framework`, it can be used to the same effect as on Linux if combined with `-dead_strip_dylibs`. This commit also adds handling for `-needed_library` which is macOS specific flag only (in addition to `-needed-l`). Finally, in order to leverage new linker testing harness, this commit added ability to specify lowering to those flags via `build.zig`: `linkSystemLibraryNeeded` (and related), and `linkFrameworkNeeded`. --- lib/std/build.zig | 72 +++++++++++++++++--- src/Compilation.zig | 8 +-- src/link.zig | 2 +- src/link/MachO.zig | 74 +++++++++++++-------- src/main.zig | 40 ++++++++--- test/link.zig | 9 +++ test/link/macho/dead_strip_dylibs/build.zig | 2 +- test/link/macho/dead_strip_dylibs/main.c | 3 +- test/link/macho/dylib/main.c | 2 +- test/link/macho/needed_framework/build.zig | 27 ++++++++ test/link/macho/needed_framework/main.c | 3 + test/link/macho/needed_l/a.c | 1 + test/link/macho/needed_l/build.zig | 35 ++++++++++ test/link/macho/needed_l/main.c | 3 + test/link/macho/search_strategy/main.c | 2 +- 15 files changed, 227 insertions(+), 56 deletions(-) create mode 100644 test/link/macho/needed_framework/build.zig create mode 100644 test/link/macho/needed_framework/main.c create mode 100644 test/link/macho/needed_l/a.c create mode 100644 test/link/macho/needed_l/build.zig create mode 100644 test/link/macho/needed_l/main.c diff --git a/lib/std/build.zig b/lib/std/build.zig index da5ffb33bde3..ab4472d01ee4 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -11,7 +11,6 @@ const ArrayList = std.ArrayList; const StringHashMap = std.StringHashMap; const Allocator = mem.Allocator; const process = std.process; -const BufSet = std.BufSet; const EnvMap = std.process.EnvMap; const fmt_lib = std.fmt; const File = std.fs.File; @@ -1484,7 +1483,7 @@ pub const LibExeObjStep = struct { lib_paths: ArrayList([]const u8), rpaths: ArrayList([]const u8), framework_dirs: ArrayList([]const u8), - frameworks: BufSet, + frameworks: StringHashMap(bool), verbose_link: bool, verbose_cc: bool, emit_analysis: EmitOption = .default, @@ -1643,6 +1642,7 @@ pub const LibExeObjStep = struct { pub const SystemLib = struct { name: []const u8, + needed: bool, use_pkg_config: enum { /// Don't use pkg-config, just pass -lfoo where foo is name. no, @@ -1744,7 +1744,7 @@ pub const LibExeObjStep = struct { .kind = kind, .root_src = root_src, .name = name, - .frameworks = BufSet.init(builder.allocator), + .frameworks = StringHashMap(bool).init(builder.allocator), .step = Step.init(base_id, name, builder.allocator, make), .version = ver, .out_filename = undefined, @@ -1893,8 +1893,11 @@ pub const LibExeObjStep = struct { } pub fn linkFramework(self: *LibExeObjStep, framework_name: []const u8) void { - // Note: No need to dupe because frameworks dupes internally. - self.frameworks.insert(framework_name) catch unreachable; + self.frameworks.put(self.builder.dupe(framework_name), false) catch unreachable; + } + + pub fn linkFrameworkNeeded(self: *LibExeObjStep, framework_name: []const u8) void { + self.frameworks.put(self.builder.dupe(framework_name), true) catch unreachable; } /// Returns whether the library, executable, or object depends on a particular system library. @@ -1935,6 +1938,7 @@ pub const LibExeObjStep = struct { self.link_objects.append(.{ .system_lib = .{ .name = "c", + .needed = false, .use_pkg_config = .no, }, }) catch unreachable; @@ -1947,6 +1951,7 @@ pub const LibExeObjStep = struct { self.link_objects.append(.{ .system_lib = .{ .name = "c++", + .needed = false, .use_pkg_config = .no, }, }) catch unreachable; @@ -1971,6 +1976,19 @@ pub const LibExeObjStep = struct { self.link_objects.append(.{ .system_lib = .{ .name = self.builder.dupe(name), + .needed = false, + .use_pkg_config = .no, + }, + }) catch unreachable; + } + + /// This one has no integration with anything, it just puts -needed-lname on the command line. + /// Prefer to use `linkSystemLibraryNeeded` instead. + pub fn linkSystemLibraryNeededName(self: *LibExeObjStep, name: []const u8) void { + self.link_objects.append(.{ + .system_lib = .{ + .name = self.builder.dupe(name), + .needed = true, .use_pkg_config = .no, }, }) catch unreachable; @@ -1982,6 +2000,19 @@ pub const LibExeObjStep = struct { self.link_objects.append(.{ .system_lib = .{ .name = self.builder.dupe(lib_name), + .needed = false, + .use_pkg_config = .force, + }, + }) catch unreachable; + } + + /// This links against a system library, exclusively using pkg-config to find the library. + /// Prefer to use `linkSystemLibraryNeeded` instead. + pub fn linkSystemLibraryNeededPkgConfigOnly(self: *LibExeObjStep, lib_name: []const u8) void { + self.link_objects.append(.{ + .system_lib = .{ + .name = self.builder.dupe(lib_name), + .needed = true, .use_pkg_config = .force, }, }) catch unreachable; @@ -2084,6 +2115,14 @@ pub const LibExeObjStep = struct { } pub fn linkSystemLibrary(self: *LibExeObjStep, name: []const u8) void { + self.linkSystemLibraryInner(name, false); + } + + pub fn linkSystemLibraryNeeded(self: *LibExeObjStep, name: []const u8) void { + self.linkSystemLibraryInner(name, true); + } + + fn linkSystemLibraryInner(self: *LibExeObjStep, name: []const u8, needed: bool) void { if (isLibCLibrary(name)) { self.linkLibC(); return; @@ -2096,6 +2135,7 @@ pub const LibExeObjStep = struct { self.link_objects.append(.{ .system_lib = .{ .name = self.builder.dupe(name), + .needed = needed, .use_pkg_config = .yes, }, }) catch unreachable; @@ -2437,7 +2477,7 @@ pub const LibExeObjStep = struct { if (!other.isDynamicLibrary()) { var it = other.frameworks.iterator(); while (it.next()) |framework| { - self.frameworks.insert(framework.*) catch unreachable; + self.frameworks.put(framework.key_ptr.*, framework.value_ptr.*) catch unreachable; } } }, @@ -2473,8 +2513,9 @@ pub const LibExeObjStep = struct { }, .system_lib => |system_lib| { + const prefix: []const u8 = if (system_lib.needed) "-needed-l" else "-l"; switch (system_lib.use_pkg_config) { - .no => try zig_args.append(builder.fmt("-l{s}", .{system_lib.name})), + .no => try zig_args.append(builder.fmt("{s}{s}", .{ prefix, system_lib.name })), .yes, .force => { if (self.runPkgConfig(system_lib.name)) |args| { try zig_args.appendSlice(args); @@ -2488,7 +2529,10 @@ pub const LibExeObjStep = struct { .yes => { // pkg-config failed, so fall back to linking the library // by name directly. - try zig_args.append(builder.fmt("-l{s}", .{system_lib.name})); + try zig_args.append(builder.fmt("{s}{s}", .{ + prefix, + system_lib.name, + })); }, .force => { panic("pkg-config failed for library {s}", .{system_lib.name}); @@ -2972,9 +3016,15 @@ pub const LibExeObjStep = struct { } var it = self.frameworks.iterator(); - while (it.next()) |framework| { - zig_args.append("-framework") catch unreachable; - zig_args.append(framework.*) catch unreachable; + while (it.next()) |entry| { + const name = entry.key_ptr.*; + const needed = entry.value_ptr.*; + if (needed) { + zig_args.append("-needed_framework") catch unreachable; + } else { + zig_args.append("-framework") catch unreachable; + } + zig_args.append(name) catch unreachable; } } else { if (self.framework_dirs.items.len > 0) { diff --git a/src/Compilation.zig b/src/Compilation.zig index fffa777f22e5..01f6a15e2c1d 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -791,7 +791,7 @@ pub const InitOptions = struct { c_source_files: []const CSourceFile = &[0]CSourceFile{}, link_objects: []LinkObject = &[0]LinkObject{}, framework_dirs: []const []const u8 = &[0][]const u8{}, - frameworks: []const []const u8 = &[0][]const u8{}, + frameworks: std.StringArrayHashMapUnmanaged(SystemLib) = .{}, system_lib_names: []const []const u8 = &.{}, system_lib_infos: []const SystemLib = &.{}, /// These correspond to the WASI libc emulated subcomponents including: @@ -1097,7 +1097,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { // Our linker can't handle objects or most advanced options yet. if (options.link_objects.len != 0 or options.c_source_files.len != 0 or - options.frameworks.len != 0 or + options.frameworks.count() != 0 or options.system_lib_names.len != 0 or options.link_libc or options.link_libcpp or link_eh_frame_hdr or @@ -1215,7 +1215,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { options.target, options.is_native_abi, link_libc, - options.system_lib_names.len != 0 or options.frameworks.len != 0, + options.system_lib_names.len != 0 or options.frameworks.count() != 0, options.libc_installation, options.native_darwin_sdk != null, ); @@ -2485,7 +2485,7 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes // Mach-O specific stuff man.hash.addListOfBytes(comp.bin_file.options.framework_dirs); - man.hash.addListOfBytes(comp.bin_file.options.frameworks); + link.hashAddSystemLibs(&man.hash, comp.bin_file.options.frameworks); try man.addOptionalFile(comp.bin_file.options.entitlements); man.hash.addOptional(comp.bin_file.options.pagezero_size); man.hash.addOptional(comp.bin_file.options.search_strategy); diff --git a/src/link.zig b/src/link.zig index 18e10dc74c12..c3d1d216c057 100644 --- a/src/link.zig +++ b/src/link.zig @@ -162,7 +162,7 @@ pub const Options = struct { objects: []Compilation.LinkObject, framework_dirs: []const []const u8, - frameworks: []const []const u8, + frameworks: std.StringArrayHashMapUnmanaged(SystemLib), system_libs: std.StringArrayHashMapUnmanaged(SystemLib), wasi_emulated_libs: []const wasi_libc.CRTFile, lib_dirs: []const []const u8, diff --git a/src/link/MachO.zig b/src/link/MachO.zig index bd354ba1ce66..d660d2ce7e83 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -561,7 +561,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No man.hash.add(self.base.options.dead_strip_dylibs); man.hash.addListOfBytes(self.base.options.lib_dirs); man.hash.addListOfBytes(self.base.options.framework_dirs); - man.hash.addListOfBytes(self.base.options.frameworks); + link.hashAddSystemLibs(&man.hash, self.base.options.frameworks); man.hash.addListOfBytes(self.base.options.rpath_list); if (is_dyn_lib) { man.hash.addOptionalBytes(self.base.options.install_name); @@ -768,19 +768,20 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No } // Shared and static libraries passed via `-l` flag. - var search_lib_names = std.ArrayList([]const u8).init(arena); + var candidate_libs = std.StringArrayHashMap(Compilation.SystemLib).init(arena); - const system_libs = self.base.options.system_libs.keys(); - for (system_libs) |link_lib| { + const system_lib_names = self.base.options.system_libs.keys(); + for (system_lib_names) |system_lib_name| { // 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 .dylib files, in which // case we want to avoid prepending "-l". - if (Compilation.classifyFileExt(link_lib) == .shared_library) { - try positionals.append(link_lib); + if (Compilation.classifyFileExt(system_lib_name) == .shared_library) { + try positionals.append(system_lib_name); continue; } - try search_lib_names.append(link_lib); + const system_lib_info = self.base.options.system_libs.get(system_lib_name).?; + try candidate_libs.put(system_lib_name, system_lib_info); } var lib_dirs = std.ArrayList([]const u8).init(arena); @@ -792,18 +793,18 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No } } - var libs = std.ArrayList([]const u8).init(arena); + var libs = std.StringArrayHashMap(Compilation.SystemLib).init(arena); // Assume ld64 default -search_paths_first if no strategy specified. const search_strategy = self.base.options.search_strategy orelse .paths_first; - outer: for (search_lib_names.items) |lib_name| { + outer: for (candidate_libs.keys()) |lib_name| { switch (search_strategy) { .paths_first => { // Look in each directory for a dylib (stub first), and then for archive for (lib_dirs.items) |dir| { for (&[_][]const u8{ ".tbd", ".dylib", ".a" }) |ext| { if (try resolveLib(arena, dir, lib_name, ext)) |full_path| { - try libs.append(full_path); + try libs.put(full_path, candidate_libs.get(lib_name).?); continue :outer; } } @@ -817,13 +818,13 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No for (lib_dirs.items) |dir| { for (&[_][]const u8{ ".tbd", ".dylib" }) |ext| { if (try resolveLib(arena, dir, lib_name, ext)) |full_path| { - try libs.append(full_path); + try libs.put(full_path, candidate_libs.get(lib_name).?); continue :outer; } } } else for (lib_dirs.items) |dir| { if (try resolveLib(arena, dir, lib_name, ".a")) |full_path| { - try libs.append(full_path); + try libs.put(full_path, candidate_libs.get(lib_name).?); } else { log.warn("library not found for '-l{s}'", .{lib_name}); lib_not_found = true; @@ -847,7 +848,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No // re-exports every single symbol definition. for (lib_dirs.items) |dir| { if (try resolveLib(arena, dir, "System", ".tbd")) |full_path| { - try libs.append(full_path); + try libs.put(full_path, .{ .needed = false }); libsystem_available = true; break :blk; } @@ -857,8 +858,8 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No for (lib_dirs.items) |dir| { if (try resolveLib(arena, dir, "System", ".dylib")) |libsystem_path| { if (try resolveLib(arena, dir, "c", ".dylib")) |libc_path| { - try libs.append(libsystem_path); - try libs.append(libc_path); + try libs.put(libsystem_path, .{ .needed = false }); + try libs.put(libc_path, .{ .needed = false }); libsystem_available = true; break :blk; } @@ -872,7 +873,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No const full_path = try comp.zig_lib_directory.join(arena, &[_][]const u8{ "libc", "darwin", libsystem_name, }); - try libs.append(full_path); + try libs.put(full_path, .{ .needed = false }); } // frameworks @@ -885,16 +886,16 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No } } - outer: for (self.base.options.frameworks) |framework| { + outer: for (self.base.options.frameworks.keys()) |f_name| { for (framework_dirs.items) |dir| { for (&[_][]const u8{ ".tbd", ".dylib", "" }) |ext| { - if (try resolveFramework(arena, dir, framework, ext)) |full_path| { - try libs.append(full_path); + if (try resolveFramework(arena, dir, f_name, ext)) |full_path| { + try libs.put(full_path, self.base.options.frameworks.get(f_name).?); continue :outer; } } } else { - log.warn("framework not found for '-framework {s}'", .{framework}); + log.warn("framework not found for '-framework {s}'", .{f_name}); framework_not_found = true; } } @@ -1025,15 +1026,25 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No try argv.append("-lc"); for (self.base.options.system_libs.keys()) |l_name| { - try argv.append(try std.fmt.allocPrint(arena, "-l{s}", .{l_name})); + const needed = self.base.options.system_libs.get(l_name).?.needed; + const arg = if (needed) + try std.fmt.allocPrint(arena, "-needed-l{s}", .{l_name}) + else + try std.fmt.allocPrint(arena, "-l{s}", .{l_name}); + try argv.append(arg); } for (self.base.options.lib_dirs) |lib_dir| { try argv.append(try std.fmt.allocPrint(arena, "-L{s}", .{lib_dir})); } - for (self.base.options.frameworks) |framework| { - try argv.append(try std.fmt.allocPrint(arena, "-framework {s}", .{framework})); + for (self.base.options.frameworks.keys()) |framework| { + const needed = self.base.options.frameworks.get(framework).?.needed; + const arg = if (needed) + try std.fmt.allocPrint(arena, "-needed_framework {s}", .{framework}) + else + try std.fmt.allocPrint(arena, "-framework {s}", .{framework}); + try argv.append(arg); } for (self.base.options.framework_dirs) |framework_dir| { @@ -1056,7 +1067,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No defer dependent_libs.deinit(); try self.parseInputFiles(positionals.items, self.base.options.sysroot, &dependent_libs); try self.parseAndForceLoadStaticArchives(must_link_archives.keys()); - try self.parseLibs(libs.items, self.base.options.sysroot, &dependent_libs); + try self.parseLibs(libs.keys(), libs.values(), self.base.options.sysroot, &dependent_libs); try self.parseDependentLibs(self.base.options.sysroot, &dependent_libs); } @@ -1381,6 +1392,7 @@ const DylibCreateOpts = struct { dependent_libs: *std.fifo.LinearFifo(Dylib.Id, .Dynamic), id: ?Dylib.Id = null, is_dependent: bool = false, + is_needed: bool = false, }; pub fn parseDylib(self: *MachO, path: []const u8, opts: DylibCreateOpts) ParseDylibError!bool { @@ -1431,7 +1443,7 @@ pub fn parseDylib(self: *MachO, path: []const u8, opts: DylibCreateOpts) ParseDy try self.dylibs_map.putNoClobber(self.base.allocator, dylib.id.?.name, dylib_id); const should_link_dylib_even_if_unreachable = blk: { - if (self.base.options.dead_strip_dylibs) break :blk false; + if (self.base.options.dead_strip_dylibs and !opts.is_needed) break :blk false; break :blk !(opts.is_dependent or self.referenced_dylibs.contains(dylib_id)); }; @@ -1479,12 +1491,20 @@ fn parseAndForceLoadStaticArchives(self: *MachO, files: []const []const u8) !voi } } -fn parseLibs(self: *MachO, libs: []const []const u8, syslibroot: ?[]const u8, dependent_libs: anytype) !void { - for (libs) |lib| { +fn parseLibs( + self: *MachO, + lib_names: []const []const u8, + lib_infos: []const Compilation.SystemLib, + syslibroot: ?[]const u8, + dependent_libs: anytype, +) !void { + for (lib_names) |lib, i| { + const lib_info = lib_infos[i]; log.debug("parsing lib path '{s}'", .{lib}); if (try self.parseDylib(lib, .{ .syslibroot = syslibroot, .dependent_libs = dependent_libs, + .is_needed = lib_info.needed, })) continue; if (try self.parseArchive(lib, false)) continue; diff --git a/src/main.zig b/src/main.zig index 8f9d3e5a13f0..cbfd403352f5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -444,6 +444,8 @@ const usage_build_generic = \\ --stack [size] Override default stack size \\ --image-base [addr] Set base address for executable image \\ -framework [name] (Darwin) link against framework + \\ -needed_framework [name] (Darwin) link against framework (even if unused) + \\ -needed_library [lib] (Darwin) link against system library (even if unused) \\ -F[dir] (Darwin) add search path for frameworks \\ -install_name=[value] (Darwin) add dylib's install name \\ --entitlements [path] (Darwin) add path to entitlements file for embedding in code signature @@ -750,8 +752,7 @@ fn buildOutputType( var framework_dirs = std.ArrayList([]const u8).init(gpa); defer framework_dirs.deinit(); - var frameworks = std.ArrayList([]const u8).init(gpa); - defer frameworks.deinit(); + var frameworks: std.StringArrayHashMapUnmanaged(Compilation.SystemLib) = .{}; // null means replace with the test executable binary var test_exec_args = std.ArrayList(?[]const u8).init(gpa); @@ -912,9 +913,15 @@ fn buildOutputType( fatal("expected parameter after {s}", .{arg}); }); } else if (mem.eql(u8, arg, "-framework")) { - try frameworks.append(args_iter.next() orelse { + const path = args_iter.next() orelse { fatal("expected parameter after {s}", .{arg}); - }); + }; + try frameworks.put(gpa, path, .{ .needed = false }); + } else if (mem.eql(u8, arg, "-needed_framework")) { + const path = args_iter.next() orelse { + fatal("expected parameter after {s}", .{arg}); + }; + try frameworks.put(gpa, path, .{ .needed = true }); } else if (mem.eql(u8, arg, "-install_name")) { install_name = args_iter.next() orelse { fatal("expected parameter after {s}", .{arg}); @@ -956,7 +963,10 @@ 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. try system_libs.put(next_arg, .{ .needed = false }); - } else if (mem.eql(u8, arg, "--needed-library") or mem.eql(u8, arg, "-needed-l")) { + } else if (mem.eql(u8, arg, "--needed-library") or + mem.eql(u8, arg, "-needed-l") or + mem.eql(u8, arg, "--needed_library")) + { const next_arg = args_iter.next() orelse { fatal("expected parameter after {s}", .{arg}); }; @@ -1586,7 +1596,7 @@ fn buildOutputType( try clang_argv.appendSlice(it.other_args); }, .framework_dir => try framework_dirs.append(it.only_arg), - .framework => try frameworks.append(it.only_arg), + .framework => try frameworks.put(gpa, it.only_arg, .{ .needed = false }), .nostdlibinc => want_native_include_dirs = false, .strip => strip = true, .exec_model => { @@ -1874,7 +1884,19 @@ fn buildOutputType( if (i >= linker_args.items.len) { fatal("expected linker arg after '{s}'", .{arg}); } - try frameworks.append(linker_args.items[i]); + try frameworks.put(gpa, linker_args.items[i], .{ .needed = false }); + } else if (mem.eql(u8, arg, "-needed_framework")) { + i += 1; + if (i >= linker_args.items.len) { + fatal("expected linker arg after '{s}'", .{arg}); + } + try frameworks.put(gpa, linker_args.items[i], .{ .needed = true }); + } else if (mem.eql(u8, arg, "-needed_library")) { + i += 1; + if (i >= linker_args.items.len) { + fatal("expected linker arg after '{s}'", .{arg}); + } + try system_libs.put(linker_args.items[i], .{ .needed = true }); } else if (mem.eql(u8, arg, "-compatibility_version")) { i += 1; if (i >= linker_args.items.len) { @@ -2244,7 +2266,7 @@ fn buildOutputType( if (comptime builtin.target.isDarwin()) { // If we want to link against frameworks, we need system headers. - if (framework_dirs.items.len > 0 or frameworks.items.len > 0) + if (framework_dirs.items.len > 0 or frameworks.count() > 0) want_native_include_dirs = true; } @@ -2734,7 +2756,7 @@ fn buildOutputType( .c_source_files = c_source_files.items, .link_objects = link_objects.items, .framework_dirs = framework_dirs.items, - .frameworks = frameworks.items, + .frameworks = frameworks, .system_lib_names = system_libs.keys(), .system_lib_infos = system_libs.values(), .wasi_emulated_libs = wasi_emulated_libs.items, diff --git a/test/link.zig b/test/link.zig index 51aef4c496cb..6878881a6686 100644 --- a/test/link.zig +++ b/test/link.zig @@ -45,6 +45,15 @@ pub fn addCases(cases: *tests.StandaloneContext) void { .requires_macos_sdk = true, }); + cases.addBuildFile("test/link/macho/needed_l/build.zig", .{ + .build_modes = true, + }); + + cases.addBuildFile("test/link/macho/needed_framework/build.zig", .{ + .build_modes = true, + .requires_macos_sdk = true, + }); + // Try to build and run an Objective-C executable. cases.addBuildFile("test/link/macho/objc/build.zig", .{ .build_modes = true, diff --git a/test/link/macho/dead_strip_dylibs/build.zig b/test/link/macho/dead_strip_dylibs/build.zig index 2e4b69c22920..efdaf191bd5a 100644 --- a/test/link/macho/dead_strip_dylibs/build.zig +++ b/test/link/macho/dead_strip_dylibs/build.zig @@ -6,6 +6,7 @@ pub fn build(b: *Builder) void { const mode = b.standardReleaseOptions(); const test_step = b.step("test", "Test the program"); + test_step.dependOn(b.getInstallStep()); { // Without -dead_strip_dylibs we expect `-la` to include liba.dylib in the final executable @@ -37,7 +38,6 @@ pub fn build(b: *Builder) void { fn createScenario(b: *Builder, mode: std.builtin.Mode) *LibExeObjectStep { const exe = b.addExecutable("test", null); - b.default_step.dependOn(&exe.step); exe.addCSourceFile("main.c", &[0][]const u8{}); exe.setBuildMode(mode); exe.linkLibC(); diff --git a/test/link/macho/dead_strip_dylibs/main.c b/test/link/macho/dead_strip_dylibs/main.c index 836d1b1cc1b2..06668f552203 100644 --- a/test/link/macho/dead_strip_dylibs/main.c +++ b/test/link/macho/dead_strip_dylibs/main.c @@ -1,10 +1,11 @@ #include -int main() { +int main(int argc, char* argv[]) { if (objc_getClass("NSObject") == 0) { return -1; } if (objc_getClass("NSApplication") == 0) { return -2; } + return 0; } diff --git a/test/link/macho/dylib/main.c b/test/link/macho/dylib/main.c index be1647ddad88..941903f219b5 100644 --- a/test/link/macho/dylib/main.c +++ b/test/link/macho/dylib/main.c @@ -3,7 +3,7 @@ char* hello(); extern char world[]; -int main() { +int main(int argc, char* argv[]) { printf("%s %s", hello(), world); return 0; } diff --git a/test/link/macho/needed_framework/build.zig b/test/link/macho/needed_framework/build.zig new file mode 100644 index 000000000000..43159359412e --- /dev/null +++ b/test/link/macho/needed_framework/build.zig @@ -0,0 +1,27 @@ +const std = @import("std"); +const Builder = std.build.Builder; +const LibExeObjectStep = std.build.LibExeObjStep; + +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + + const test_step = b.step("test", "Test the program"); + test_step.dependOn(b.getInstallStep()); + + // -dead_strip_dylibs + // -needed_framework Cocoa + const exe = b.addExecutable("test", null); + exe.addCSourceFile("main.c", &[0][]const u8{}); + exe.setBuildMode(mode); + exe.linkLibC(); + exe.linkFrameworkNeeded("Cocoa"); + exe.dead_strip_dylibs = true; + + const check = exe.checkObject(.macho); + check.checkStart("cmd LOAD_DYLIB"); + check.checkNext("name {*}Cocoa"); + test_step.dependOn(&check.step); + + const run_cmd = exe.run(); + test_step.dependOn(&run_cmd.step); +} diff --git a/test/link/macho/needed_framework/main.c b/test/link/macho/needed_framework/main.c new file mode 100644 index 000000000000..ca68d24cc704 --- /dev/null +++ b/test/link/macho/needed_framework/main.c @@ -0,0 +1,3 @@ +int main(int argc, char* argv[]) { + return 0; +} diff --git a/test/link/macho/needed_l/a.c b/test/link/macho/needed_l/a.c new file mode 100644 index 000000000000..4bcf8c97861b --- /dev/null +++ b/test/link/macho/needed_l/a.c @@ -0,0 +1 @@ +int a = 42; diff --git a/test/link/macho/needed_l/build.zig b/test/link/macho/needed_l/build.zig new file mode 100644 index 000000000000..708a09dc32e2 --- /dev/null +++ b/test/link/macho/needed_l/build.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const Builder = std.build.Builder; +const LibExeObjectStep = std.build.LibExeObjStep; + +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + + const test_step = b.step("test", "Test the program"); + test_step.dependOn(b.getInstallStep()); + + const dylib = b.addSharedLibrary("a", null, b.version(1, 0, 0)); + dylib.setBuildMode(mode); + dylib.addCSourceFile("a.c", &.{}); + dylib.linkLibC(); + dylib.install(); + + // -dead_strip_dylibs + // -needed-la + const exe = b.addExecutable("test", null); + exe.addCSourceFile("main.c", &[0][]const u8{}); + exe.setBuildMode(mode); + exe.linkLibC(); + exe.linkSystemLibraryNeeded("a"); + exe.addLibraryPath(b.pathFromRoot("zig-out/lib")); + exe.addRPath(b.pathFromRoot("zig-out/lib")); + exe.dead_strip_dylibs = true; + + const check = exe.checkObject(.macho); + check.checkStart("cmd LOAD_DYLIB"); + check.checkNext("name @rpath/liba.dylib"); + test_step.dependOn(&check.step); + + const run_cmd = exe.run(); + test_step.dependOn(&run_cmd.step); +} diff --git a/test/link/macho/needed_l/main.c b/test/link/macho/needed_l/main.c new file mode 100644 index 000000000000..ca68d24cc704 --- /dev/null +++ b/test/link/macho/needed_l/main.c @@ -0,0 +1,3 @@ +int main(int argc, char* argv[]) { + return 0; +} diff --git a/test/link/macho/search_strategy/main.c b/test/link/macho/search_strategy/main.c index be1647ddad88..941903f219b5 100644 --- a/test/link/macho/search_strategy/main.c +++ b/test/link/macho/search_strategy/main.c @@ -3,7 +3,7 @@ char* hello(); extern char world[]; -int main() { +int main(int argc, char* argv[]) { printf("%s %s", hello(), world); return 0; }