build: link libghostty-vt on Apple hosts with native linker

Replaces: https://github.com/ghostty-org/ghostty/pull/13427

Zig's Mach-O linker does not emit LC_ENCRYPTION_INFO_64 for physical
iOS dylibs. This allows libghostty-vt to build successfully but causes
frameworks containing it to fail App Store validation.

I think it'd be cleaner to always just build Apple targets on Apple hosts
with the native linker. We don't need to rely on Zig being correct and 
this helps ensure compatibility for details like this.
This commit is contained in:
Mitchell Hashimoto
2026-07-23 06:38:43 -07:00
parent 30de782e8e
commit d65cb5128a
5 changed files with 375 additions and 1 deletions

View File

@@ -102,6 +102,16 @@ pub fn initShared(
b: *std.Build,
zig: *const GhosttyZig,
) !GhosttyLibVt {
const target = zig.vt.resolved_target.?;
// Prefer Apple's linker for final Darwin dylibs when its toolchain is
// available. Besides producing the platform load commands expected by
// Apple distribution, this lets the static archive's libSystem symbol
// override take effect. Cross-compiled macOS dylibs continue through Zig.
if (@import("apple_sdk").nativeLink.available(target)) {
return initLibApple(b, zig);
}
return initLib(b, zig, .dynamic);
}
@@ -340,6 +350,127 @@ fn initLib(
};
}
/// Builds a shared Darwin library with Apple's linker.
fn initLibApple(
b: *std.Build,
zig: *const GhosttyZig,
) !GhosttyLibVt {
// Sorry, this function has a lot of comments because there are
// a lot of flags that aren't obvious.
const target = zig.vt.resolved_target.?;
assert(target.result.os.tag.isDarwin());
// Build the combined archive through the normal static path first.
const static = try initStatic(b, zig);
// Zig's module-level export metadata does not carry into this separate
// native link, so explicitly expose only libghostty-vt's public C ABI.
const exports = b.addWriteFiles().add(
"libghostty-vt.exports",
"_ghostty_*\n",
);
// Match Zig's versioned dylib naming so existing consumers and packaging
// continue to receive the same real name and compatibility symlinks.
const real_name = b.fmt("libghostty-vt.{d}.{d}.{d}.dylib", .{
zig.version.major,
zig.version.minor,
zig.version.patch,
});
const soname = b.fmt("libghostty-vt.{d}.dylib", .{zig.version.major});
const native_link = try @import("apple_sdk").nativeLink.addCommand(
b,
"link libghostty-vt with Apple ld",
target,
);
native_link.addArgs(&.{
// Emit a Mach-O dynamic library instead of an executable.
"-dynamiclib",
// Pull every object from the combined static archive.
"-Wl,-all_load",
// Remove unreachable private code pulled in by all_load.
"-Wl,-dead_strip",
// Leave room for packaging tools to rewrite install names.
"-Wl,-headerpad_max_install_names",
});
// Restrict exports to libghostty-vt's public C ABI.
native_link.addPrefixedFileArg("-Wl,-exported_symbols_list,", exports);
// Link the archive produced by the normal static-library path.
native_link.addFileArg(static.output);
native_link.addArgs(&.{
// Give framework consumers a stable runtime-relative identity.
"-install_name",
"@rpath/libghostty-vt.dylib",
// Record the exact package version for runtime inspection.
"-current_version",
b.fmt("{d}.{d}.{d}", .{
zig.version.major,
zig.version.minor,
zig.version.patch,
}),
// Preserve compatibility with the initial public ABI.
"-compatibility_version",
"1.0.0",
// Write the fully versioned dylib used by the install symlinks.
"-o",
});
const output = native_link.addOutputFileArg(real_name);
// Recreate addInstallArtifact's dylib layout: install the fully versioned
// file, then point the major-version and unversioned names at it.
const artifact_install = b.addInstallFileWithDir(
output,
.lib,
real_name,
);
const soname_install = b.addSystemCommand(&.{
"/bin/ln",
"-sf",
real_name,
b.getInstallPath(.lib, soname),
});
soname_install.step.dependOn(&artifact_install.step);
const unversioned_install = b.addSystemCommand(&.{
"/bin/ln",
"-sf",
soname,
b.getInstallPath(.lib, "libghostty-vt.dylib"),
});
unversioned_install.step.dependOn(&soname_install.step);
// The native link is a Run step rather than a Compile step, so install the
// public headers explicitly instead of relying on addInstallArtifact.
const headers_install = b.addInstallDirectory(.{
.source_dir = b.path("include/ghostty"),
.install_dir = .header,
.install_subdir = "ghostty",
.include_extensions = &.{".h"},
});
unversioned_install.step.dependOn(&headers_install.step);
// Preserve the debug-symbol output exposed by the normal shared-library
// path for framework and release packaging.
const dsymutil = RunStep.create(b, "dsymutil");
dsymutil.addArgs(&.{"dsymutil"});
dsymutil.addFileArg(output);
dsymutil.addArgs(&.{"-o"});
const dsym = dsymutil.addOutputFileArg("libghostty-vt.dSYM");
const pcs = pkgConfigFiles(b, zig, target.result.os.tag);
return .{
.step = &native_link.step,
.artifact = &unversioned_install.step,
.kind = .shared,
.output = output,
.dsym = dsym,
.pkg_config = pcs.shared,
.pkg_config_static = pcs.static,
};
}
/// Returns the Libs.private value for the pkg-config file.
/// Vendored C++ dependencies are built in no-libcxx mode so consumers
/// don't need libc++. System-provided simdutf still requires it.