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

@@ -1,6 +1,9 @@
const std = @import("std");
const builtin = @import("builtin");
/// Helpers for linking Mach-O artifacts with Apple's native toolchain.
pub const nativeLink = @import("native_link.zig");
// The cache. This always uses b.allocator and never frees memory
// (which is idiomatic for a Zig build exe). We cache the libc txt
// file we create because it is expensive to generate (subprocesses).

View File

@@ -0,0 +1,85 @@
//! Helpers for invoking Apple's native linker through the target SDK.
//!
//! Callers provide all artifact-specific flags, inputs, and outputs.
const std = @import("std");
const builtin = @import("builtin");
const RunStep = std.Build.Step.Run;
/// Returns true when the host Apple toolchain can link the target.
///
/// This is limited to targets covered by the SDK and target-triple mappings
/// below. Add other Darwin platforms alongside support for those mappings.
pub fn available(target: std.Build.ResolvedTarget) bool {
if (!builtin.os.tag.isDarwin()) return false;
return switch (target.result.os.tag) {
.macos, .ios => true,
else => false,
};
}
/// Create an Apple Clang link command configured for the target SDK and
/// deployment version. The caller supplies the artifact-specific link flags,
/// inputs, and output.
pub fn addCommand(
b: *std.Build,
name: []const u8,
target: std.Build.ResolvedTarget,
) !*RunStep {
if (!available(target)) return error.AppleNativeLinkerUnavailable;
const run = RunStep.create(b, name);
run.addArgs(&.{
"/usr/bin/xcrun",
"--sdk",
try sdkName(target),
"clang",
"-fuse-ld=ld",
"-target",
try clangTarget(b, target),
});
return run;
}
fn sdkName(target: std.Build.ResolvedTarget) ![]const u8 {
return switch (target.result.os.tag) {
.macos => "macosx",
.ios => switch (target.result.abi) {
.simulator => "iphonesimulator",
else => "iphoneos",
},
else => error.UnsupportedAppleTarget,
};
}
fn clangTarget(
b: *std.Build,
target: std.Build.ResolvedTarget,
) ![]const u8 {
const arch = switch (target.result.cpu.arch) {
.aarch64 => "arm64",
.x86_64 => "x86_64",
else => return error.UnsupportedAppleArchitecture,
};
const minimum = target.query.os_version_min orelse
return error.AppleDeploymentTargetMissing;
return switch (target.result.os.tag) {
.macos => b.fmt(
"{s}-apple-macosx{f}",
.{ arch, minimum.semver },
),
.ios => switch (target.result.abi) {
.simulator => b.fmt(
"{s}-apple-ios{f}-simulator",
.{ arch, minimum.semver },
),
else => b.fmt(
"{s}-apple-ios{f}",
.{ arch, minimum.semver },
),
},
else => error.UnsupportedAppleTarget,
};
}