Commit f353b59efa

Jakub Konka <kubkon@jakubkonka.com>
2022-06-27 23:56:45
macho: discriminate between normal and weak dylibs
Parse `-weak-lx` and `-weak_framework x` in the CLI.
1 parent 1188415
Changed files (5)
lib/std/macho.zig
@@ -2085,11 +2085,13 @@ pub fn GenericCommandWithData(comptime Cmd: type) type {
 
 pub fn createLoadDylibCommand(
     allocator: Allocator,
+    cmd_id: LC,
     name: []const u8,
     timestamp: u32,
     current_version: u32,
     compatibility_version: u32,
 ) !GenericCommandWithData(dylib_command) {
+    assert(cmd_id == .LOAD_DYLIB or cmd_id == .LOAD_WEAK_DYLIB or cmd_id == .REEXPORT_DYLIB or cmd_id == .ID_DYLIB);
     const cmdsize = @intCast(u32, mem.alignForwardGeneric(
         u64,
         @sizeOf(dylib_command) + name.len + 1, // +1 for nul
@@ -2097,7 +2099,7 @@ pub fn createLoadDylibCommand(
     ));
 
     var dylib_cmd = emptyGenericCommandWithData(dylib_command{
-        .cmd = .LOAD_DYLIB,
+        .cmd = cmd_id,
         .cmdsize = cmdsize,
         .dylib = .{
             .name = @sizeOf(dylib_command),
src/link/MachO/Dylib.zig
@@ -30,6 +30,7 @@ dysymtab_cmd_index: ?u16 = null,
 id_cmd_index: ?u16 = null,
 
 id: ?Id = null,
+weak: bool = false,
 
 /// Parsed symbol table represented as hash map of symbols'
 /// names. We can and should defer creating *Symbols until
@@ -141,7 +142,13 @@ pub fn deinit(self: *Dylib, allocator: Allocator) void {
     }
 }
 
-pub fn parse(self: *Dylib, allocator: Allocator, target: std.Target, dependent_libs: anytype) !void {
+pub fn parse(
+    self: *Dylib,
+    allocator: Allocator,
+    target: std.Target,
+    dylib_id: u16,
+    dependent_libs: anytype,
+) !void {
     log.debug("parsing shared library '{s}'", .{self.name});
 
     self.library_offset = try fat.getLibraryOffset(self.file.reader(), target);
@@ -163,12 +170,18 @@ pub fn parse(self: *Dylib, allocator: Allocator, target: std.Target, dependent_l
         return error.MismatchedCpuArchitecture;
     }
 
-    try self.readLoadCommands(allocator, reader, dependent_libs);
+    try self.readLoadCommands(allocator, reader, dylib_id, dependent_libs);
     try self.parseId(allocator);
     try self.parseSymbols(allocator);
 }
 
-fn readLoadCommands(self: *Dylib, allocator: Allocator, reader: anytype, dependent_libs: anytype) !void {
+fn readLoadCommands(
+    self: *Dylib,
+    allocator: Allocator,
+    reader: anytype,
+    dylib_id: u16,
+    dependent_libs: anytype,
+) !void {
     const should_lookup_reexports = self.header.?.flags & macho.MH_NO_REEXPORTED_DYLIBS == 0;
 
     try self.load_commands.ensureUnusedCapacity(allocator, self.header.?.ncmds);
@@ -190,7 +203,7 @@ fn readLoadCommands(self: *Dylib, allocator: Allocator, reader: anytype, depende
                 if (should_lookup_reexports) {
                     // Parse install_name to dependent dylib.
                     var id = try Id.fromLoadCommand(allocator, cmd.dylib);
-                    try dependent_libs.writeItem(id);
+                    try dependent_libs.writeItem(.{ .id = id, .parent = dylib_id });
                 }
             },
             else => {
@@ -338,6 +351,7 @@ pub fn parseFromStub(
     allocator: Allocator,
     target: std.Target,
     lib_stub: LibStub,
+    dylib_id: u16,
     dependent_libs: anytype,
 ) !void {
     if (lib_stub.inner.len == 0) return error.EmptyStubFile;
@@ -417,7 +431,7 @@ pub fn parseFromStub(
                                 log.debug("  (found re-export '{s}')", .{lib});
 
                                 var dep_id = try Id.default(allocator, lib);
-                                try dependent_libs.writeItem(dep_id);
+                                try dependent_libs.writeItem(.{ .id = dep_id, .parent = dylib_id });
                             }
                         }
                     }
@@ -522,7 +536,7 @@ pub fn parseFromStub(
                     log.debug("  (found re-export '{s}')", .{lib});
 
                     var dep_id = try Id.default(allocator, lib);
-                    try dependent_libs.writeItem(dep_id);
+                    try dependent_libs.writeItem(.{ .id = dep_id, .parent = dylib_id });
                 }
             }
         }
src/link/MachO.zig
@@ -52,6 +52,11 @@ pub const SearchStrategy = enum {
     dylibs_first,
 };
 
+const SystemLib = struct {
+    needed: bool = false,
+    weak: bool = false,
+};
+
 base: File,
 
 /// If this is not null, an object file is created by LLVM and linked with LLD afterwards.
@@ -768,7 +773,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
             }
 
             // Shared and static libraries passed via `-l` flag.
-            var candidate_libs = std.StringArrayHashMap(Compilation.SystemLib).init(arena);
+            var candidate_libs = std.StringArrayHashMap(SystemLib).init(arena);
 
             const system_lib_names = self.base.options.system_libs.keys();
             for (system_lib_names) |system_lib_name| {
@@ -781,7 +786,10 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
                 }
 
                 const system_lib_info = self.base.options.system_libs.get(system_lib_name).?;
-                try candidate_libs.put(system_lib_name, system_lib_info);
+                try candidate_libs.put(system_lib_name, .{
+                    .needed = system_lib_info.needed,
+                    .weak = system_lib_info.weak,
+                });
             }
 
             var lib_dirs = std.ArrayList([]const u8).init(arena);
@@ -793,7 +801,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
                 }
             }
 
-            var libs = std.StringArrayHashMap(Compilation.SystemLib).init(arena);
+            var libs = std.StringArrayHashMap(SystemLib).init(arena);
 
             // Assume ld64 default -search_paths_first if no strategy specified.
             const search_strategy = self.base.options.search_strategy orelse .paths_first;
@@ -890,7 +898,11 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
                 for (framework_dirs.items) |dir| {
                     for (&[_][]const u8{ ".tbd", ".dylib", "" }) |ext| {
                         if (try resolveFramework(arena, dir, f_name, ext)) |full_path| {
-                            try libs.put(full_path, self.base.options.frameworks.get(f_name).?);
+                            const info = self.base.options.frameworks.get(f_name).?;
+                            try libs.put(full_path, .{
+                                .needed = info.needed,
+                                .weak = info.weak,
+                            });
                             continue :outer;
                         }
                     }
@@ -1026,9 +1038,11 @@ 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| {
-                    const needed = self.base.options.system_libs.get(l_name).?.needed;
-                    const arg = if (needed)
+                    const info = self.base.options.system_libs.get(l_name).?;
+                    const arg = if (info.needed)
                         try std.fmt.allocPrint(arena, "-needed-l{s}", .{l_name})
+                    else if (info.weak)
+                        try std.fmt.allocPrint(arena, "-weak-l{s}", .{l_name})
                     else
                         try std.fmt.allocPrint(arena, "-l{s}", .{l_name});
                     try argv.append(arg);
@@ -1039,9 +1053,11 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
                 }
 
                 for (self.base.options.frameworks.keys()) |framework| {
-                    const needed = self.base.options.frameworks.get(framework).?.needed;
-                    const arg = if (needed)
+                    const info = self.base.options.frameworks.get(framework).?;
+                    const arg = if (info.needed)
                         try std.fmt.allocPrint(arena, "-needed_framework {s}", .{framework})
+                    else if (info.weak)
+                        try std.fmt.allocPrint(arena, "-weak_framework {s}", .{framework})
                     else
                         try std.fmt.allocPrint(arena, "-framework {s}", .{framework});
                     try argv.append(arg);
@@ -1063,7 +1079,10 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
                 Compilation.dump_argv(argv.items);
             }
 
-            var dependent_libs = std.fifo.LinearFifo(Dylib.Id, .Dynamic).init(self.base.allocator);
+            var dependent_libs = std.fifo.LinearFifo(struct {
+                id: Dylib.Id,
+                parent: u16,
+            }, .Dynamic).init(self.base.allocator);
             defer dependent_libs.deinit();
             try self.parseInputFiles(positionals.items, self.base.options.sysroot, &dependent_libs);
             try self.parseAndForceLoadStaticArchives(must_link_archives.keys());
@@ -1389,13 +1408,18 @@ const ParseDylibError = error{
 
 const DylibCreateOpts = struct {
     syslibroot: ?[]const u8,
-    dependent_libs: *std.fifo.LinearFifo(Dylib.Id, .Dynamic),
     id: ?Dylib.Id = null,
-    is_dependent: bool = false,
-    is_needed: bool = false,
+    dependent: bool = false,
+    needed: bool = false,
+    weak: bool = false,
 };
 
-pub fn parseDylib(self: *MachO, path: []const u8, opts: DylibCreateOpts) ParseDylibError!bool {
+pub fn parseDylib(
+    self: *MachO,
+    path: []const u8,
+    dependent_libs: anytype,
+    opts: DylibCreateOpts,
+) ParseDylibError!bool {
     const file = fs.cwd().openFile(path, .{}) catch |err| switch (err) {
         error.FileNotFound => return false,
         else => |e| return e,
@@ -1405,12 +1429,19 @@ pub fn parseDylib(self: *MachO, path: []const u8, opts: DylibCreateOpts) ParseDy
     const name = try self.base.allocator.dupe(u8, path);
     errdefer self.base.allocator.free(name);
 
+    const dylib_id = @intCast(u16, self.dylibs.items.len);
     var dylib = Dylib{
         .name = name,
         .file = file,
+        .weak = opts.weak,
     };
 
-    dylib.parse(self.base.allocator, self.base.options.target, opts.dependent_libs) catch |err| switch (err) {
+    dylib.parse(
+        self.base.allocator,
+        self.base.options.target,
+        dylib_id,
+        dependent_libs,
+    ) catch |err| switch (err) {
         error.EndOfStream, error.NotDylib => {
             try file.seekTo(0);
 
@@ -1420,7 +1451,13 @@ pub fn parseDylib(self: *MachO, path: []const u8, opts: DylibCreateOpts) ParseDy
             };
             defer lib_stub.deinit();
 
-            try dylib.parseFromStub(self.base.allocator, self.base.options.target, lib_stub, opts.dependent_libs);
+            try dylib.parseFromStub(
+                self.base.allocator,
+                self.base.options.target,
+                lib_stub,
+                dylib_id,
+                dependent_libs,
+            );
         },
         else => |e| return e,
     };
@@ -1438,13 +1475,12 @@ pub fn parseDylib(self: *MachO, path: []const u8, opts: DylibCreateOpts) ParseDy
         }
     }
 
-    const dylib_id = @intCast(u16, self.dylibs.items.len);
     try self.dylibs.append(self.base.allocator, dylib);
     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 and !opts.is_needed) break :blk false;
-        break :blk !(opts.is_dependent or self.referenced_dylibs.contains(dylib_id));
+        if (self.base.options.dead_strip_dylibs and !opts.needed) break :blk false;
+        break :blk !(opts.dependent or self.referenced_dylibs.contains(dylib_id));
     };
 
     if (should_link_dylib_even_if_unreachable) {
@@ -1467,9 +1503,8 @@ fn parseInputFiles(self: *MachO, files: []const []const u8, syslibroot: ?[]const
 
         if (try self.parseObject(full_path)) continue;
         if (try self.parseArchive(full_path, false)) continue;
-        if (try self.parseDylib(full_path, .{
+        if (try self.parseDylib(full_path, dependent_libs, .{
             .syslibroot = syslibroot,
-            .dependent_libs = dependent_libs,
         })) continue;
 
         log.warn("unknown filetype for positional input file: '{s}'", .{file_name});
@@ -1494,17 +1529,17 @@ fn parseAndForceLoadStaticArchives(self: *MachO, files: []const []const u8) !voi
 fn parseLibs(
     self: *MachO,
     lib_names: []const []const u8,
-    lib_infos: []const Compilation.SystemLib,
+    lib_infos: []const 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, .{
+        if (try self.parseDylib(lib, dependent_libs, .{
             .syslibroot = syslibroot,
-            .dependent_libs = dependent_libs,
-            .is_needed = lib_info.needed,
+            .needed = lib_info.needed,
+            .weak = lib_info.weak,
         })) continue;
         if (try self.parseArchive(lib, false)) continue;
 
@@ -1522,20 +1557,21 @@ fn parseDependentLibs(self: *MachO, syslibroot: ?[]const u8, dependent_libs: any
     const arena = arena_alloc.allocator();
     defer arena_alloc.deinit();
 
-    while (dependent_libs.readItem()) |*id| {
-        defer id.deinit(self.base.allocator);
+    while (dependent_libs.readItem()) |*dep_id| {
+        defer dep_id.id.deinit(self.base.allocator);
 
-        if (self.dylibs_map.contains(id.name)) continue;
+        if (self.dylibs_map.contains(dep_id.id.name)) continue;
 
+        const weak = self.dylibs.items[dep_id.parent].weak;
         const has_ext = blk: {
-            const basename = fs.path.basename(id.name);
+            const basename = fs.path.basename(dep_id.id.name);
             break :blk mem.lastIndexOfScalar(u8, basename, '.') != null;
         };
-        const extension = if (has_ext) fs.path.extension(id.name) else "";
+        const extension = if (has_ext) fs.path.extension(dep_id.id.name) else "";
         const without_ext = if (has_ext) blk: {
-            const index = mem.lastIndexOfScalar(u8, id.name, '.') orelse unreachable;
-            break :blk id.name[0..index];
-        } else id.name;
+            const index = mem.lastIndexOfScalar(u8, dep_id.id.name, '.') orelse unreachable;
+            break :blk dep_id.id.name[0..index];
+        } else dep_id.id.name;
 
         for (&[_][]const u8{ extension, ".tbd" }) |ext| {
             const with_ext = try std.fmt.allocPrint(arena, "{s}{s}", .{ without_ext, ext });
@@ -1543,15 +1579,15 @@ fn parseDependentLibs(self: *MachO, syslibroot: ?[]const u8, dependent_libs: any
 
             log.debug("trying dependency at fully resolved path {s}", .{full_path});
 
-            const did_parse_successfully = try self.parseDylib(full_path, .{
-                .id = id.*,
+            const did_parse_successfully = try self.parseDylib(full_path, dependent_libs, .{
+                .id = dep_id.id,
                 .syslibroot = syslibroot,
-                .is_dependent = true,
-                .dependent_libs = dependent_libs,
+                .dependent = true,
+                .weak = weak,
             });
             if (did_parse_successfully) break;
         } else {
-            log.warn("unable to resolve dependency {s}", .{id.name});
+            log.warn("unable to resolve dependency {s}", .{dep_id.id.name});
         }
     }
 }
@@ -3441,6 +3477,7 @@ fn addLoadDylibLC(self: *MachO, id: u16) !void {
     const dylib_id = dylib.id orelse unreachable;
     var dylib_cmd = try macho.createLoadDylibCommand(
         self.base.allocator,
+        if (dylib.weak) .LOAD_WEAK_DYLIB else .LOAD_DYLIB,
         dylib_id.name,
         dylib_id.timestamp,
         dylib_id.current_version,
@@ -4885,13 +4922,13 @@ fn populateMissingMetadata(self: *MachO) !void {
             std.builtin.Version{ .major = 1, .minor = 0, .patch = 0 };
         var dylib_cmd = try macho.createLoadDylibCommand(
             self.base.allocator,
+            .ID_DYLIB,
             install_name,
             2,
             current_version.major << 16 | current_version.minor << 8 | current_version.patch,
             compat_version.major << 16 | compat_version.minor << 8 | compat_version.patch,
         );
         errdefer dylib_cmd.deinit(self.base.allocator);
-        dylib_cmd.inner.cmd = .ID_DYLIB;
         try self.load_commands.append(self.base.allocator, .{ .dylib = dylib_cmd });
         self.load_commands_dirty = true;
     }
src/link.zig
@@ -21,6 +21,7 @@ const TypedValue = @import("TypedValue.zig");
 
 pub const SystemLib = struct {
     needed: bool = false,
+    weak: bool = false,
 };
 
 pub const CacheMode = enum { incremental, whole };
src/main.zig
@@ -443,9 +443,12 @@ const usage_build_generic =
     \\  --subsystem [subsystem]        (Windows) /SUBSYSTEM:<subsystem> to the linker
     \\  --stack [size]                 Override default stack size
     \\  --image-base [addr]            Set base address for executable image
+    \\  -weak-l[lib]                   (Darwin) link against system library and mark it and all referenced symbols as weak
+    \\    -weak_library [lib]
     \\  -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)
+    \\  -weak_framework [name]         (Darwin) link against framework and mark it and all referenced symbols as weak
     \\  -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
@@ -916,7 +919,12 @@ fn buildOutputType(
                         const path = args_iter.next() orelse {
                             fatal("expected parameter after {s}", .{arg});
                         };
-                        try frameworks.put(gpa, path, .{ .needed = false });
+                        try frameworks.put(gpa, path, .{});
+                    } else if (mem.eql(u8, arg, "-weak_framework")) {
+                        const path = args_iter.next() orelse {
+                            fatal("expected parameter after {s}", .{arg});
+                        };
+                        try frameworks.put(gpa, path, .{ .weak = true });
                     } else if (mem.eql(u8, arg, "-needed_framework")) {
                         const path = args_iter.next() orelse {
                             fatal("expected parameter after {s}", .{arg});
@@ -962,7 +970,7 @@ 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 });
+                        try system_libs.put(next_arg, .{});
                     } else if (mem.eql(u8, arg, "--needed-library") or
                         mem.eql(u8, arg, "-needed-l") or
                         mem.eql(u8, arg, "-needed_library"))
@@ -971,6 +979,11 @@ fn buildOutputType(
                             fatal("expected parameter after {s}", .{arg});
                         };
                         try system_libs.put(next_arg, .{ .needed = true });
+                    } else if (mem.eql(u8, arg, "-weak_library") or mem.eql(u8, arg, "-weak-l")) {
+                        const next_arg = args_iter.next() orelse {
+                            fatal("expected parameter after {s}", .{arg});
+                        };
+                        try system_libs.put(next_arg, .{ .weak = true });
                     } else if (mem.eql(u8, arg, "-D") or
                         mem.eql(u8, arg, "-isystem") or
                         mem.eql(u8, arg, "-I") or
@@ -1300,9 +1313,11 @@ fn buildOutputType(
                     } else if (mem.startsWith(u8, arg, "-l")) {
                         // 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(arg["-l".len..], .{ .needed = false });
+                        try system_libs.put(arg["-l".len..], .{});
                     } else if (mem.startsWith(u8, arg, "-needed-l")) {
                         try system_libs.put(arg["-needed-l".len..], .{ .needed = true });
+                    } else if (mem.startsWith(u8, arg, "-weak-l")) {
+                        try system_libs.put(arg["-weak-l".len..], .{ .weak = true });
                     } else if (mem.startsWith(u8, arg, "-D") or
                         mem.startsWith(u8, arg, "-I"))
                     {
@@ -1596,7 +1611,7 @@ fn buildOutputType(
                         try clang_argv.appendSlice(it.other_args);
                     },
                     .framework_dir => try framework_dirs.append(it.only_arg),
-                    .framework => try frameworks.put(gpa, it.only_arg, .{ .needed = false }),
+                    .framework => try frameworks.put(gpa, it.only_arg, .{}),
                     .nostdlibinc => want_native_include_dirs = false,
                     .strip => strip = true,
                     .exec_model => {
@@ -1879,12 +1894,18 @@ fn buildOutputType(
                     ) catch |err| {
                         fatal("unable to parse '{s}': {s}", .{ arg, @errorName(err) });
                     };
-                } else if (mem.eql(u8, arg, "-framework") or mem.eql(u8, arg, "-weak_framework")) {
+                } else if (mem.eql(u8, arg, "-framework")) {
+                    i += 1;
+                    if (i >= linker_args.items.len) {
+                        fatal("expected linker arg after '{s}'", .{arg});
+                    }
+                    try frameworks.put(gpa, linker_args.items[i], .{});
+                } else if (mem.eql(u8, arg, "-weak_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 = false });
+                    try frameworks.put(gpa, linker_args.items[i], .{ .weak = true });
                 } else if (mem.eql(u8, arg, "-needed_framework")) {
                     i += 1;
                     if (i >= linker_args.items.len) {
@@ -1897,6 +1918,14 @@ fn buildOutputType(
                         fatal("expected linker arg after '{s}'", .{arg});
                     }
                     try system_libs.put(linker_args.items[i], .{ .needed = true });
+                } else if (mem.startsWith(u8, arg, "-weak-l")) {
+                    try system_libs.put(arg["-weak-l".len..], .{ .weak = true });
+                } else if (mem.eql(u8, arg, "-weak_library")) {
+                    i += 1;
+                    if (i >= linker_args.items.len) {
+                        fatal("expected linker arg after '{s}'", .{arg});
+                    }
+                    try system_libs.put(linker_args.items[i], .{ .weak = true });
                 } else if (mem.eql(u8, arg, "-compatibility_version")) {
                     i += 1;
                     if (i >= linker_args.items.len) {