Commit 79b3285aa2

Jakub Konka <kubkon@jakubkonka.com>
2023-08-29 15:27:44
macho: handle mismatched and missing platform errors
1 parent 1cae41b
src/link/MachO/Dylib.zig
@@ -178,6 +178,26 @@ pub fn parseFromBinary(
     }
 }
 
+/// Returns Platform composed from the first encountered build version type load command:
+/// either LC_BUILD_VERSION or LC_VERSION_MIN_*.
+pub fn getPlatform(self: Dylib, data: []align(@alignOf(u64)) const u8) ?Platform {
+    var it = LoadCommandIterator{
+        .ncmds = self.header.?.ncmds,
+        .buffer = data[@sizeOf(macho.mach_header_64)..][0..self.header.?.sizeofcmds],
+    };
+    while (it.next()) |cmd| {
+        switch (cmd.cmd()) {
+            .BUILD_VERSION,
+            .VERSION_MIN_MACOSX,
+            .VERSION_MIN_IPHONEOS,
+            .VERSION_MIN_TVOS,
+            .VERSION_MIN_WATCHOS,
+            => return Platform.fromLoadCommand(cmd),
+            else => {},
+        }
+    } else return null;
+}
+
 fn addObjCClassSymbol(self: *Dylib, allocator: Allocator, sym_name: []const u8) !void {
     const expanded = &[_][]const u8{
         try std.fmt.allocPrint(allocator, "_OBJC_CLASS_$_{s}", .{sym_name}),
@@ -212,27 +232,27 @@ fn addWeakSymbol(self: *Dylib, allocator: Allocator, sym_name: []const u8) !void
     try self.symbols.putNoClobber(allocator, try allocator.dupe(u8, sym_name), true);
 }
 
-const TargetMatcher = struct {
+pub const TargetMatcher = struct {
     allocator: Allocator,
-    target: CrossTarget,
+    cpu_arch: std.Target.Cpu.Arch,
+    os_tag: std.Target.Os.Tag,
+    abi: std.Target.Abi,
     target_strings: std.ArrayListUnmanaged([]const u8) = .{},
 
-    pub fn init(allocator: Allocator, target: CrossTarget) !TargetMatcher {
+    pub fn init(allocator: Allocator, target: std.Target) !TargetMatcher {
         var self = TargetMatcher{
             .allocator = allocator,
-            .target = target,
+            .cpu_arch = target.cpu.arch,
+            .os_tag = target.os.tag,
+            .abi = target.abi,
         };
-        const apple_string = try targetToAppleString(allocator, target);
+        const apple_string = try toAppleTargetTriple(allocator, self.cpu_arch, self.os_tag, self.abi);
         try self.target_strings.append(allocator, apple_string);
 
-        const abi = target.abi orelse .none;
-        if (abi == .simulator) {
+        if (self.abi == .simulator) {
             // For Apple simulator targets, linking gets tricky as we need to link against the simulator
             // hosts dylibs too.
-            const host_target = try targetToAppleString(allocator, .{
-                .cpu_arch = target.cpu_arch.?,
-                .os_tag = .macos,
-            });
+            const host_target = try toAppleTargetTriple(allocator, self.cpu_arch, .macos, .none);
             try self.target_strings.append(allocator, host_target);
         }
 
@@ -246,7 +266,7 @@ const TargetMatcher = struct {
         self.target_strings.deinit(self.allocator);
     }
 
-    inline fn cpuArchToAppleString(cpu_arch: std.Target.Cpu.Arch) []const u8 {
+    inline fn fmtCpuArch(cpu_arch: std.Target.Cpu.Arch) []const u8 {
         return switch (cpu_arch) {
             .aarch64 => "arm64",
             .x86_64 => "x86_64",
@@ -254,7 +274,7 @@ const TargetMatcher = struct {
         };
     }
 
-    inline fn abiToAppleString(abi: std.Target.Abi) ?[]const u8 {
+    inline fn fmtAbi(abi: std.Target.Abi) ?[]const u8 {
         return switch (abi) {
             .none => null,
             .simulator => "simulator",
@@ -263,14 +283,18 @@ const TargetMatcher = struct {
         };
     }
 
-    pub fn targetToAppleString(allocator: Allocator, target: CrossTarget) ![]const u8 {
-        const cpu_arch = cpuArchToAppleString(target.cpu_arch.?);
-        const os_tag = @tagName(target.os_tag.?);
-        const target_abi = abiToAppleString(target.abi orelse .none);
-        if (target_abi) |abi| {
-            return std.fmt.allocPrint(allocator, "{s}-{s}-{s}", .{ cpu_arch, os_tag, abi });
+    pub fn toAppleTargetTriple(
+        allocator: Allocator,
+        cpu_arch: std.Target.Cpu.Arch,
+        os_tag: std.Target.Os.Tag,
+        abi: std.Target.Abi,
+    ) ![]const u8 {
+        const cpu_arch_s = fmtCpuArch(cpu_arch);
+        const os_tag_s = @tagName(os_tag);
+        if (fmtAbi(abi)) |abi_s| {
+            return std.fmt.allocPrint(allocator, "{s}-{s}-{s}", .{ cpu_arch_s, os_tag_s, abi_s });
         }
-        return std.fmt.allocPrint(allocator, "{s}-{s}", .{ cpu_arch, os_tag });
+        return std.fmt.allocPrint(allocator, "{s}-{s}", .{ cpu_arch_s, os_tag_s });
     }
 
     fn hasValue(stack: []const []const u8, needle: []const u8) bool {
@@ -280,7 +304,7 @@ const TargetMatcher = struct {
         return false;
     }
 
-    fn matchesTarget(self: TargetMatcher, targets: []const []const u8) bool {
+    pub fn matchesTarget(self: TargetMatcher, targets: []const []const u8) bool {
         for (self.target_strings.items) |t| {
             if (hasValue(targets, t)) return true;
         }
@@ -288,26 +312,7 @@ const TargetMatcher = struct {
     }
 
     fn matchesArch(self: TargetMatcher, archs: []const []const u8) bool {
-        return hasValue(archs, cpuArchToAppleString(self.target.cpu_arch.?));
-    }
-
-    pub fn matchesTargetTbd(self: TargetMatcher, tbd: Tbd) !bool {
-        var arena = std.heap.ArenaAllocator.init(self.allocator);
-        defer arena.deinit();
-
-        const targets = switch (tbd) {
-            .v3 => |v3| blk: {
-                var targets = std.ArrayList([]const u8).init(arena.allocator());
-                for (v3.archs) |arch| {
-                    const target = try std.fmt.allocPrint(arena.allocator(), "{s}-{s}", .{ arch, v3.platform });
-                    try targets.append(target);
-                }
-                break :blk targets.items;
-            },
-            .v4 => |v4| v4.targets,
-        };
-
-        return self.matchesTarget(targets);
+        return hasValue(archs, fmtCpuArch(self.cpu_arch));
     }
 };
 
@@ -342,15 +347,16 @@ pub fn parseFromStub(
 
     log.debug("  (install_name '{s}')", .{umbrella_lib.installName()});
 
-    var matcher = try TargetMatcher.init(allocator, .{
-        .cpu_arch = target.cpu.arch,
-        .os_tag = target.os.tag,
-        .abi = target.abi,
-    });
+    var matcher = try TargetMatcher.init(allocator, target);
     defer matcher.deinit();
 
     for (lib_stub.inner, 0..) |elem, stub_index| {
-        if (!(try matcher.matchesTargetTbd(elem))) continue;
+        const targets = try elem.targets(allocator);
+        defer {
+            for (targets) |t| allocator.free(t);
+            allocator.free(targets);
+        }
+        if (!matcher.matchesTarget(targets)) continue;
 
         if (stub_index > 0) {
             // TODO I thought that we could switch on presence of `parent-umbrella` map;
@@ -541,8 +547,8 @@ const fat = @import("fat.zig");
 const tapi = @import("../tapi.zig");
 
 const Allocator = mem.Allocator;
-const CrossTarget = std.zig.CrossTarget;
 const LibStub = tapi.LibStub;
 const LoadCommandIterator = macho.LoadCommandIterator;
 const MachO = @import("../MachO.zig");
+const Platform = @import("load_commands.zig").Platform;
 const Tbd = tapi.Tbd;
src/link/MachO/load_commands.zig
@@ -77,7 +77,7 @@ fn calcLCsSize(gpa: Allocator, options: *const link.Options, ctx: CalcLCsSizeCtx
     // LC_SOURCE_VERSION
     sizeofcmds += @sizeOf(macho.source_version_command);
     // LC_BUILD_VERSION or LC_VERSION_MIN_
-    if (Platform.fromOptions(options).isBuildVersionCompatible()) {
+    if (Platform.fromTarget(options.target).isBuildVersionCompatible()) {
         // LC_BUILD_VERSION
         sizeofcmds += @sizeOf(macho.build_version_command) + @sizeOf(macho.build_tool_version);
     } else {
@@ -353,11 +353,11 @@ pub const Platform = struct {
         }
     }
 
-    pub fn fromOptions(options: *const link.Options) Platform {
+    pub fn fromTarget(target: std.Target) Platform {
         return .{
-            .os_tag = options.target.os.tag,
-            .abi = options.target.abi,
-            .version = options.target.os.version_range.semver.min,
+            .os_tag = target.os.tag,
+            .abi = target.abi,
+            .version = target.os.version_range.semver.min,
         };
     }
 
@@ -383,6 +383,28 @@ pub const Platform = struct {
         }
         return false;
     }
+
+    pub fn fmtTarget(plat: Platform) std.fmt.Formatter(formatTarget) {
+        return .{ .data = plat };
+    }
+
+    pub fn formatTarget(
+        plat: Platform,
+        comptime unused_fmt_string: []const u8,
+        options: std.fmt.FormatOptions,
+        writer: anytype,
+    ) !void {
+        _ = unused_fmt_string;
+        _ = options;
+        try writer.print("{s}", .{@tagName(plat.os_tag)});
+        if (plat.abi != .none) {
+            try writer.print("-{s}", .{@tagName(plat.abi)});
+        }
+    }
+
+    pub fn eqlTarget(plat: Platform, other: Platform) bool {
+        return plat.os_tag == other.os_tag and plat.abi == other.abi;
+    }
 };
 
 const SupportedPlatforms = struct {
src/link/MachO/Object.zig
@@ -940,7 +940,7 @@ pub fn parseDwarfInfo(self: Object) DwarfInfo {
     return di;
 }
 
-/// Returns Options.Platform composed from the first encountered build version type load command:
+/// Returns Platform composed from the first encountered build version type load command:
 /// either LC_BUILD_VERSION or LC_VERSION_MIN_*.
 pub fn getPlatform(self: Object) ?Platform {
     var it = LoadCommandIterator{
src/link/MachO/zld.zig
@@ -347,11 +347,17 @@ pub fn linkWithZld(
 
         var parse_error_ctx: struct {
             detected_arch: std.Target.Cpu.Arch,
-            detected_os: std.Target.Os.Tag,
+            detected_platform: ?Platform,
+            detected_stub_targets: []const []const u8,
         } = .{
             .detected_arch = undefined,
-            .detected_os = undefined,
+            .detected_platform = null,
+            .detected_stub_targets = &[0][]const u8{},
         };
+        defer {
+            for (parse_error_ctx.detected_stub_targets) |t| gpa.free(t);
+            gpa.free(parse_error_ctx.detected_stub_targets);
+        }
 
         for (positionals.items) |obj| {
             const in_file = try std.fs.cwd().openFile(obj.path, .{});
@@ -363,25 +369,7 @@ pub fn linkWithZld(
                 obj.must_link,
                 &dependent_libs,
                 &parse_error_ctx,
-            ) catch |err| switch (err) {
-                error.DylibAlreadyExists => {},
-                error.UnknownFileType => try macho_file.reportParseError(obj.path, "unknown file type", .{}),
-                error.MissingArchFatLib => try macho_file.reportParseError(
-                    obj.path,
-                    "missing architecture in universal file, expected '{s}'",
-                    .{@tagName(cpu_arch)},
-                ),
-                error.InvalidArch => try macho_file.reportParseError(
-                    obj.path,
-                    "invalid architecture '{s}', expected '{s}'",
-                    .{ @tagName(parse_error_ctx.detected_arch), @tagName(cpu_arch) },
-                ),
-                else => |e| try macho_file.reportParseError(
-                    obj.path,
-                    "parsing positional argument failed with error '{s}'",
-                    .{@errorName(e)},
-                ),
-            };
+            ) catch |err| try macho_file.handleAndReportParseError(obj.path, err, parse_error_ctx);
         }
 
         for (libs.keys(), libs.values()) |path, lib| {
@@ -395,25 +383,7 @@ pub fn linkWithZld(
                 false,
                 &dependent_libs,
                 &parse_error_ctx,
-            ) catch |err| switch (err) {
-                error.DylibAlreadyExists => {},
-                error.UnknownFileType => try macho_file.reportParseError(path, "unknown file type", .{}),
-                error.MissingArchFatLib => try macho_file.reportParseError(
-                    path,
-                    "missing architecture in universal file, expected '{s}'",
-                    .{@tagName(cpu_arch)},
-                ),
-                error.InvalidArch => try macho_file.reportParseError(
-                    path,
-                    "invalid architecture '{s}', expected '{s}'",
-                    .{ @tagName(parse_error_ctx.detected_arch), @tagName(cpu_arch) },
-                ),
-                else => |e| try macho_file.reportParseError(
-                    path,
-                    "parsing library failed with error '{s}'",
-                    .{@errorName(e)},
-                ),
-            };
+            ) catch |err| try macho_file.handleAndReportParseError(path, err, parse_error_ctx);
         }
 
         macho_file.parseDependentLibs(&dependent_libs, &parse_error_ctx) catch |err| {
@@ -590,7 +560,7 @@ pub fn linkWithZld(
             .version = 0,
         });
         {
-            const platform = load_commands.Platform.fromOptions(&macho_file.base.options);
+            const platform = Platform.fromTarget(macho_file.base.options.target);
             const sdk_version: ?std.SemanticVersion = if (macho_file.base.options.sysroot) |path|
                 load_commands.inferSdkVersionFromSdkPath(path)
             else
@@ -1252,6 +1222,7 @@ const MachO = @import("../MachO.zig");
 const Md5 = std.crypto.hash.Md5;
 const LibStub = @import("../tapi.zig").LibStub;
 const Object = @import("Object.zig");
+const Platform = load_commands.Platform;
 const Section = MachO.Section;
 const StringTable = @import("../strtab.zig").StringTable;
 const SymbolWithLoc = MachO.SymbolWithLoc;
src/link/MachO.zig
@@ -396,16 +396,24 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
         self.dylibs_map.clearRetainingCapacity();
         self.referenced_dylibs.clearRetainingCapacity();
 
-        const cpu_arch = self.base.options.target.cpu.arch;
         var dependent_libs = std.fifo.LinearFifo(struct {
             id: Dylib.Id,
             parent: u16,
         }, .Dynamic).init(arena);
 
-        var parse_error_ctx: union {
-            none: void,
+        var parse_error_ctx: struct {
             detected_arch: std.Target.Cpu.Arch,
-        } = .{ .none = {} };
+            detected_platform: ?Platform,
+            detected_stub_targets: []const []const u8,
+        } = .{
+            .detected_arch = undefined,
+            .detected_platform = null,
+            .detected_stub_targets = &[0][]const u8{},
+        };
+        defer {
+            for (parse_error_ctx.detected_stub_targets) |target| self.base.allocator.free(target);
+            self.base.allocator.free(parse_error_ctx.detected_stub_targets);
+        }
 
         for (libs.keys(), libs.values()) |path, lib| {
             const in_file = try std.fs.cwd().openFile(path, .{});
@@ -418,25 +426,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
                 false,
                 &dependent_libs,
                 &parse_error_ctx,
-            ) catch |err| switch (err) {
-                error.DylibAlreadyExists => {},
-                error.UnknownFileType => try self.reportParseError(path, "unknown file type", .{}),
-                error.MissingArchFatLib => try self.reportParseError(
-                    path,
-                    "missing architecture in universal file, expected '{s}'",
-                    .{@tagName(cpu_arch)},
-                ),
-                error.InvalidArch => try self.reportParseError(
-                    path,
-                    "invalid architecture '{s}', expected '{s}'",
-                    .{ @tagName(parse_error_ctx.detected_arch), @tagName(cpu_arch) },
-                ),
-                else => |e| try self.reportParseError(
-                    path,
-                    "parsing library failed with error '{s}'",
-                    .{@errorName(e)},
-                ),
-            };
+            ) catch |err| try self.handleAndReportParseError(path, err, parse_error_ctx);
         }
 
         self.parseDependentLibs(&dependent_libs, &parse_error_ctx) catch |err| {
@@ -586,7 +576,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
         .version = 0,
     });
     {
-        const platform = load_commands.Platform.fromOptions(&self.base.options);
+        const platform = Platform.fromTarget(self.base.options.target);
         const sdk_version: ?std.SemanticVersion = if (self.base.options.sysroot) |path|
             load_commands.inferSdkVersionFromSdkPath(path)
         else
@@ -738,7 +728,8 @@ fn resolveLib(
 const ParseError = error{
     UnknownFileType,
     MissingArchFatLib,
-    InvalidArch,
+    InvalidTarget,
+    InvalidLibStubTargets,
     DylibAlreadyExists,
     IncompatibleDylibVersion,
     OutOfMemory,
@@ -798,19 +789,24 @@ fn parseObject(
     };
     errdefer object.deinit(gpa);
     try object.parse(gpa);
-    try self.objects.append(gpa, object);
 
     const cpu_arch: std.Target.Cpu.Arch = switch (object.header.cputype) {
         macho.CPU_TYPE_ARM64 => .aarch64,
         macho.CPU_TYPE_X86_64 => .x86_64,
         else => unreachable,
     };
-    const self_cpu_arch = self.base.options.target.cpu.arch;
+    error_ctx.detected_arch = cpu_arch;
 
-    if (self_cpu_arch != cpu_arch) {
-        error_ctx.detected_arch = cpu_arch;
-        return error.InvalidArch;
+    if (object.getPlatform()) |platform| {
+        error_ctx.detected_platform = platform;
+    }
+
+    if (self.base.options.target.cpu.arch != cpu_arch) return error.InvalidTarget;
+    if (error_ctx.detected_platform) |platform| {
+        if (!Platform.fromTarget(self.base.options.target).eqlTarget(platform)) return error.InvalidTarget;
     }
+
+    try self.objects.append(gpa, object);
 }
 
 pub fn parseLibrary(
@@ -825,14 +821,12 @@ pub fn parseLibrary(
     const tracy = trace(@src());
     defer tracy.end();
 
-    const cpu_arch = self.base.options.target.cpu.arch;
-
     if (fat.isFatLibrary(file)) {
-        const offset = try self.parseFatLibrary(file, cpu_arch);
+        const offset = try self.parseFatLibrary(file, self.base.options.target.cpu.arch);
         try file.seekTo(offset);
 
         if (Archive.isArchive(file, offset)) {
-            try self.parseArchive(path, offset, must_link, cpu_arch, error_ctx);
+            try self.parseArchive(path, offset, must_link, error_ctx);
         } else if (Dylib.isDylib(file, offset)) {
             try self.parseDylib(file, path, offset, dependent_libs, .{
                 .needed = lib.needed,
@@ -840,7 +834,7 @@ pub fn parseLibrary(
             }, error_ctx);
         } else return error.UnknownFileType;
     } else if (Archive.isArchive(file, 0)) {
-        try self.parseArchive(path, 0, must_link, cpu_arch, error_ctx);
+        try self.parseArchive(path, 0, must_link, error_ctx);
     } else if (Dylib.isDylib(file, 0)) {
         try self.parseDylib(file, path, 0, dependent_libs, .{
             .needed = lib.needed,
@@ -850,7 +844,7 @@ pub fn parseLibrary(
         self.parseLibStub(file, path, dependent_libs, .{
             .needed = lib.needed,
             .weak = lib.weak,
-        }) catch |err| switch (err) {
+        }, error_ctx) catch |err| switch (err) {
             error.NotLibStub, error.UnexpectedToken => return error.UnknownFileType,
             else => |e| return e,
         };
@@ -872,7 +866,6 @@ fn parseArchive(
     path: []const u8,
     fat_offset: u64,
     must_link: bool,
-    cpu_arch: std.Target.Cpu.Arch,
     error_ctx: anytype,
 ) ParseError!void {
     const gpa = self.base.allocator;
@@ -899,14 +892,20 @@ fn parseArchive(
         var object = try archive.parseObject(gpa, off); // TODO we are doing all this work to pull the header only!
         defer object.deinit(gpa);
 
-        const parsed_cpu_arch: std.Target.Cpu.Arch = switch (object.header.cputype) {
+        const cpu_arch: std.Target.Cpu.Arch = switch (object.header.cputype) {
             macho.CPU_TYPE_ARM64 => .aarch64,
             macho.CPU_TYPE_X86_64 => .x86_64,
             else => unreachable,
         };
-        if (cpu_arch != parsed_cpu_arch) {
-            error_ctx.detected_arch = parsed_cpu_arch;
-            return error.InvalidArch;
+        error_ctx.detected_arch = cpu_arch;
+
+        if (object.getPlatform()) |platform| {
+            error_ctx.detected_platform = platform;
+        }
+
+        if (self.base.options.target.cpu.arch != cpu_arch) return error.InvalidTarget;
+        if (error_ctx.detected_platform) |platform| {
+            if (!Platform.fromTarget(self.base.options.target).eqlTarget(platform)) return error.InvalidTarget;
         }
     }
 
@@ -945,8 +944,6 @@ fn parseDylib(
     error_ctx: anytype,
 ) ParseError!void {
     const gpa = self.base.allocator;
-    const self_cpu_arch = self.base.options.target.cpu.arch;
-
     const file_stat = try file.stat();
     const file_size = math.cast(usize, file_stat.size - offset) orelse return error.Overflow;
 
@@ -969,12 +966,16 @@ fn parseDylib(
         macho.CPU_TYPE_X86_64 => .x86_64,
         else => unreachable,
     };
-    if (self_cpu_arch != cpu_arch) {
-        error_ctx.detected_arch = cpu_arch;
-        return error.InvalidArch;
+    error_ctx.detected_arch = cpu_arch;
+
+    if (dylib.getPlatform(contents)) |platform| {
+        error_ctx.detected_platform = platform;
     }
 
-    // TODO verify platform
+    if (self.base.options.target.cpu.arch != cpu_arch) return error.InvalidTarget;
+    if (error_ctx.detected_platform) |platform| {
+        if (!Platform.fromTarget(self.base.options.target).eqlTarget(platform)) return error.InvalidTarget;
+    }
 
     try self.addDylib(dylib, .{
         .needed = dylib_options.needed,
@@ -988,6 +989,7 @@ fn parseLibStub(
     path: []const u8,
     dependent_libs: anytype,
     dylib_options: DylibOpts,
+    error_ctx: anytype,
 ) ParseError!void {
     const gpa = self.base.allocator;
     var lib_stub = try LibStub.loadFromFile(gpa, file);
@@ -995,7 +997,20 @@ fn parseLibStub(
 
     if (lib_stub.inner.len == 0) return error.NotLibStub;
 
-    // TODO verify platform
+    // Verify target
+    {
+        var matcher = try Dylib.TargetMatcher.init(gpa, self.base.options.target);
+        defer matcher.deinit();
+
+        const first_tbd = lib_stub.inner[0];
+        const targets = try first_tbd.targets(gpa);
+        if (!matcher.matchesTarget(targets)) {
+            error_ctx.detected_stub_targets = targets;
+            return error.InvalidLibStubTargets;
+        }
+        for (targets) |t| gpa.free(t);
+        gpa.free(targets);
+    }
 
     var dylib = Dylib{ .weak = dylib_options.weak };
     errdefer dylib.deinit(gpa);
@@ -1104,7 +1119,7 @@ pub fn parseDependentLibs(self: *MachO, dependent_libs: anytype, error_ctx: anyt
                 self.parseLibStub(file, full_path, dependent_libs, .{
                     .dependent = true,
                     .weak = weak,
-                }) catch |err| switch (err) {
+                }, error_ctx) catch |err| switch (err) {
                     error.NotLibStub, error.UnexpectedToken => continue,
                     else => |e| return e,
                 };
@@ -4830,6 +4845,53 @@ pub fn getSectionPrecedence(header: macho.section_64) u8 {
     return (@as(u8, @intCast(segment_precedence)) << 4) + section_precedence;
 }
 
+pub fn handleAndReportParseError(self: *MachO, path: []const u8, err: ParseError, parse_error_ctx: anytype) !void {
+    const cpu_arch = self.base.options.target.cpu.arch;
+    switch (err) {
+        error.DylibAlreadyExists => {},
+        error.UnknownFileType => try self.reportParseError(path, "unknown file type", .{}),
+        error.MissingArchFatLib => try self.reportParseError(
+            path,
+            "missing architecture in universal file, expected '{s}'",
+            .{@tagName(cpu_arch)},
+        ),
+        error.InvalidTarget => if (parse_error_ctx.detected_platform) |platform| {
+            try self.reportParseError(path, "invalid target '{s}-{}', expected '{s}-{}'", .{
+                @tagName(parse_error_ctx.detected_arch),
+                platform.fmtTarget(),
+                @tagName(cpu_arch),
+                Platform.fromTarget(self.base.options.target).fmtTarget(),
+            });
+        } else {
+            try self.reportParseError(
+                path,
+                "invalid architecture '{s}', expected '{s}'",
+                .{ @tagName(parse_error_ctx.detected_arch), @tagName(cpu_arch) },
+            );
+        },
+        error.InvalidLibStubTargets => {
+            var targets_string = std.ArrayList(u8).init(self.base.allocator);
+            defer targets_string.deinit();
+            try targets_string.writer().writeAll("(");
+            for (parse_error_ctx.detected_stub_targets) |t| {
+                try targets_string.writer().print("{s}, ", .{t});
+            }
+            try targets_string.resize(targets_string.items.len - 2);
+            try targets_string.writer().writeAll(")");
+            try self.reportParseError(path, "invalid targets '{s}', expected '{s}-{}'", .{
+                targets_string.items,
+                @tagName(cpu_arch),
+                Platform.fromTarget(self.base.options.target).fmtTarget(),
+            });
+        },
+        else => |e| try self.reportParseError(
+            path,
+            "parsing positional argument failed with error '{s}'",
+            .{@errorName(e)},
+        ),
+    }
+}
+
 pub fn reportParseError(self: *MachO, path: []const u8, comptime format: []const u8, args: anytype) !void {
     const gpa = self.base.allocator;
     try self.misc_errors.ensureUnusedCapacity(gpa, 1);
@@ -5140,66 +5202,6 @@ pub fn logAtom(self: *MachO, atom_index: Atom.Index, logger: anytype) void {
     }
 }
 
-const MachO = @This();
-
-const std = @import("std");
-const build_options = @import("build_options");
-const builtin = @import("builtin");
-const assert = std.debug.assert;
-const dwarf = std.dwarf;
-const fs = std.fs;
-const log = std.log.scoped(.link);
-const macho = std.macho;
-const math = std.math;
-const mem = std.mem;
-const meta = std.meta;
-
-const aarch64 = @import("../arch/aarch64/bits.zig");
-const calcUuid = @import("MachO/uuid.zig").calcUuid;
-const codegen = @import("../codegen.zig");
-const dead_strip = @import("MachO/dead_strip.zig");
-const fat = @import("MachO/fat.zig");
-const link = @import("../link.zig");
-const llvm_backend = @import("../codegen/llvm.zig");
-const load_commands = @import("MachO/load_commands.zig");
-const stubs = @import("MachO/stubs.zig");
-const tapi = @import("tapi.zig");
-const target_util = @import("../target.zig");
-const thunks = @import("MachO/thunks.zig");
-const trace = @import("../tracy.zig").trace;
-const zld = @import("MachO/zld.zig");
-
-const Air = @import("../Air.zig");
-const Allocator = mem.Allocator;
-const Archive = @import("MachO/Archive.zig");
-pub const Atom = @import("MachO/Atom.zig");
-const Cache = std.Build.Cache;
-const CodeSignature = @import("MachO/CodeSignature.zig");
-const Compilation = @import("../Compilation.zig");
-const Dwarf = File.Dwarf;
-const DwarfInfo = @import("MachO/DwarfInfo.zig");
-const Dylib = @import("MachO/Dylib.zig");
-const File = link.File;
-const Object = @import("MachO/Object.zig");
-const LibStub = tapi.LibStub;
-const Liveness = @import("../Liveness.zig");
-const LlvmObject = @import("../codegen/llvm.zig").Object;
-const Md5 = std.crypto.hash.Md5;
-const Module = @import("../Module.zig");
-const InternPool = @import("../InternPool.zig");
-const Relocation = @import("MachO/Relocation.zig");
-const StringTable = @import("strtab.zig").StringTable;
-const TableSection = @import("table_section.zig").TableSection;
-const Trie = @import("MachO/Trie.zig");
-const Type = @import("../type.zig").Type;
-const TypedValue = @import("../TypedValue.zig");
-const Value = @import("../value.zig").Value;
-
-pub const DebugSymbols = @import("MachO/DebugSymbols.zig");
-pub const Bind = @import("MachO/dyld_info/bind.zig").Bind(*const MachO, SymbolWithLoc);
-pub const LazyBind = @import("MachO/dyld_info/bind.zig").LazyBind(*const MachO, SymbolWithLoc);
-pub const Rebase = @import("MachO/dyld_info/Rebase.zig");
-
 pub const base_tag: File.Tag = File.Tag.macho;
 pub const N_DEAD: u16 = @as(u16, @bitCast(@as(i16, -1)));
 
@@ -5332,3 +5334,64 @@ pub const default_pagezero_vmsize: u64 = 0x100000000;
 /// the table of load commands. This should be plenty for any
 /// potential future extensions.
 pub const default_headerpad_size: u32 = 0x1000;
+
+const MachO = @This();
+
+const std = @import("std");
+const build_options = @import("build_options");
+const builtin = @import("builtin");
+const assert = std.debug.assert;
+const dwarf = std.dwarf;
+const fs = std.fs;
+const log = std.log.scoped(.link);
+const macho = std.macho;
+const math = std.math;
+const mem = std.mem;
+const meta = std.meta;
+
+const aarch64 = @import("../arch/aarch64/bits.zig");
+const calcUuid = @import("MachO/uuid.zig").calcUuid;
+const codegen = @import("../codegen.zig");
+const dead_strip = @import("MachO/dead_strip.zig");
+const fat = @import("MachO/fat.zig");
+const link = @import("../link.zig");
+const llvm_backend = @import("../codegen/llvm.zig");
+const load_commands = @import("MachO/load_commands.zig");
+const stubs = @import("MachO/stubs.zig");
+const tapi = @import("tapi.zig");
+const target_util = @import("../target.zig");
+const thunks = @import("MachO/thunks.zig");
+const trace = @import("../tracy.zig").trace;
+const zld = @import("MachO/zld.zig");
+
+const Air = @import("../Air.zig");
+const Allocator = mem.Allocator;
+const Archive = @import("MachO/Archive.zig");
+pub const Atom = @import("MachO/Atom.zig");
+const Cache = std.Build.Cache;
+const CodeSignature = @import("MachO/CodeSignature.zig");
+const Compilation = @import("../Compilation.zig");
+const Dwarf = File.Dwarf;
+const DwarfInfo = @import("MachO/DwarfInfo.zig");
+const Dylib = @import("MachO/Dylib.zig");
+const File = link.File;
+const Object = @import("MachO/Object.zig");
+const LibStub = tapi.LibStub;
+const Liveness = @import("../Liveness.zig");
+const LlvmObject = @import("../codegen/llvm.zig").Object;
+const Md5 = std.crypto.hash.Md5;
+const Module = @import("../Module.zig");
+const InternPool = @import("../InternPool.zig");
+const Platform = load_commands.Platform;
+const Relocation = @import("MachO/Relocation.zig");
+const StringTable = @import("strtab.zig").StringTable;
+const TableSection = @import("table_section.zig").TableSection;
+const Trie = @import("MachO/Trie.zig");
+const Type = @import("../type.zig").Type;
+const TypedValue = @import("../TypedValue.zig");
+const Value = @import("../value.zig").Value;
+
+pub const DebugSymbols = @import("MachO/DebugSymbols.zig");
+pub const Bind = @import("MachO/dyld_info/bind.zig").Bind(*const MachO, SymbolWithLoc);
+pub const LazyBind = @import("MachO/dyld_info/bind.zig").LazyBind(*const MachO, SymbolWithLoc);
+pub const Rebase = @import("MachO/dyld_info/Rebase.zig");
src/link/tapi.zig
@@ -81,6 +81,30 @@ pub const Tbd = union(enum) {
     v3: TbdV3,
     v4: TbdV4,
 
+    /// Caller owns memory.
+    pub fn targets(self: Tbd, gpa: Allocator) error{OutOfMemory}![]const []const u8 {
+        var out = std.ArrayList([]const u8).init(gpa);
+        defer out.deinit();
+
+        switch (self) {
+            .v3 => |v3| {
+                try out.ensureTotalCapacityPrecise(v3.archs.len);
+                for (v3.archs) |arch| {
+                    const target = try std.fmt.allocPrint(gpa, "{s}-{s}", .{ arch, v3.platform });
+                    out.appendAssumeCapacity(target);
+                }
+            },
+            .v4 => |v4| {
+                try out.ensureTotalCapacityPrecise(v4.targets.len);
+                for (v4.targets) |t| {
+                    out.appendAssumeCapacity(try gpa.dupe(u8, t));
+                }
+            },
+        }
+
+        return out.toOwnedSlice();
+    }
+
     pub fn currentVersion(self: Tbd) ?VersionField {
         return switch (self) {
             .v3 => |v3| v3.current_version,