From efc5c97bff87d4c28ae9642fe69d9bc2c7e9eeb7 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Mon, 27 Jun 2022 09:24:18 +0200 Subject: [PATCH] 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 968ee043bf..da5ffb33bd 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 652938d741..fffa777f22 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 21d54d531c..18e10dc74c 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 77059a7fd9..0e7d7c89ee 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 79545d1e1a..faeb7c9d27 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 4ddee493b8..bd354ba1ce 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 b074799771..467aa89621 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 d63e235360..8f9d3e5a13 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 0c301d6bcb..51aef4c496 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 0000000000..2e4b69c229 --- /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 0000000000..836d1b1cc1 --- /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 7086606f30..0000000000 --- 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 b9dab990b2..0000000000 --- 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); -}