From 5a6eaf634be944e91fe16659ea636e0cb063c78a Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Fri, 16 May 2025 00:06:47 +0200 Subject: [PATCH 1/3] Enable macos-15 runner --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 16d8a03c..028239f9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ macos-13, macos-14 ] + os: [ macos-13, macos-14, macos-15 ] steps: - uses: actions/checkout@v3 From 58cd472a52fe43021d88ae759a8cc4fd5591c2f6 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Sat, 17 May 2025 08:25:57 +0200 Subject: [PATCH 2/3] Add failing test case --- test/macho.zig | 55 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/test/macho.zig b/test/macho.zig index 4338235f..4dd25d07 100644 --- a/test/macho.zig +++ b/test/macho.zig @@ -7,6 +7,7 @@ pub fn addTests(step: *Step, opts: Options) void { step.dependOn(testBuildVersionIOS(b, opts)); step.dependOn(testDeadStrip(b, opts)); step.dependOn(testDeadStripDylibs(b, opts)); + step.dependOn(testDedupDylibs(b, opts)); step.dependOn(testDylib(b, opts)); step.dependOn(testDylibReexport(b, opts)); step.dependOn(testDylibReexportDeep(b, opts)); @@ -384,6 +385,59 @@ fn testDeadStripDylibs(b: *Build, opts: Options) *Step { return test_step; } +fn testDedupDylibs(b: *Build, opts: Options) *Step { + const test_step = b.step("test-macho-dedup-dylibs", ""); + + const obj = cc(b, "a.o", opts); + obj.addArg("-c"); + obj.addCSource( + \\char world[] = "world"; + \\char* hello() { + \\ return "Hello"; + \\} + ); + + const dylib = ld(b, "liba.dylib", opts); + dylib.addFileSource(obj.getFile()); + dylib.addArgs(&.{ + "-dynamic", + "-syslibroot", + opts.macos_sdk, + "-dylib", + "-install_name", + "@rpath/liba.dylib", + "-lSystem", + "-lc", + }); + + const check = dylib.check(); + check.checkInHeaders(); + // Check that we only have one copy of libSystem present + check.checkContains("libSystem"); + check.checkNotPresent("libSystem"); + test_step.dependOn(&check.step); + + const exe = cc(b, "main", opts); + exe.addCSource( + \\#include + \\char* hello(); + \\extern char world[]; + \\int main() { + \\ printf("%s %s", hello(), world); + \\ return 0; + \\} + ); + exe.addArg("-la"); + exe.addPrefixedDirectorySource("-L", dylib.getDir()); + exe.addPrefixedDirectorySource("-Wl,-rpath,", dylib.getDir()); + + const run = exe.run(); + run.expectStdOutEqual("Hello world"); + test_step.dependOn(run.step()); + + return test_step; +} + fn testDylib(b: *Build, opts: Options) *Step { const test_step = b.step("test-macho-dylib", ""); @@ -2801,7 +2855,6 @@ fn testSearchStrategy(b: *Build, opts: Options) *Step { const obj = cc(b, "a.o", opts); obj.addArg("-c"); obj.addCSource( - \\#include \\char world[] = "world"; \\char* hello() { \\ return "Hello"; From 70c9f2000618afe5b28f24742f489fc47fb368e1 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Sun, 18 May 2025 08:07:29 +0200 Subject: [PATCH 3/3] Remove duplicate dylibs This is a loader hard-error now to load the same library multiple times since macOS 15. --- src/Dylib.zig | 19 ++++++++++++++++++ src/MachO.zig | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/macho.zig | 1 + 3 files changed, 73 insertions(+) diff --git a/src/Dylib.zig b/src/Dylib.zig index b9cb68c7..fe7b12aa 100644 --- a/src/Dylib.zig +++ b/src/Dylib.zig @@ -1011,6 +1011,25 @@ pub const Id = struct { return out; } + + pub fn eql(id: Id, other: Id) bool { + return mem.eql(u8, id.name, other.name) and + id.timestamp == other.timestamp and + id.current_version == other.current_version and + id.compatibility_version == other.compatibility_version; + } + + /// Hashes the Id. + /// TODO: we currently do differentiate between dylibs installed at the *same* path but having different + /// versions. This might be wrong and we should dedup them. Then again, surely this should be an error, right? + pub fn hash(id: Id) u64 { + var hasher = std.hash.Wyhash.init(0); + hasher.update(id.name); + hasher.update(mem.asBytes(&id.timestamp)); + hasher.update(mem.asBytes(&id.current_version)); + hasher.update(mem.asBytes(&id.compatibility_version)); + return hasher.final(); + } }; const Export = struct { diff --git a/src/MachO.zig b/src/MachO.zig index 36f9a8bb..78cebf6e 100644 --- a/src/MachO.zig +++ b/src/MachO.zig @@ -249,6 +249,7 @@ pub fn link(self: *MachO) !void { } try self.parseInputFiles(); + try self.dedupDylibs(resolved_objects.items); try self.parseDependentDylibs(arena, lib_dirs.items, framework_dirs.items); if (!self.options.relocatable) { @@ -832,6 +833,58 @@ fn isHoisted(self: *MachO, install_name: []const u8) bool { return false; } +fn dedupDylibs(self: *MachO, resolved_objects: []const LinkObject) !void { + const tracy = trace(@src()); + defer tracy.end(); + + var map = std.HashMap(Dylib.Id, void, struct { + pub fn hash(ctx: @This(), id: Dylib.Id) u64 { + _ = ctx; + return id.hash(); + } + + pub fn eql(ctx: @This(), id: Dylib.Id, other: Dylib.Id) bool { + _ = ctx; + return id.eql(other); + } + }, std.hash_map.default_max_load_percentage).init(self.allocator); + defer map.deinit(); + try map.ensureTotalCapacity(@intCast(self.dylibs.items.len)); + + var marked_dylibs = std.ArrayList(bool).init(self.allocator); + defer marked_dylibs.deinit(); + try marked_dylibs.ensureTotalCapacityPrecise(self.dylibs.items.len); + marked_dylibs.resize(self.dylibs.items.len) catch unreachable; + @memset(marked_dylibs.items, false); + + for (self.dylibs.items, marked_dylibs.items) |index, *marker| { + const dylib = self.getFile(index).dylib; + const cmd_object = resolved_objects[@intFromEnum(index)]; + + const gop = map.getOrPutAssumeCapacity(dylib.id.?); + + if (!gop.found_existing) continue; + + if (cmd_object.tag == .lib) { + self.warn("ignoring duplicate libraries: {}", .{cmd_object}); + } + + marker.* = true; + } + + var i: usize = 0; + while (i < self.dylibs.items.len) { + const index = self.dylibs.items[i]; + const marker = marked_dylibs.items[i]; + if (marker) { + _ = self.dylibs.orderedRemove(i); + _ = marked_dylibs.orderedRemove(i); + self.files.items(.data)[@intFromEnum(index)].dylib.deinit(self.allocator); + self.files.set(@intFromEnum(index), .none); + } else i += 1; + } +} + fn parseDependentDylibs( self: *MachO, arena: Allocator, diff --git a/test/macho.zig b/test/macho.zig index 4dd25d07..dfd02ccd 100644 --- a/test/macho.zig +++ b/test/macho.zig @@ -407,6 +407,7 @@ fn testDedupDylibs(b: *Build, opts: Options) *Step { "-install_name", "@rpath/liba.dylib", "-lSystem", + "-lSystem", "-lc", });