Skip to content

Commit c44f860

Browse files
committed
Fix the linux CI build as well.
The 0.1.0 bundle had /Users/runner/hostedtoolcache/.../libruby.4.0.dylib hardcoded in its install_name; dyld refused to load it on any non-CI machine. ld couldn't find libruby in setup-ruby's libdir and fell back to /usr/lib, linking the .so against Ubuntu's system Ruby 3.2. That ABI mismatch was hidden by the silent pure-Ruby fallback, which means CI had been running fallback specs on Linux all along. Seems that the problem is in zig_rb's build.zig polluting the exported module.
1 parent ed826d0 commit c44f860

2 files changed

Lines changed: 39 additions & 17 deletions

File tree

Rakefile

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,31 @@ task compile: :sync_version do
3131
fix_macos_install_names if RUBY_PLATFORM.include?("darwin")
3232
end
3333

34-
# Zig's link line for the bundle picks up libruby with an absolute install
35-
# name (CI's hostedtoolcache, or a developer's prefix). Without rewriting,
36-
# dlopen on a different machine fails because the path doesn't exist.
37-
# Convert to @rpath form and add an rpath that resolves at load time
38-
# relative to the host ruby executable, then re-codesign (Apple Silicon
39-
# requires a valid signature; modifying load commands invalidates it).
34+
# Strip non-portable artifacts the Zig linker leaves in the macOS bundle:
35+
# absolute LC_RPATH entries (from Zig's addLibraryPath, which targets the
36+
# host ruby's libdir) and any absolute libruby reference that may slip back
37+
# in if zig_rb's contamination returns. Re-codesign because Apple Silicon
38+
# rejects unsigned binaries and modifying load commands invalidates the
39+
# signature.
4040
def fix_macos_install_names
4141
bundles = Dir["lib/carbon_fiber/*/carbon_fiber_native.bundle"]
4242
raise "No macOS bundles to post-process under lib/carbon_fiber/*/" if bundles.empty?
4343

4444
bundles.each do |bundle|
4545
deps = `otool -L #{bundle.shellescape}`
4646
libruby_line = deps.lines.find { |l| l.match?(%r{/libruby\.\d+\.\d+\.dylib}) }
47-
raise "No libruby reference found in #{bundle}; link layout changed?" unless libruby_line
48-
49-
old = libruby_line.match(/^\s*(\S.*?)\s+\(/)[1]
50-
if old.start_with?("/")
51-
new = "@rpath/#{File.basename(old)}"
52-
sh "install_name_tool", "-change", old, new, bundle
47+
if libruby_line
48+
old = libruby_line.match(/^\s*(\S.*?)\s+\(/)[1]
49+
if old.start_with?("/")
50+
new = "@rpath/#{File.basename(old)}"
51+
sh "install_name_tool", "-change", old, new, bundle
52+
end
5353
end
5454

5555
rpaths = `otool -l #{bundle.shellescape}`.scan(/^\s+path\s+(.+?)\s+\(offset \d+\)/).flatten
5656
rpaths.each do |rpath|
5757
sh "install_name_tool", "-delete_rpath", rpath, bundle if rpath.start_with?("/")
5858
end
59-
unless rpaths.include?("@executable_path/../lib")
60-
sh "install_name_tool", "-add_rpath", "@executable_path/../lib", bundle
61-
end
6259

6360
sh "codesign", "--sign", "-", "--force", bundle
6461

build.zig

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,32 @@ pub fn build(b: *std.Build) void {
1313
.optimize = optimize,
1414
});
1515

16+
// zig_rb's build.zig calls `zig_tests.linkSystemLibrary("ruby")` on a
17+
// Compile step whose root_module is the same module exported to
18+
// consumers; via the deprecated alias, that mutates the shared module's
19+
// link_objects with a "ruby" system_lib. Inheriting it would bake an
20+
// absolute libruby path into the macOS bundle and pull in whatever
21+
// libruby Linux's ld happens to resolve (often the runner's system
22+
// Ruby, breaking cross-version loads). Strip it here; Ruby's C-API
23+
// symbols are resolved at dlopen against the host ruby process, like
24+
// every other distributable Ruby native extension.
25+
const rb_module = zig_rb_dep.module("zig_rb");
26+
var i: usize = 0;
27+
while (i < rb_module.link_objects.items.len) {
28+
const lo = rb_module.link_objects.items[i];
29+
if (lo == .system_lib and std.mem.eql(u8, lo.system_lib.name, "ruby")) {
30+
_ = rb_module.link_objects.orderedRemove(i);
31+
} else {
32+
i += 1;
33+
}
34+
}
35+
1636
const fibers_module = b.createModule(.{
1737
.root_source_file = b.path("ext/carbon_fiber_native/main.zig"),
1838
.target = target,
1939
.optimize = optimize,
2040
.imports = &.{
21-
.{ .name = "rb", .module = zig_rb_dep.module("zig_rb") },
41+
.{ .name = "rb", .module = rb_module },
2242
.{ .name = "xev", .module = libxev_dep.module("xev") },
2343
},
2444
});
@@ -38,12 +58,17 @@ pub fn build(b: *std.Build) void {
3858
},
3959
);
4060

61+
// macOS' linker rejects undefined symbols in shared libraries by default;
62+
// Linux' ld permits them. Allow undefined Ruby C-API symbols on macOS so
63+
// dyld resolves them at dlopen against the host ruby process.
64+
const is_macos = target.result.os.tag == .macos;
65+
if (is_macos) fibers_ext.linker_allow_shlib_undefined = true;
66+
4167
// Enable full LTO for release builds to allow cross-module inlining between
4268
// the event loop, I/O helpers, and Ruby binding layers.
4369
// macOS excluded: Zig 0.15's linker can't resolve Xcode 26.4 TBD entries,
4470
// so we use DEVELOPER_DIR=/dev/null which bypasses the system SDK but also
4571
// prevents LLD (required for LTO) from linking against libSystem.
46-
const is_macos = target.result.os.tag == .macos;
4772
if (optimize != .Debug and !is_macos) fibers_ext.lto = .full;
4873

4974
const allocator = b.allocator;

0 commit comments

Comments
 (0)