Commit 8afe6210e9

Jakub Konka <kubkon@jakubkonka.com>
2021-08-07 10:23:30
macho: add TAPI v3 parser
This turns out needed to correctly support version back to macOS 10.14 (Mojave)
1 parent 60a5552
Changed files (2)
src
src/link/MachO/Dylib.zig
@@ -10,9 +10,10 @@ const math = std.math;
 const mem = std.mem;
 const fat = @import("fat.zig");
 const commands = @import("commands.zig");
+const tapi = @import("../tapi.zig");
 
 const Allocator = mem.Allocator;
-const LibStub = @import("../tapi.zig").LibStub;
+const LibStub = tapi.LibStub;
 const LoadCommand = commands.LoadCommand;
 const MachO = @import("../MachO.zig");
 
@@ -315,9 +316,9 @@ fn parseSymbols(self: *Dylib, allocator: *Allocator) !void {
     }
 }
 
-fn hasTarget(targets: []const []const u8, target: []const u8) bool {
-    for (targets) |t| {
-        if (mem.eql(u8, t, target)) return true;
+fn hasValue(stack: []const []const u8, needle: []const u8) bool {
+    for (stack) |v| {
+        if (mem.eql(u8, v, needle)) return true;
     }
     return false;
 }
@@ -334,6 +335,78 @@ fn addObjCClassSymbols(self: *Dylib, allocator: *Allocator, sym_name: []const u8
     }
 }
 
+fn hasArch(archs: []const []const u8, arch: []const u8) bool {
+    for (archs) |x| {
+        if (mem.eql(u8, x, arch)) return true;
+    }
+    return false;
+}
+
+fn parseFromStubV3(self: *Dylib, allocator: *Allocator, target: std.Target, lib_stub: LibStub) !void {
+    var umbrella_libs = std.StringHashMap(void).init(allocator);
+    defer umbrella_libs.deinit();
+
+    const arch_string = @tagName(target.cpu.arch);
+
+    for (lib_stub.inner) |elem, stub_index| {
+        const stub = elem.v3;
+        if (!hasArch(stub.archs, arch_string)) continue;
+
+        if (stub_index > 0) {
+            // TODO I thought that we could switch on presence of `parent-umbrella` map;
+            // however, turns out `libsystem_notify.dylib` is fully reexported by `libSystem.dylib`
+            // BUT does not feature a `parent-umbrella` map as the only sublib. Apple's bug perhaps?
+            try umbrella_libs.put(stub.install_name, .{});
+        }
+
+        if (stub.exports) |exports| {
+            for (exports) |exp| {
+                if (!hasArch(exp.archs, arch_string)) continue;
+
+                if (exp.symbols) |symbols| {
+                    for (symbols) |sym_name| {
+                        if (self.symbols.contains(sym_name)) continue;
+                        try self.symbols.putNoClobber(allocator, try allocator.dupe(u8, sym_name), {});
+                    }
+                }
+
+                if (exp.re_exports) |re_exports| {
+                    for (re_exports) |reexp| {
+                        if (self.symbols.contains(reexp)) continue;
+                        try self.symbols.putNoClobber(allocator, try allocator.dupe(u8, reexp), {});
+                    }
+                }
+            }
+        }
+    }
+
+    log.debug("{s}", .{lib_stub.inner[0].installName()});
+
+    // // TODO track which libs were already parsed in different steps
+    // for (lib_stub.inner) |elem| {
+    //     const stub = elem.v3;
+    //     if (!archMatches(stub.archs, arch_string)) continue;
+
+    //     if (stub.reexported_libraries) |reexports| {
+    //         for (reexports) |reexp| {
+    //             if (!matcher.matches(reexp.targets)) continue;
+
+    //             for (reexp.libraries) |lib| {
+    //                 if (umbrella_libs.contains(lib)) {
+    //                     log.debug("  | {s} <= {s}", .{ lib, umbrella_lib.install_name });
+    //                     continue;
+    //                 }
+
+    //                 log.debug("  | {s}", .{lib});
+
+    //                 const dep_id = try Id.default(allocator, lib);
+    //                 try self.dependent_libs.append(allocator, dep_id);
+    //             }
+    //         }
+    //     }
+    // }
+}
+
 fn targetToAppleString(allocator: *Allocator, target: std.Target) ![]const u8 {
     const arch = switch (target.cpu.arch) {
         .aarch64 => "arm64",
@@ -380,6 +453,13 @@ const TargetMatcher = struct {
         self.target_strings.deinit(self.allocator);
     }
 
+    fn hasTarget(targets: []const []const u8, target: []const u8) bool {
+        for (targets) |x| {
+            if (mem.eql(u8, x, target)) return true;
+        }
+        return false;
+    }
+
     fn matches(self: TargetMatcher, targets: []const []const u8) bool {
         for (self.target_strings.items) |t| {
             if (hasTarget(targets, t)) return true;
@@ -388,29 +468,15 @@ const TargetMatcher = struct {
     }
 };
 
-pub fn parseFromStub(self: *Dylib, allocator: *Allocator, target: std.Target, lib_stub: LibStub) !void {
-    if (lib_stub.inner.len == 0) return error.EmptyStubFile;
-
-    log.debug("parsing shared library from stub '{s}'", .{self.name});
-
-    const umbrella_lib = lib_stub.inner[0];
-
-    var id = try Id.default(allocator, umbrella_lib.install_name);
-    if (umbrella_lib.current_version) |version| {
-        try id.parseCurrentVersion(version);
-    }
-    if (umbrella_lib.compatibility_version) |version| {
-        try id.parseCompatibilityVersion(version);
-    }
-    self.id = id;
-
+fn parseFromStubV4(self: *Dylib, allocator: *Allocator, target: std.Target, lib_stub: LibStub) !void {
     var matcher = try TargetMatcher.init(allocator, target);
     defer matcher.deinit();
 
     var umbrella_libs = std.StringHashMap(void).init(allocator);
     defer umbrella_libs.deinit();
 
-    for (lib_stub.inner) |stub, stub_index| {
+    for (lib_stub.inner) |elem, stub_index| {
+        const stub = elem.v4;
         if (!matcher.matches(stub.targets)) continue;
 
         if (stub_index > 0) {
@@ -465,10 +531,11 @@ pub fn parseFromStub(self: *Dylib, allocator: *Allocator, target: std.Target, li
         }
     }
 
-    log.debug("{s}", .{umbrella_lib.install_name});
+    log.debug("{s}", .{lib_stub.inner[0].installName()});
 
     // TODO track which libs were already parsed in different steps
-    for (lib_stub.inner) |stub| {
+    for (lib_stub.inner) |elem| {
+        const stub = elem.v4;
         if (!matcher.matches(stub.targets)) continue;
 
         if (stub.reexported_libraries) |reexports| {
@@ -477,7 +544,7 @@ pub fn parseFromStub(self: *Dylib, allocator: *Allocator, target: std.Target, li
 
                 for (reexp.libraries) |lib| {
                     if (umbrella_libs.contains(lib)) {
-                        log.debug("  | {s} <= {s}", .{ lib, umbrella_lib.install_name });
+                        log.debug("  | {s} <= {s}", .{ lib, lib_stub.inner[0].installName() });
                         continue;
                     }
 
@@ -491,6 +558,28 @@ pub fn parseFromStub(self: *Dylib, allocator: *Allocator, target: std.Target, li
     }
 }
 
+pub fn parseFromStub(self: *Dylib, allocator: *Allocator, target: std.Target, lib_stub: LibStub) !void {
+    if (lib_stub.inner.len == 0) return error.EmptyStubFile;
+
+    log.debug("parsing shared library from stub '{s}'", .{self.name});
+
+    const umbrella_lib = lib_stub.inner[0];
+
+    var id = try Id.default(allocator, umbrella_lib.installName());
+    if (umbrella_lib.currentVersion()) |version| {
+        try id.parseCurrentVersion(version);
+    }
+    if (umbrella_lib.compatibilityVersion()) |version| {
+        try id.parseCompatibilityVersion(version);
+    }
+    self.id = id;
+
+    switch (umbrella_lib) {
+        .v3 => try self.parseFromStubV3(allocator, target, lib_stub),
+        .v4 => try self.parseFromStubV4(allocator, target, lib_stub),
+    }
+}
+
 pub fn parseDependentLibs(
     self: *Dylib,
     allocator: *Allocator,
src/link/tapi.zig
@@ -6,6 +6,88 @@ const log = std.log.scoped(.tapi);
 const Allocator = mem.Allocator;
 const Yaml = @import("tapi/yaml.zig").Yaml;
 
+const VersionField = union(enum) {
+    string: []const u8,
+    float: f64,
+    int: u64,
+};
+
+pub const TbdV3 = struct {
+    archs: []const []const u8,
+    uuids: []const []const u8,
+    platform: []const u8,
+    install_name: []const u8,
+    current_version: ?VersionField,
+    compatibility_version: ?VersionField,
+    objc_constraint: []const u8,
+    exports: ?[]const struct {
+        archs: []const []const u8,
+        re_exports: ?[]const []const u8,
+        symbols: ?[]const []const u8,
+    },
+};
+
+pub const TbdV4 = struct {
+    tbd_version: u3,
+    targets: []const []const u8,
+    uuids: []const struct {
+        target: []const u8,
+        value: []const u8,
+    },
+    install_name: []const u8,
+    current_version: ?VersionField,
+    compatibility_version: ?VersionField,
+    reexported_libraries: ?[]const struct {
+        targets: []const []const u8,
+        libraries: []const []const u8,
+    },
+    parent_umbrella: ?[]const struct {
+        targets: []const []const u8,
+        umbrella: []const u8,
+    },
+    exports: ?[]const struct {
+        targets: []const []const u8,
+        symbols: ?[]const []const u8,
+        objc_classes: ?[]const []const u8,
+    },
+    reexports: ?[]const struct {
+        targets: []const []const u8,
+        symbols: ?[]const []const u8,
+        objc_classes: ?[]const []const u8,
+    },
+    allowable_clients: ?[]const struct {
+        targets: []const []const u8,
+        clients: []const []const u8,
+    },
+    objc_classes: ?[]const []const u8,
+};
+
+pub const Tbd = union(enum) {
+    v3: TbdV3,
+    v4: TbdV4,
+
+    pub fn currentVersion(self: Tbd) ?VersionField {
+        return switch (self) {
+            .v3 => |v3| v3.current_version,
+            .v4 => |v4| v4.current_version,
+        };
+    }
+
+    pub fn compatibilityVersion(self: Tbd) ?VersionField {
+        return switch (self) {
+            .v3 => |v3| v3.compatibility_version,
+            .v4 => |v4| v4.compatibility_version,
+        };
+    }
+
+    pub fn installName(self: Tbd) []const u8 {
+        return switch (self) {
+            .v3 => |v3| v3.install_name,
+            .v4 => |v4| v4.install_name,
+        };
+    }
+};
+
 pub const LibStub = struct {
     /// Underlying memory for stub's contents.
     yaml: Yaml,
@@ -13,49 +95,6 @@ pub const LibStub = struct {
     /// Typed contents of the tbd file.
     inner: []Tbd,
 
-    const Tbd = struct {
-        tbd_version: u3,
-        targets: []const []const u8,
-        uuids: []const struct {
-            target: []const u8,
-            value: []const u8,
-        },
-        install_name: []const u8,
-        current_version: ?union(enum) {
-            string: []const u8,
-            float: f64,
-            int: u64,
-        },
-        compatibility_version: ?union(enum) {
-            string: []const u8,
-            float: f64,
-            int: u64,
-        },
-        reexported_libraries: ?[]const struct {
-            targets: []const []const u8,
-            libraries: []const []const u8,
-        },
-        parent_umbrella: ?[]const struct {
-            targets: []const []const u8,
-            umbrella: []const u8,
-        },
-        exports: ?[]const struct {
-            targets: []const []const u8,
-            symbols: ?[]const []const u8,
-            objc_classes: ?[]const []const u8,
-        },
-        reexports: ?[]const struct {
-            targets: []const []const u8,
-            symbols: ?[]const []const u8,
-            objc_classes: ?[]const []const u8,
-        },
-        allowable_clients: ?[]const struct {
-            targets: []const []const u8,
-            clients: []const []const u8,
-        },
-        objc_classes: ?[]const []const u8,
-    };
-
     pub fn loadFromFile(allocator: *Allocator, file: fs.File) !LibStub {
         const source = try file.readToEndAlloc(allocator, std.math.maxInt(u32));
         defer allocator.free(source);
@@ -65,16 +104,41 @@ pub const LibStub = struct {
             .inner = undefined,
         };
 
-        lib_stub.inner = lib_stub.yaml.parse([]Tbd) catch |err| blk: {
-            switch (err) {
-                error.TypeMismatch => {
-                    // TODO clean this up.
-                    var out = try lib_stub.yaml.arena.allocator.alloc(Tbd, 1);
-                    out[0] = try lib_stub.yaml.parse(Tbd);
-                    break :blk out;
-                },
-                else => |e| return e,
+        // TODO clean this up.
+        lib_stub.inner = blk: {
+            err: {
+                const inner = lib_stub.yaml.parse([]TbdV4) catch |err| switch (err) {
+                    error.TypeMismatch => break :err,
+                    else => |e| return e,
+                };
+                var out = try lib_stub.yaml.arena.allocator.alloc(Tbd, inner.len);
+                for (inner) |doc, i| {
+                    out[i] = .{ .v4 = doc };
+                }
+                break :blk out;
+            }
+
+            err: {
+                const inner = lib_stub.yaml.parse(TbdV4) catch |err| switch (err) {
+                    error.TypeMismatch => break :err,
+                    else => |e| return e,
+                };
+                var out = try lib_stub.yaml.arena.allocator.alloc(Tbd, 1);
+                out[0] = .{ .v4 = inner };
+                break :blk out;
+            }
+
+            err: {
+                const inner = lib_stub.yaml.parse(TbdV3) catch |err| switch (err) {
+                    error.TypeMismatch => break :err,
+                    else => |e| return e,
+                };
+                var out = try lib_stub.yaml.arena.allocator.alloc(Tbd, 1);
+                out[0] = .{ .v3 = inner };
+                break :blk out;
             }
+
+            return error.TypeMismatch;
         };
 
         return lib_stub;