libghostty: enable cross-compiling macOS from Linux/Windows

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.
pull/12417/head
Mitchell Hashimoto 2026-04-24 10:40:32 -07:00
parent 48ccec182a
commit 6b69ea0517
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
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}),