Commit efc5c97bff

Jakub Konka <kubkon@jakubkonka.com>
2022-06-27 09:24:18
macho: implement -dead_strip_dylibs linker flag
1 parent a76775b
Changed files (13)
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) {
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);
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);
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, {});
     }
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);
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);
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;
     }
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;
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;
+}
test/link/macho/dead_strip_dylibs/main.c
@@ -0,0 +1,10 @@
+#include <objc/runtime.h>
+
+int main() {
+  if (objc_getClass("NSObject") == 0) {
+    return -1;
+  }
+  if (objc_getClass("NSApplication") == 0) {
+    return -2;
+  }
+}
test/link/macho/frameworks/build.zig
@@ -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);
-}
test/link/macho/frameworks/main.c
@@ -1,7 +0,0 @@
-#include <assert.h>
-#include <objc/runtime.h>
-
-int main() {
-  assert(objc_getClass("NSObject") > 0);
-  assert(objc_getClass("NSApplication") > 0);
-}
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,
         });