Commit 0dd28920da

Jakub Konka <kubkon@jakubkonka.com>
2022-06-27 19:48:10
macho: implement and handle `-needed-*` and `-needed_*` family of flags
MachO linker now handles `-needed-l<name>`, `-needed_library=<name>` and `-needed_framework=<name>`. 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`.
1 parent efc5c97
Changed files (15)
lib
src
test
link
macho
dead_strip_dylibs
dylib
needed_framework
needed_l
search_strategy
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) {
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;
 
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);
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,
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,
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();
test/link/macho/dead_strip_dylibs/main.c
@@ -1,10 +1,11 @@
 #include <objc/runtime.h>
 
-int main() {
+int main(int argc, char* argv[]) {
   if (objc_getClass("NSObject") == 0) {
     return -1;
   }
   if (objc_getClass("NSApplication") == 0) {
     return -2;
   }
+  return 0;
 }
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;
 }
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);
+}
test/link/macho/needed_framework/main.c
@@ -0,0 +1,3 @@
+int main(int argc, char* argv[]) {
+  return 0;
+}
test/link/macho/needed_l/a.c
@@ -0,0 +1,1 @@
+int a = 42;
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);
+}
test/link/macho/needed_l/main.c
@@ -0,0 +1,3 @@
+int main(int argc, char* argv[]) {
+  return 0;
+}
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;
 }
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,