libghostty: enable cross-compiling macOS from Linux/Windows (#12417)

This allows libghostty-vt to be cross-compiled for macOS from non-macOS
platforms. I've updated pkg/apple-sdk to fallback to Zig's embedded
macOS headers if the macOS SDK is not found. Additionally,
CombineArchivesStep has been updated to use Linux tooling on Linux. CI
updated to test this.
pull/12425/head
Mitchell Hashimoto 2026-04-24 13:19:11 -07:00 committed by GitHub
commit 2ed382a155
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 116 additions and 66 deletions

View File

@ -569,14 +569,15 @@ jobs:
build-libghostty-vt:
strategy:
matrix:
target:
[
target: [
aarch64-macos,
x86_64-macos,
aarch64-linux,
x86_64-linux,
x86_64-linux-musl,
x86_64-windows,
x86_64-windows-gnu,
# doesn't work yet, we need a way to find msvc libc/c++ headers
# x86_64-windows-msvc
wasm32-freestanding,
]
runs-on: namespace-profile-ghostty-sm
@ -607,8 +608,7 @@ jobs:
- name: Build
run: |
nix develop -c zig build -Demit-lib-vt \
-Dtarget=${{ matrix.target }} \
-Dsimd=false
-Dtarget=${{ matrix.target }}
# lib-vt requires macOS runner for macOS/iOS builds because it requires the `apple_sdk` path
build-libghostty-vt-macos:

View File

@ -1,4 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
@ -8,9 +9,9 @@ pub fn build(b: *std.Build) !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.
/// frameworks. When running on a Darwin host, this uses the native
/// SDK installed on the system via `xcrun`. When cross-compiling from
/// a non-Darwin host, it falls back to Zig's bundled Darwin headers.
pub fn addPaths(
b: *std.Build,
step: *std.Build.Step.Compile,
@ -25,12 +26,19 @@ pub fn addPaths(
abi: std.Target.Abi,
};
var map: std.AutoHashMapUnmanaged(Key, ?struct {
libc: std.Build.LazyPath,
framework: []const u8,
system_include: []const u8,
library: []const u8,
}) = .{};
const Value = union(enum) {
native: struct {
libc: std.Build.LazyPath,
framework: []const u8,
system_include: []const u8,
library: []const u8,
},
cross: struct {
libc: std.Build.LazyPath,
},
};
var map: std.AutoHashMapUnmanaged(Key, ?Value) = .{};
};
const target = step.rootModuleTarget();
@ -40,54 +48,85 @@ pub fn addPaths(
.abi = target.abi,
});
if (!gop.found_existing) {
// 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,
if (!gop.found_existing) init: {
if (comptime builtin.os.tag.isDarwin()) {
// 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 stream: std.io.Writer.Allocating = .init(b.allocator);
defer stream.deinit();
try libc.render(&stream.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", stream.written());
// 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.* = .{ .native = .{
.libc = path,
.framework = framework_path,
.system_include = libc.sys_include_dir.?,
.library = library_path,
} };
break :init;
}
// Cross-compiling to Darwin from a non-Darwin host.
// Zig only bundles macOS headers, so for other Apple platforms
// we leave the value as null to produce a descriptive error.
if (target.os.tag != .macos) {
gop.value_ptr.* = null;
break :init;
}
// Fall back to Zig's bundled Darwin headers for libc resolution.
const zig_lib_path = b.graph.zig_lib_directory.path.?;
const include_dir = b.pathJoin(&.{
zig_lib_path, "libc", "include", "any-macos-any",
});
// Render the file compatible with the `--libc` Zig flag.
var stream: std.io.Writer.Allocating = .init(b.allocator);
defer stream.deinit();
try libc.render(&stream.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", stream.written());
const path = wf.add("libc.txt", b.fmt(
\\include_dir={s}
\\sys_include_dir={s}
\\crt_dir=
\\msvc_lib_dir=
\\kernel32_lib_dir=
\\gcc_dir=
\\
, .{ include_dir, include_dir }));
// 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,
};
gop.value_ptr.* = .{ .cross = .{ .libc = path } };
}
const value = gop.value_ptr.* orelse return switch (target.os.tag) {
@ -101,11 +140,18 @@ pub fn addPaths(
else => error.XcodeAppleSDKNotFound,
};
step.setLibCFile(value.libc);
switch (value) {
.native => |native| {
step.setLibCFile(native.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 });
// This is only necessary until this bug is fixed:
// https://github.com/ziglang/zig/issues/24024
step.root_module.addSystemFrameworkPath(.{ .cwd_relative = native.framework });
step.root_module.addSystemIncludePath(.{ .cwd_relative = native.system_include });
step.root_module.addLibraryPath(.{ .cwd_relative = native.library });
},
.cross => |cross| {
step.setLibCFile(cross.libc);
},
}
}

View File

@ -83,8 +83,9 @@ pub fn build(b: *std.Build) !void {
"-fno-sanitize-trap=undefined",
});
if (target.result.os.tag == .freebsd or target.result.abi == .musl) {
if (target.result.os.tag == .freebsd or target.result.os.tag == .linux) {
try flags.append(b.allocator, "-fPIC");
lib.root_module.pic = true;
}
if (target.result.os.tag != .windows) {

View File

@ -2,6 +2,7 @@
//! Uses libtool on Darwin and a cross-platform MRI-script build tool
//! on all other platforms (including Windows).
const std = @import("std");
const builtin = @import("builtin");
const LibtoolStep = @import("LibtoolStep.zig");
/// Combine multiple static archives into a single fat archive.
@ -15,7 +16,9 @@ pub fn create(
name: []const u8,
sources: []const std.Build.LazyPath,
) struct { step: *std.Build.Step, output: std.Build.LazyPath } {
if (target.result.os.tag.isDarwin()) {
if (target.result.os.tag.isDarwin() and
comptime builtin.os.tag.isDarwin())
{
const libtool = LibtoolStep.create(b, .{
.name = name,
.out_name = b.fmt("lib{s}-fat.a", .{name}),