Commit 2e28ab153c

Jakub Konka <kubkon@jakubkonka.com>
2023-08-28 20:55:45
macho: parse platform info from each object file into Platform struct
1 parent ec03619
src/link/MachO/Dylib.zig
@@ -217,7 +217,7 @@ const TargetMatcher = struct {
     target: CrossTarget,
     target_strings: std.ArrayListUnmanaged([]const u8) = .{},
 
-    fn init(allocator: Allocator, target: CrossTarget) !TargetMatcher {
+    pub fn init(allocator: Allocator, target: CrossTarget) !TargetMatcher {
         var self = TargetMatcher{
             .allocator = allocator,
             .target = target,
@@ -239,7 +239,7 @@ const TargetMatcher = struct {
         return self;
     }
 
-    fn deinit(self: *TargetMatcher) void {
+    pub fn deinit(self: *TargetMatcher) void {
         for (self.target_strings.items) |t| {
             self.allocator.free(t);
         }
@@ -263,7 +263,7 @@ const TargetMatcher = struct {
         };
     }
 
-    fn targetToAppleString(allocator: Allocator, target: CrossTarget) ![]const u8 {
+    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);
@@ -291,7 +291,7 @@ const TargetMatcher = struct {
         return hasValue(archs, cpuArchToAppleString(self.target.cpu_arch.?));
     }
 
-    fn matchesTargetTbd(self: TargetMatcher, tbd: Tbd) !bool {
+    pub fn matchesTargetTbd(self: TargetMatcher, tbd: Tbd) !bool {
         var arena = std.heap.ArenaAllocator.init(self.allocator);
         defer arena.deinit();
 
src/link/MachO/load_commands.zig
@@ -76,8 +76,14 @@ fn calcLCsSize(gpa: Allocator, options: *const link.Options, ctx: CalcLCsSizeCtx
     }
     // LC_SOURCE_VERSION
     sizeofcmds += @sizeOf(macho.source_version_command);
-    // LC_BUILD_VERSION
-    sizeofcmds += @sizeOf(macho.build_version_command) + @sizeOf(macho.build_tool_version);
+    // LC_BUILD_VERSION or LC_VERSION_MIN_
+    if (Platform.fromOptions(options).isBuildVersionCompatible()) {
+        // LC_BUILD_VERSION
+        sizeofcmds += @sizeOf(macho.build_version_command) + @sizeOf(macho.build_tool_version);
+    } else {
+        // LC_VERSION_MIN_
+        sizeofcmds += @sizeOf(macho.version_min_command);
+    }
     // LC_UUID
     sizeofcmds += @sizeOf(macho.uuid_command);
     // LC_LOAD_DYLIB
@@ -252,33 +258,28 @@ pub fn writeRpathLCs(gpa: Allocator, options: *const link.Options, lc_writer: an
     }
 }
 
-pub fn writeBuildVersionLC(options: *const link.Options, lc_writer: anytype) !void {
-    const cmdsize = @sizeOf(macho.build_version_command) + @sizeOf(macho.build_tool_version);
-    const platform_version = blk: {
-        const ver = options.target.os.version_range.semver.min;
-        const platform_version = @as(u32, @intCast(ver.major << 16 | ver.minor << 8));
-        break :blk platform_version;
-    };
-    const sdk_version: ?std.SemanticVersion = options.darwin_sdk_version orelse blk: {
-        if (options.sysroot) |path| break :blk inferSdkVersionFromSdkPath(path);
-        break :blk null;
+pub fn writeVersionMinLC(platform: Platform, sdk_version: ?std.SemanticVersion, lc_writer: anytype) !void {
+    const cmd: macho.LC = switch (platform.os_tag) {
+        .macos => .VERSION_MIN_MACOSX,
+        .ios => .VERSION_MIN_IPHONEOS,
+        .tvos => .VERSION_MIN_TVOS,
+        .watchos => .VERSION_MIN_WATCHOS,
+        else => unreachable,
     };
-    const sdk_version_value: u32 = if (sdk_version) |ver|
-        @intCast(ver.major << 16 | ver.minor << 8)
-    else
-        platform_version;
-    const is_simulator_abi = options.target.abi == .simulator;
+    try lc_writer.writeAll(mem.asBytes(&macho.version_min_command{
+        .cmd = cmd,
+        .version = platform.toAppleVersion(),
+        .sdk = if (sdk_version) |ver| Platform.semanticVersionToAppleVersion(ver) else platform.toAppleVersion(),
+    }));
+}
+
+pub fn writeBuildVersionLC(platform: Platform, sdk_version: ?std.SemanticVersion, lc_writer: anytype) !void {
+    const cmdsize = @sizeOf(macho.build_version_command) + @sizeOf(macho.build_tool_version);
     try lc_writer.writeStruct(macho.build_version_command{
         .cmdsize = cmdsize,
-        .platform = switch (options.target.os.tag) {
-            .macos => .MACOS,
-            .ios => if (is_simulator_abi) macho.PLATFORM.IOSSIMULATOR else macho.PLATFORM.IOS,
-            .watchos => if (is_simulator_abi) macho.PLATFORM.WATCHOSSIMULATOR else macho.PLATFORM.WATCHOS,
-            .tvos => if (is_simulator_abi) macho.PLATFORM.TVOSSIMULATOR else macho.PLATFORM.TVOS,
-            else => unreachable,
-        },
-        .minos = platform_version,
-        .sdk = sdk_version_value,
+        .platform = platform.toApplePlatform(),
+        .minos = platform.toAppleVersion(),
+        .sdk = if (sdk_version) |ver| Platform.semanticVersionToAppleVersion(ver) else platform.toAppleVersion(),
         .ntools = 1,
     });
     try lc_writer.writeAll(mem.asBytes(&macho.build_tool_version{
@@ -301,7 +302,124 @@ pub fn writeLoadDylibLCs(dylibs: []const Dylib, referenced: []u16, lc_writer: an
     }
 }
 
-fn inferSdkVersionFromSdkPath(path: []const u8) ?std.SemanticVersion {
+pub const Platform = struct {
+    os_tag: std.Target.Os.Tag,
+    abi: std.Target.Abi,
+    version: std.SemanticVersion,
+
+    /// Using Apple's ld64 as our blueprint, `min_version` as well as `sdk_version` are set to
+    /// the extracted minimum platform version.
+    pub fn fromLoadCommand(lc: macho.LoadCommandIterator.LoadCommand) Platform {
+        switch (lc.cmd()) {
+            .BUILD_VERSION => {
+                const cmd = lc.cast(macho.build_version_command).?;
+                return .{
+                    .os_tag = switch (cmd.platform) {
+                        .MACOS => .macos,
+                        .IOS, .IOSSIMULATOR => .ios,
+                        .TVOS, .TVOSSIMULATOR => .tvos,
+                        .WATCHOS, .WATCHOSSIMULATOR => .watchos,
+                        else => @panic("TODO"),
+                    },
+                    .abi = switch (cmd.platform) {
+                        .IOSSIMULATOR,
+                        .TVOSSIMULATOR,
+                        .WATCHOSSIMULATOR,
+                        => .simulator,
+                        else => .none,
+                    },
+                    .version = appleVersionToSemanticVersion(cmd.minos),
+                };
+            },
+            .VERSION_MIN_MACOSX,
+            .VERSION_MIN_IPHONEOS,
+            .VERSION_MIN_TVOS,
+            .VERSION_MIN_WATCHOS,
+            => {
+                const cmd = lc.cast(macho.version_min_command).?;
+                return .{
+                    .os_tag = switch (lc.cmd()) {
+                        .VERSION_MIN_MACOSX => .macos,
+                        .VERSION_MIN_IPHONEOS => .ios,
+                        .VERSION_MIN_TVOS => .tvos,
+                        .VERSION_MIN_WATCHOS => .watchos,
+                        else => unreachable,
+                    },
+                    .abi = .none,
+                    .version = appleVersionToSemanticVersion(cmd.version),
+                };
+            },
+            else => unreachable,
+        }
+    }
+
+    pub fn fromOptions(options: *const link.Options) Platform {
+        return .{
+            .os_tag = options.target.os.tag,
+            .abi = options.target.abi,
+            .version = options.target.os.version_range.semver.min,
+        };
+    }
+
+    pub fn toAppleVersion(plat: Platform) u32 {
+        return semanticVersionToAppleVersion(plat.version);
+    }
+
+    pub fn toApplePlatform(plat: Platform) macho.PLATFORM {
+        return switch (plat.os_tag) {
+            .macos => .MACOS,
+            .ios => if (plat.abi == .simulator) .IOSSIMULATOR else .IOS,
+            .tvos => if (plat.abi == .simulator) .TVOSSIMULATOR else .TVOS,
+            .watchos => if (plat.abi == .simulator) .WATCHOSSIMULATOR else .WATCHOS,
+            else => unreachable,
+        };
+    }
+
+    pub fn isBuildVersionCompatible(plat: Platform) bool {
+        inline for (supported_platforms) |sup_plat| {
+            if (sup_plat[0] == plat.os_tag and sup_plat[1] == plat.abi) {
+                return sup_plat[2] <= plat.toAppleVersion();
+            }
+        }
+        return false;
+    }
+
+    pub inline fn semanticVersionToAppleVersion(version: std.SemanticVersion) u32 {
+        const major = version.major;
+        const minor = version.minor;
+        const patch = version.patch;
+        return (@as(u32, @intCast(major)) << 16) | (@as(u32, @intCast(minor)) << 8) | @as(u32, @intCast(patch));
+    }
+
+    inline fn appleVersionToSemanticVersion(version: u32) std.SemanticVersion {
+        return .{
+            .major = @as(u16, @truncate(version >> 16)),
+            .minor = @as(u8, @truncate(version >> 8)),
+            .patch = @as(u8, @truncate(version)),
+        };
+    }
+};
+
+const SupportedPlatforms = struct {
+    std.Target.Os.Tag,
+    std.Target.Abi,
+    u32, // Min platform version for which to emit LC_BUILD_VERSION
+    u32, // Min supported platform version
+    ?[]const u8, // Env var to look for
+};
+
+// Source: https://github.com/apple-oss-distributions/ld64/blob/59a99ab60399c5e6c49e6945a9e1049c42b71135/src/ld/PlatformSupport.cpp#L52
+const supported_platforms = [_]SupportedPlatforms{
+    .{ .macos, .none, 0xA0E00, 0xA0800, "MACOSX_DEPLOYMENT_TARGET" },
+    .{ .ios, .none, 0xC0000, 0x70000, "IPHONEOS_DEPLOYMENT_TARGET" },
+    .{ .tvos, .none, 0xC0000, 0x70000, "TVOS_DEPLOYMENT_TARGET" },
+    .{ .watchos, .none, 0x50000, 0x20000, "WATCHOS_DEPLOYMENT_TARGET" },
+    .{ .ios, .simulator, 0xD0000, 0x80000, null },
+    .{ .tvos, .simulator, 0xD0000, 0x80000, null },
+    .{ .watchos, .simulator, 0x60000, 0x20000, null },
+};
+
+pub fn inferSdkVersionFromSdkPath(path: []const u8) ?std.SemanticVersion {
     const stem = std.fs.path.stem(path);
     const start = for (stem, 0..) |c, i| {
         if (std.ascii.isDigit(c)) break i;
src/link/MachO/Object.zig
@@ -940,6 +940,26 @@ pub fn parseDwarfInfo(self: Object) DwarfInfo {
     return di;
 }
 
+/// Returns Options.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{
+        .ncmds = self.header.ncmds,
+        .buffer = self.contents[@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;
+}
+
 pub fn getSectionContents(self: Object, sect: macho.section_64) []const u8 {
     const size = @as(usize, @intCast(sect.size));
     return self.contents[sect.offset..][0..size];
@@ -1089,5 +1109,6 @@ const Atom = @import("Atom.zig");
 const DwarfInfo = @import("DwarfInfo.zig");
 const LoadCommandIterator = macho.LoadCommandIterator;
 const MachO = @import("../MachO.zig");
+const Platform = @import("load_commands.zig").Platform;
 const SymbolWithLoc = MachO.SymbolWithLoc;
 const UnwindInfo = @import("UnwindInfo.zig");
src/link/MachO/zld.zig
@@ -345,10 +345,13 @@ pub fn linkWithZld(
             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_os: std.Target.Os.Tag,
+        } = .{
+            .detected_arch = undefined,
+            .detected_os = undefined,
+        };
 
         for (positionals.items) |obj| {
             const in_file = try std.fs.cwd().openFile(obj.path, .{});
@@ -586,7 +589,18 @@ pub fn linkWithZld(
         try lc_writer.writeStruct(macho.source_version_command{
             .version = 0,
         });
-        try load_commands.writeBuildVersionLC(&macho_file.base.options, lc_writer);
+        {
+            const platform = load_commands.Platform.fromOptions(&macho_file.base.options);
+            const sdk_version: ?std.SemanticVersion = macho_file.base.options.darwin_sdk_version orelse blk: {
+                if (macho_file.base.options.sysroot) |path| break :blk load_commands.inferSdkVersionFromSdkPath(path);
+                break :blk null;
+            };
+            if (platform.isBuildVersionCompatible()) {
+                try load_commands.writeBuildVersionLC(platform, sdk_version, lc_writer);
+            } else {
+                try load_commands.writeVersionMinLC(platform, sdk_version, lc_writer);
+            }
+        }
 
         const uuid_cmd_offset = @sizeOf(macho.mach_header_64) + @as(u32, @intCast(lc_buffer.items.len));
         try lc_writer.writeStruct(macho_file.uuid_cmd);
src/link/MachO.zig
@@ -585,7 +585,18 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
     try lc_writer.writeStruct(macho.source_version_command{
         .version = 0,
     });
-    try load_commands.writeBuildVersionLC(&self.base.options, lc_writer);
+    {
+        const platform = load_commands.Platform.fromOptions(&self.base.options);
+        const sdk_version: ?std.SemanticVersion = self.base.options.darwin_sdk_version orelse blk: {
+            if (self.base.options.sysroot) |path| break :blk load_commands.inferSdkVersionFromSdkPath(path);
+            break :blk null;
+        };
+        if (platform.isBuildVersionCompatible()) {
+            try load_commands.writeBuildVersionLC(platform, sdk_version, lc_writer);
+        } else {
+            try load_commands.writeVersionMinLC(platform, sdk_version, lc_writer);
+        }
+    }
 
     const uuid_cmd_offset = @sizeOf(macho.mach_header_64) + @as(u32, @intCast(lc_buffer.items.len));
     try lc_writer.writeStruct(self.uuid_cmd);
@@ -797,7 +808,7 @@ fn parseObject(
     const self_cpu_arch = self.base.options.target.cpu.arch;
 
     if (self_cpu_arch != cpu_arch) {
-        error_ctx.* = .{ .detected_arch = cpu_arch };
+        error_ctx.detected_arch = cpu_arch;
         return error.InvalidArch;
     }
 }
@@ -894,7 +905,7 @@ fn parseArchive(
             else => unreachable,
         };
         if (cpu_arch != parsed_cpu_arch) {
-            error_ctx.* = .{ .detected_arch = parsed_cpu_arch };
+            error_ctx.detected_arch = parsed_cpu_arch;
             return error.InvalidArch;
         }
     }
@@ -959,7 +970,7 @@ fn parseDylib(
         else => unreachable,
     };
     if (self_cpu_arch != cpu_arch) {
-        error_ctx.* = .{ .detected_arch = cpu_arch };
+        error_ctx.detected_arch = cpu_arch;
         return error.InvalidArch;
     }