build: use a libc txt file to point to correct Apple SDK (#7469)

This fixes an issue where Ghostty would not build against the macOS 15.5
SDK.

What was happening was that Zig was adding its embedded libc paths to
the clang command line, which included old headers that were
incompatible with the latest (macOS 15.5) SDK. Ghostty was adding the
newer paths but they were being overridden by the embedded libc paths.

The reason this was happening is because Zig was using its own logic to
find the libc paths and this was colliding with the paths we were
setting manually. To fix this, we now use a `libc.txt` file that
explicitly tells Zig where to find libc, and we base this on our own SDK
search logic.
This commit is contained in:
Mitchell Hashimoto
2025-05-29 15:20:42 -07:00
committed by GitHub
19 changed files with 92 additions and 52 deletions

View File

@@ -7,12 +7,17 @@ pub fn build(b: *std.Build) !void {
_ = optimize;
}
/// Add the SDK framework, include, and library paths to the given module.
/// The module target is used to determine the SDK to use so it must have
/// a resolved target.
pub fn addPaths(b: *std.Build, m: *std.Build.Module) !void {
/// Setup the step to point to the proper Apple SDK for libc and
/// frameworks. This expects and relies on the native SDK being
/// installed on the system. Ghostty doesn't support cross-compilation
/// for Apple platforms.
pub fn addPaths(
b: *std.Build,
step: *std.Build.Step.Compile,
) !void {
// The cache. This always uses b.allocator and never frees memory
// (which is idiomatic for a Zig build exe).
// (which is idiomatic for a Zig build exe). We cache the libc txt
// file we create because it is expensive to generate (subprocesses).
const Cache = struct {
const Key = struct {
arch: std.Target.Cpu.Arch,
@@ -20,27 +25,72 @@ pub fn addPaths(b: *std.Build, m: *std.Build.Module) !void {
abi: std.Target.Abi,
};
var map: std.AutoHashMapUnmanaged(Key, ?[]const u8) = .{};
var map: std.AutoHashMapUnmanaged(Key, ?struct {
libc: std.Build.LazyPath,
framework: []const u8,
system_include: []const u8,
library: []const u8,
}) = .{};
};
const target = m.resolved_target.?.result;
const target = step.rootModuleTarget();
const gop = try Cache.map.getOrPut(b.allocator, .{
.arch = target.cpu.arch,
.os = target.os.tag,
.abi = target.abi,
});
// This executes `xcrun` to get the SDK path. We don't want to execute
// this multiple times so we cache the value.
if (!gop.found_existing) {
gop.value_ptr.* = std.zig.system.darwin.getSdk(
b.allocator,
m.resolved_target.?.result,
);
// Detect our SDK using the "findNative" Zig stdlib function.
// This is really important because it forces using `xcrun` to
// find the SDK path.
const libc = try std.zig.LibCInstallation.findNative(.{
.allocator = b.allocator,
.target = step.rootModuleTarget(),
.verbose = false,
});
// Render the file compatible with the `--libc` Zig flag.
var list: std.ArrayList(u8) = .init(b.allocator);
defer list.deinit();
try libc.render(list.writer());
// Create a temporary file to store the libc path because
// `--libc` expects a file path.
const wf = b.addWriteFiles();
const path = wf.add("libc.txt", list.items);
// Determine our framework path. Zig has a bug where it doesn't
// parse this from the libc txt file for `-framework` flags:
// https://github.com/ziglang/zig/issues/24024
const framework_path = framework: {
const down1 = std.fs.path.dirname(libc.sys_include_dir.?).?;
const down2 = std.fs.path.dirname(down1).?;
break :framework try std.fs.path.join(b.allocator, &.{
down2,
"System",
"Library",
"Frameworks",
});
};
const library_path = library: {
const down1 = std.fs.path.dirname(libc.sys_include_dir.?).?;
break :library try std.fs.path.join(b.allocator, &.{
down1,
"lib",
});
};
gop.value_ptr.* = .{
.libc = path,
.framework = framework_path,
.system_include = libc.sys_include_dir.?,
.library = library_path,
};
}
// The active SDK we want to use
const path = gop.value_ptr.* orelse return switch (target.os.tag) {
const value = gop.value_ptr.* orelse return switch (target.os.tag) {
// Return a more descriptive error. Before we just returned the
// generic error but this was confusing a lot of community members.
// It costs us nothing in the build script to return something better.
@@ -50,7 +100,12 @@ pub fn addPaths(b: *std.Build, m: *std.Build.Module) !void {
.watchos => error.XcodeWatchOSSDKNotFound,
else => error.XcodeAppleSDKNotFound,
};
m.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ path, "/System/Library/Frameworks" }) });
m.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ path, "/usr/include" }) });
m.addLibraryPath(.{ .cwd_relative = b.pathJoin(&.{ path, "/usr/lib" }) });
step.setLibCFile(value.libc);
// This is only necessary until this bug is fixed:
// https://github.com/ziglang/zig/issues/24024
step.root_module.addSystemFrameworkPath(.{ .cwd_relative = value.framework });
step.root_module.addSystemIncludePath(.{ .cwd_relative = value.system_include });
step.root_module.addLibraryPath(.{ .cwd_relative = value.library });
}

View File

@@ -13,7 +13,7 @@ pub fn build(b: *std.Build) !void {
lib.addIncludePath(b.path("vendor"));
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, lib);
}
var flags = std.ArrayList([]const u8).init(b.allocator);

View File

@@ -84,8 +84,7 @@ pub fn build(b: *std.Build) !void {
if (target.result.os.tag.isDarwin()) {
if (!target.query.isNative()) {
try @import("apple_sdk").addPaths(b, lib.root_module);
try @import("apple_sdk").addPaths(b, module);
try @import("apple_sdk").addPaths(b, lib);
}
lib.addCSourceFile(.{
.file = imgui.path("backends/imgui_impl_metal.mm"),

View File

@@ -69,7 +69,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu
lib.linkLibC();
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, lib);
}
var flags = std.ArrayList([]const u8).init(b.allocator);

View File

@@ -24,7 +24,7 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize,
});
if (target.result.os.tag.isDarwin()) {
try apple_sdk.addPaths(b, exe.root_module);
try apple_sdk.addPaths(b, exe);
}
const tests_run = b.addRunArtifact(exe);
@@ -122,8 +122,7 @@ fn buildLib(
},
.macos => {
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, module);
try apple_sdk.addPaths(b, lib);
// Transitive dependencies, explicit linkage of these works around
// ziglang/zig#17130

View File

@@ -16,10 +16,6 @@ pub fn build(b: *std.Build) !void {
module.addIncludePath(upstream.path(""));
module.addIncludePath(b.path("override"));
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, module);
}
if (target.query.isNative()) {
const test_exe = b.addTest(.{
@@ -55,7 +51,7 @@ fn buildGlslang(
lib.addIncludePath(b.path("override"));
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, lib);
}
var flags = std.ArrayList([]const u8).init(b.allocator);

View File

@@ -93,8 +93,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu
lib.linkLibCpp();
if (target.result.os.tag.isDarwin()) {
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, module);
try apple_sdk.addPaths(b, lib);
}
const dynamic_link_opts = options.dynamic_link_opts;

View File

@@ -23,8 +23,7 @@ pub fn build(b: *std.Build) !void {
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, module);
try apple_sdk.addPaths(b, lib);
}
var flags = std.ArrayList([]const u8).init(b.allocator);

View File

@@ -40,7 +40,7 @@ pub fn build(b: *std.Build) !void {
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, lib);
}
if (b.lazyDependency("gettext", .{})) |upstream| {

View File

@@ -15,7 +15,7 @@ pub fn build(b: *std.Build) !void {
}
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, lib);
}
// For dynamic linking, we prefer dynamic linking and to search by

View File

@@ -45,8 +45,7 @@ pub fn build(b: *std.Build) !void {
module.linkFramework("CoreVideo", .{});
module.linkFramework("QuartzCore", .{});
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, module);
try apple_sdk.addPaths(b, lib);
}
b.installArtifact(lib);
@@ -58,7 +57,7 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize,
});
if (target.result.os.tag.isDarwin()) {
try apple_sdk.addPaths(b, test_exe.root_module);
try apple_sdk.addPaths(b, test_exe);
}
test_exe.linkLibrary(lib);

View File

@@ -67,7 +67,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, lib);
}
if (b.lazyDependency("oniguruma", .{})) |upstream| {

View File

@@ -20,8 +20,7 @@ pub fn build(b: *std.Build) !void {
lib.linkLibC();
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, module);
try apple_sdk.addPaths(b, lib);
}
var flags = std.ArrayList([]const u8).init(b.allocator);

View File

@@ -14,7 +14,7 @@ pub fn build(b: *std.Build) !void {
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, lib);
}
var flags = std.ArrayList([]const u8).init(b.allocator);

View File

@@ -44,7 +44,7 @@ fn buildSpirvCross(
lib.linkLibCpp();
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, lib);
}
var flags = std.ArrayList([]const u8).init(b.allocator);

View File

@@ -13,7 +13,7 @@ pub fn build(b: *std.Build) !void {
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, lib);
}
var flags = std.ArrayList([]const u8).init(b.allocator);

View File

@@ -11,11 +11,6 @@ pub fn build(b: *std.Build) !void {
.link_libc = true,
});
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, module);
}
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,

View File

@@ -12,7 +12,7 @@ pub fn build(b: *std.Build) !void {
lib.linkLibC();
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib.root_module);
try apple_sdk.addPaths(b, lib);
}
if (b.lazyDependency("zlib", .{})) |upstream| {

View File

@@ -377,7 +377,7 @@ pub fn add(
// We always require the system SDK so that our system headers are available.
// This makes things like `os/log.h` available for cross-compiling.
if (step.rootModuleTarget().os.tag.isDarwin()) {
try @import("apple_sdk").addPaths(b, step.root_module);
try @import("apple_sdk").addPaths(b, step);
const metallib = self.metallib.?;
metallib.output.addStepDependencies(&step.step);