From 6b69ea05170435ee6abd79b9a3da7a2609d5aaa3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Apr 2026 10:40:32 -0700 Subject: [PATCH] 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. --- .github/workflows/test.yml | 10 +- pkg/apple-sdk/build.zig | 164 +++++++++++++++++++----------- pkg/highway/build.zig | 3 +- src/build/CombineArchivesStep.zig | 5 +- 4 files changed, 116 insertions(+), 66 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f805983c..3c74fe6aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/pkg/apple-sdk/build.zig b/pkg/apple-sdk/build.zig index c573c3910..a61cd3843 100644 --- a/pkg/apple-sdk/build.zig +++ b/pkg/apple-sdk/build.zig @@ -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); + }, + } } diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index 64c0e8869..0ac776123 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -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) { diff --git a/src/build/CombineArchivesStep.zig b/src/build/CombineArchivesStep.zig index 98e6b037c..cebd8e9fc 100644 --- a/src/build/CombineArchivesStep.zig +++ b/src/build/CombineArchivesStep.zig @@ -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}),