Skip to content

Commit c547c55

Browse files
authored
Merge pull request #9 from 10d9e/fix/patch-rpath-bug-in-macos
fix(build): Implement workaround to remove duplicate rpaths on macOS
2 parents 7311c4e + 0a4a4b4 commit c547c55

File tree

2 files changed

+120
-7
lines changed

2 files changed

+120
-7
lines changed

Makefile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,18 @@ install-vcpkg-deps:
386386
# Build the project
387387
build: check-deps
388388
@echo "$(BLUE)Building ZEVM...$(NC)"
389-
@echo "$(YELLOW)Build command: $(ZIG_BUILD_CMD)$(NC)"
390-
@$(ZIG_BUILD_CMD)
389+
@echo "$(YELLOW)Build command: $(ZIG_BUILD_CMD) install$(NC)"
390+
@$(ZIG_BUILD_CMD) install
391+
@if [ "$(UNAME_S)" = "Darwin" ]; then \
392+
echo "$(BLUE)Cleaning up duplicate RPATHs in installed binaries...$(NC)"; \
393+
for bin in zig-out/bin/zevm-test zig-out/bin/zevm-bench zig-out/bin/zevm-example; do \
394+
if [ -f "$$bin" ]; then \
395+
install_name_tool -delete_rpath "/opt/homebrew/Cellar/openssl@3/3.6.0/lib" "$$bin" 2>/dev/null || true; \
396+
install_name_tool -delete_rpath "/opt/homebrew/Cellar/openssl@3/3.6.0/lib" "$$bin" 2>/dev/null || true; \
397+
otool -l "$$bin" | grep -q "path /opt/homebrew/Cellar/openssl@3/3.6.0/lib" || install_name_tool -add_rpath "/opt/homebrew/Cellar/openssl@3/3.6.0/lib" "$$bin" 2>/dev/null || true; \
398+
fi; \
399+
done; \
400+
fi
391401
@echo "$(GREEN)✓ Build complete!$(NC)"
392402

393403
# Build with dependency installation

build.zig

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,105 @@ pub fn build(b: *std.Build) void {
3434
lib_options.addOption(bool, "enable_mcl", enable_mcl);
3535
const lib_options_module = lib_options.createModule();
3636

37+
// Helper function to remove duplicate rpaths on macOS
38+
//
39+
// ROOT CAUSE:
40+
// Zig's build system automatically adds an LC_RPATH entry for each library linked via
41+
// linkSystemLibrary(). When multiple libraries are in the same directory (e.g., libssl.3.dylib
42+
// and libcrypto.3.dylib both in /opt/homebrew/Cellar/openssl@3/3.6.0/lib), Zig adds the same
43+
// rpath multiple times, causing duplicate LC_RPATH entries that dyld rejects.
44+
//
45+
// This is a known Zig issue: https://github.com/ziglang/zig/issues/24349
46+
// System libraries shouldn't need rpaths at all since they're in standard search paths.
47+
//
48+
// WORKAROUND:
49+
// We remove duplicate rpaths using install_name_tool. Since Zig's addFileArg doesn't work
50+
// reliably with shell scripts, we use individual install_name_tool commands. We remove the
51+
// duplicate rpath twice (to handle the common case of 2 duplicates) and add it back once.
52+
// This is a temporary fix until Zig addresses the root cause upstream.
53+
//
54+
// NOTE: This hardcodes the OpenSSL rpath path. For non-Homebrew installations or different
55+
// OpenSSL versions, you may need to adjust the path or add additional rpath cleanup steps.
56+
//
57+
// TODO: Remove this workaround when Zig addresses the root cause upstream in v0.16.0+
58+
const removeDuplicateRpaths = struct {
59+
fn remove(
60+
b_ctx: *std.Build,
61+
exe: *std.Build.Step.Compile,
62+
run_step: ?*std.Build.Step.Run,
63+
) void {
64+
// Output warning about workaround in yellow
65+
std.debug.print("\x1b[33mWarning: Removing duplicate rpaths as a workaround for a known Zig issue (https://github.com/ziglang/zig/issues/24349). This will be removed when Zig addresses the root cause upstream in v0.16.0+\x1b[0m\n", .{});
66+
67+
const exe_target = exe.root_module.resolved_target orelse return;
68+
if (exe_target.result.os.tag != .macos) return;
69+
70+
const bin_file = exe.getEmittedBin();
71+
// Common OpenSSL rpath for Homebrew installations
72+
// Adjust this if your OpenSSL is installed elsewhere
73+
const rpath = "/opt/homebrew/Cellar/openssl@3/3.6.0/lib";
74+
75+
// Remove first instance (ignore errors if it doesn't exist)
76+
// Use sh -c to wrap the command so we can ignore errors with || true
77+
// Redirect stderr to /dev/null to suppress error messages
78+
// Add a dummy arg so the file path becomes $1 (first arg after -c script becomes $0)
79+
const remove1_cmd = std.fmt.allocPrint(b_ctx.allocator, "install_name_tool -delete_rpath '{s}' \"$1\" 2>/dev/null || true", .{rpath}) catch @panic("OOM");
80+
const remove1 = b_ctx.addSystemCommand(&.{ "sh", "-c", remove1_cmd, "dummy" });
81+
remove1.addFileArg(bin_file);
82+
remove1.step.dependOn(&exe.step);
83+
84+
// Remove second instance (ignore errors if it doesn't exist)
85+
const remove2_cmd = std.fmt.allocPrint(b_ctx.allocator, "install_name_tool -delete_rpath '{s}' \"$1\" 2>/dev/null || true", .{rpath}) catch @panic("OOM");
86+
const remove2 = b_ctx.addSystemCommand(&.{ "sh", "-c", remove2_cmd, "dummy" });
87+
remove2.addFileArg(bin_file);
88+
remove2.step.dependOn(&remove1.step);
89+
90+
// Add it back once (only if it doesn't already exist after removal)
91+
// This ensures we have exactly one rpath if any existed before
92+
const add_back_cmd = std.fmt.allocPrint(b_ctx.allocator, "otool -l \"$1\" | grep -q \"path {s}\" || (install_name_tool -add_rpath '{s}' \"$1\" 2>/dev/null || true)", .{ rpath, rpath }) catch @panic("OOM");
93+
const add_back = b_ctx.addSystemCommand(&.{ "sh", "-c", add_back_cmd, "dummy" });
94+
add_back.addFileArg(bin_file);
95+
add_back.step.dependOn(&remove2.step);
96+
97+
// Make the run step depend on cleaning rpaths so it runs before execution
98+
if (run_step) |run| {
99+
run.step.dependOn(&add_back.step);
100+
}
101+
102+
// Also add to install step so installed binaries are clean
103+
// Since we clean the build binary before install, the installed copy should be clean
104+
// But we also clean the installed binary after installation to be safe
105+
const install_step = b_ctx.getInstallStep();
106+
install_step.dependOn(&add_back.step);
107+
108+
// Also clean the installed binary after it's copied
109+
// Get the installed binary path - use absolute path from build root
110+
const exe_name = exe.name;
111+
// Construct absolute path: build_root/zig-out/bin/exe_name
112+
const installed_bin_path = std.fmt.allocPrint(b_ctx.allocator, "zig-out/bin/{s}", .{exe_name}) catch @panic("OOM");
113+
114+
const install_remove1_cmd = std.fmt.allocPrint(b_ctx.allocator, "install_name_tool -delete_rpath '{s}' \"$1\" 2>/dev/null || true", .{rpath}) catch @panic("OOM");
115+
const install_remove1 = b_ctx.addSystemCommand(&.{ "sh", "-c", install_remove1_cmd, "dummy" });
116+
install_remove1.addArg(installed_bin_path);
117+
install_remove1.step.dependOn(install_step);
118+
119+
const install_remove2_cmd = std.fmt.allocPrint(b_ctx.allocator, "install_name_tool -delete_rpath '{s}' \"$1\" 2>/dev/null || true", .{rpath}) catch @panic("OOM");
120+
const install_remove2 = b_ctx.addSystemCommand(&.{ "sh", "-c", install_remove2_cmd, "dummy" });
121+
install_remove2.addArg(installed_bin_path);
122+
install_remove2.step.dependOn(&install_remove1.step);
123+
124+
const install_add_back_cmd = std.fmt.allocPrint(b_ctx.allocator, "otool -l \"$1\" | grep -q \"path {s}\" || (install_name_tool -add_rpath '{s}' \"$1\" 2>/dev/null || true)", .{ rpath, rpath }) catch @panic("OOM");
125+
const install_add_back = b_ctx.addSystemCommand(&.{ "sh", "-c", install_add_back_cmd, "dummy" });
126+
install_add_back.addArg(installed_bin_path);
127+
install_add_back.step.dependOn(&install_remove2.step);
128+
129+
// Make run step also depend on installed binary cleanup
130+
if (run_step) |run| {
131+
run.step.dependOn(&install_add_back.step);
132+
}
133+
}
134+
}.remove;
135+
37136
// Helper function to add crypto library linking to a step
38137
const addCryptoLibraries = struct {
39138
fn add(
@@ -297,8 +396,17 @@ pub fn build(b: *std.Build) void {
297396
test_exe.root_module.addImport("handler", handler_module);
298397
test_exe.root_module.addImport("inspector", inspector_module);
299398

399+
// Run tests
400+
const run_tests = b.addRunArtifact(test_exe);
401+
402+
// Remove duplicate rpaths on macOS before installation and running tests
403+
removeDuplicateRpaths(b, test_exe, run_tests);
404+
300405
b.installArtifact(test_exe);
301406

407+
const test_step = b.step("test", "Run unit tests");
408+
test_step.dependOn(&run_tests.step);
409+
302410
// Benchmark executable
303411
const bench_exe = b.addExecutable(.{
304412
.name = "zevm-bench",
@@ -323,11 +431,6 @@ pub fn build(b: *std.Build) void {
323431

324432
b.installArtifact(bench_exe);
325433

326-
// Run tests
327-
const run_tests = b.addRunArtifact(test_exe);
328-
const test_step = b.step("test", "Run unit tests");
329-
test_step.dependOn(&run_tests.step);
330-
331434
// Inline zig tests for interpreter module (discovers tests in all imported files)
332435
const interpreter_tests = b.addTest(.{
333436
.root_module = b.createModule(.{

0 commit comments

Comments
 (0)