From 05fb57dd4044dbd44f5b751afaa0beafea9df4bb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 14:02:06 -0700 Subject: [PATCH 1/8] build: emit xcframework for libghostty-vt on macOS On Darwin targets, the build now automatically produces a universal (arm64 + x86_64) XCFramework at lib/ghostty-vt.xcframework under the install prefix. This bundles the fat static library with headers so consumers using Xcode or Swift PM can link libghostty-vt directly. --- build.zig | 12 ++++++++ src/build/GhosttyLibVt.zig | 59 ++++++++++++++++++++++++++++++++++++++ src/build/GhosttyZig.zig | 42 +++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 78977a8c9..d8e8bd86a 100644 --- a/build.zig +++ b/build.zig @@ -152,6 +152,18 @@ pub fn build(b: *std.Build) !void { ).step); } + // libghostty-vt xcframework (Apple only, universal binary) + if (config.target.result.os.tag.isDarwin()) { + const universal = try buildpkg.GhosttyLibVt.initStaticAppleUniversal( + b, + &config, + &deps, + &mod, + ); + const xcframework = universal.xcframework(); + b.getInstallStep().dependOn(xcframework.step); + } + // Helpgen if (config.emit_helpgen) deps.help_strings.install(); diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index d5e76b9de..55f22e232 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -4,9 +4,12 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const RunStep = std.Build.Step.Run; +const Config = @import("Config.zig"); const GhosttyZig = @import("GhosttyZig.zig"); const LibtoolStep = @import("LibtoolStep.zig"); +const LipoStep = @import("LipoStep.zig"); const SharedDeps = @import("SharedDeps.zig"); +const XCFrameworkStep = @import("XCFrameworkStep.zig"); /// The step that generates the file. step: *std.Build.Step, @@ -99,6 +102,45 @@ pub fn initShared( return initLib(b, zig, .dynamic); } +/// Build a macOS universal (arm64 + x86_64) static library using lipo. +pub fn initStaticAppleUniversal( + b: *std.Build, + cfg: *const Config, + deps: *const SharedDeps, + zig: *const GhosttyZig, +) !GhosttyLibVt { + const aarch64_zig = try zig.retarget( + b, + cfg, + deps, + Config.genericMacOSTarget(b, .aarch64), + ); + const x86_64_zig = try zig.retarget( + b, + cfg, + deps, + Config.genericMacOSTarget(b, .x86_64), + ); + + const aarch64 = try initStatic(b, &aarch64_zig); + const x86_64 = try initStatic(b, &x86_64_zig); + const universal = LipoStep.create(b, .{ + .name = "ghostty-vt", + .out_name = "libghostty-vt.a", + .input_a = aarch64.output, + .input_b = x86_64.output, + }); + + return .{ + .step = universal.step, + .artifact = universal.step, + .kind = .static, + .output = universal.output, + .dsym = null, + .pkg_config = null, + }; +} + fn initLib( b: *std.Build, zig: *const GhosttyZig, @@ -294,6 +336,23 @@ fn requiresPrivate(b: *std.Build) []const u8 { return ""; } +/// Create an XCFramework bundle from the static library. +pub fn xcframework( + lib_vt: *const GhosttyLibVt, +) *XCFrameworkStep { + assert(lib_vt.kind == .static); + const b = lib_vt.step.owner; + return XCFrameworkStep.create(b, .{ + .name = "ghostty-vt", + .out_path = b.pathJoin(&.{ b.install_prefix, "lib/ghostty-vt.xcframework" }), + .libraries = &.{.{ + .library = lib_vt.output, + .headers = b.path("include/ghostty"), + .dsym = null, + }}, + }); +} + pub fn install( self: *const GhosttyLibVt, step: *std.Build.Step, diff --git a/src/build/GhosttyZig.zig b/src/build/GhosttyZig.zig index 3f3db95c6..8d5b78fb4 100644 --- a/src/build/GhosttyZig.zig +++ b/src/build/GhosttyZig.zig @@ -24,6 +24,44 @@ pub fn init( b: *std.Build, cfg: *const Config, deps: *const SharedDeps, +) !GhosttyZig { + return initInner(b, cfg, deps, "ghostty-vt", "ghostty-vt-c"); +} + +/// Create a new GhosttyZig with modules retargeted to a different +/// architecture. Used to produce universal (fat) binaries on macOS. +pub fn retarget( + self: *const GhosttyZig, + b: *std.Build, + cfg: *const Config, + deps: *const SharedDeps, + target: std.Build.ResolvedTarget, +) !GhosttyZig { + _ = self; + const retargeted_config = try b.allocator.create(Config); + retargeted_config.* = cfg.*; + retargeted_config.target = target; + + const retargeted_deps = try b.allocator.create(SharedDeps); + retargeted_deps.* = try deps.retarget(b, target); + + // Use unique module names to avoid collisions with the original target. + const arch_name = @tagName(target.result.cpu.arch); + return initInner( + b, + retargeted_config, + retargeted_deps, + b.fmt("ghostty-vt-{s}", .{arch_name}), + b.fmt("ghostty-vt-c-{s}", .{arch_name}), + ); +} + +fn initInner( + b: *std.Build, + cfg: *const Config, + deps: *const SharedDeps, + vt_name: []const u8, + vt_c_name: []const u8, ) !GhosttyZig { // Terminal module build options var vt_options = cfg.terminalOptions(.lib); @@ -37,7 +75,7 @@ pub fn init( return .{ .vt = try initVt( - "ghostty-vt", + vt_name, b, cfg, deps, @@ -46,7 +84,7 @@ pub fn init( ), .vt_c = try initVt( - "ghostty-vt-c", + vt_c_name, b, cfg, deps, From f567f7f46d0b60da3fddb18070e74b1c0deb074f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 14:13:20 -0700 Subject: [PATCH 2/8] build: add GhosttyVt module map to xcframework and Swift example The xcframework now generates its own headers directory with a GhosttyVt module map instead of reusing include/ directly, which contains the GhosttyKit module map for the macOS app. The generated directory copies the ghostty headers and adds a module.modulemap that exposes ghostty/vt.h as the umbrella header. A new swift-vt-xcframework example demonstrates consuming the xcframework from a Swift Package. It creates a terminal, writes VT sequences, and formats the output as plain text, verifying the full round-trip works with swift build and swift run. --- example/.gitignore | 1 + example/swift-vt-xcframework/Package.swift | 21 +++++++++ example/swift-vt-xcframework/README.md | 23 ++++++++++ .../swift-vt-xcframework/Sources/main.swift | 45 +++++++++++++++++++ src/build/GhosttyLibVt.zig | 20 ++++++++- 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 example/swift-vt-xcframework/Package.swift create mode 100644 example/swift-vt-xcframework/README.md create mode 100644 example/swift-vt-xcframework/Sources/main.swift diff --git a/example/.gitignore b/example/.gitignore index 6f372bc4d..9f88ccfeb 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -3,3 +3,4 @@ dist/ node_modules/ example.wasm* build/ +.build/ diff --git a/example/swift-vt-xcframework/Package.swift b/example/swift-vt-xcframework/Package.swift new file mode 100644 index 000000000..a831a42c8 --- /dev/null +++ b/example/swift-vt-xcframework/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "swift-vt-xcframework", + platforms: [.macOS(.v13)], + targets: [ + .executableTarget( + name: "swift-vt-xcframework", + dependencies: ["GhosttyVt"], + path: "Sources", + linkerSettings: [ + .linkedLibrary("c++"), + ] + ), + .binaryTarget( + name: "GhosttyVt", + path: "../../zig-out/lib/ghostty-vt.xcframework" + ), + ] +) diff --git a/example/swift-vt-xcframework/README.md b/example/swift-vt-xcframework/README.md new file mode 100644 index 000000000..3bbe8948c --- /dev/null +++ b/example/swift-vt-xcframework/README.md @@ -0,0 +1,23 @@ +# swift-vt-xcframework + +Demonstrates consuming libghostty-vt from a Swift Package using the +pre-built XCFramework. Creates a terminal, writes VT sequences into it, +and formats the screen contents as plain text. + +This example requires the XCFramework to be built first. + +## Building + +First, build the XCFramework from the repository root: + +```shell-session +zig build -Demit-lib-vt +``` + +Then build and run the Swift package: + +```shell-session +cd example/swift-vt-xcframework +swift build +swift run +``` diff --git a/example/swift-vt-xcframework/Sources/main.swift b/example/swift-vt-xcframework/Sources/main.swift new file mode 100644 index 000000000..e7d15fb89 --- /dev/null +++ b/example/swift-vt-xcframework/Sources/main.swift @@ -0,0 +1,45 @@ +import GhosttyVt + +// Create a terminal with a small grid +var terminal: GhosttyTerminal? +var opts = GhosttyTerminalOptions( + cols: 80, + rows: 24, + max_scrollback: 0 +) +let result = ghostty_terminal_new(nil, &terminal, opts) +guard result == GHOSTTY_SUCCESS, let terminal else { + fatalError("Failed to create terminal") +} + +// Write some VT-encoded content +let text = "Hello from \u{1b}[1mSwift\u{1b}[0m via xcframework!\r\n" +text.withCString { ptr in + ghostty_terminal_vt_write(terminal, ptr, strlen(ptr)) +} + +// Format the terminal contents as plain text +var fmtOpts = GhosttyFormatterTerminalOptions() +fmtOpts.size = MemoryLayout.size +fmtOpts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN +fmtOpts.trim = true + +var formatter: GhosttyFormatter? +let fmtResult = ghostty_formatter_terminal_new(nil, &formatter, terminal, fmtOpts) +guard fmtResult == GHOSTTY_SUCCESS, let formatter else { + fatalError("Failed to create formatter") +} + +var buf: UnsafeMutablePointer? +var len: Int = 0 +let allocResult = ghostty_formatter_format_alloc(formatter, nil, &buf, &len) +guard allocResult == GHOSTTY_SUCCESS, let buf else { + fatalError("Failed to format") +} + +print("Plain text (\(len) bytes):") +print(String(cString: buf)) + +ghostty_free(nil, buf, len) +ghostty_formatter_free(formatter) +ghostty_terminal_free(terminal) diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 55f22e232..904954650 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -342,12 +342,30 @@ pub fn xcframework( ) *XCFrameworkStep { assert(lib_vt.kind == .static); const b = lib_vt.step.owner; + + // Generate a headers directory with a module map for Swift PM. + // We can't use include/ directly because it contains a module map + // for GhosttyKit (the macOS app library). + const wf = b.addWriteFiles(); + _ = wf.addCopyDirectory( + b.path("include/ghostty"), + "ghostty", + .{ .include_extensions = &.{".h"} }, + ); + _ = wf.add("module.modulemap", + \\module GhosttyVt { + \\ umbrella header "ghostty/vt.h" + \\ export * + \\} + \\ + ); + return XCFrameworkStep.create(b, .{ .name = "ghostty-vt", .out_path = b.pathJoin(&.{ b.install_prefix, "lib/ghostty-vt.xcframework" }), .libraries = &.{.{ .library = lib_vt.output, - .headers = b.path("include/ghostty"), + .headers = wf.getDirectory(), .dsym = null, }}, }); From 764ff18b8edef150c7736d16a82a3f4e557fc374 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 14:16:45 -0700 Subject: [PATCH 3/8] ci: add Swift example builds on macOS Auto-discover Swift examples via example/*/Package.swift alongside the existing zig and cmake discovery. The new build-examples-swift job runs on macOS, builds the xcframework with zig build -Demit-lib-vt, then runs swift run in each example directory to verify the xcframework links and functions correctly end-to-end. --- .github/workflows/test.yml | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7c40ad3e8..a3e6639d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,6 +89,7 @@ jobs: - build-examples-zig - build-examples-cmake - build-examples-cmake-windows + - build-examples-swift - build-cmake - build-flatpak - build-libghostty-vt @@ -181,6 +182,7 @@ jobs: outputs: zig: ${{ steps.list.outputs.zig }} cmake: ${{ steps.list.outputs.cmake }} + swift: ${{ steps.list.outputs.swift }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -193,6 +195,9 @@ jobs: cmake=$(ls example/*/CMakeLists.txt 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))') echo "$cmake" | jq . echo "cmake=$cmake" >> "$GITHUB_OUTPUT" + swift=$(ls example/*/Package.swift 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))') + echo "$swift" | jq . + echo "swift=$swift" >> "$GITHUB_OUTPUT" build-examples-zig: strategy: @@ -290,6 +295,49 @@ jobs: cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=${{ github.workspace }} cmake --build build + build-examples-swift: + strategy: + fail-fast: false + matrix: + dir: ${{ fromJSON(needs.list-examples.outputs.swift) }} + name: Example ${{ matrix.dir }} + runs-on: namespace-profile-ghostty-macos-tahoe + needs: [test, list-examples] + env: + ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 + - uses: DeterminateSystems/nix-installer-action@main + with: + determinate: true + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Xcode Select + run: sudo xcode-select -s /Applications/Xcode_26.3.app + + - name: Build XCFramework + run: nix develop -c zig build -Demit-lib-vt + + - name: Build and Run Example + run: | + cd example/${{ matrix.dir }} + swift run + build-cmake: runs-on: namespace-profile-ghostty-sm needs: test From 90b706b97703bcec3dab6c2285acf86685e5fdfb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 14:22:27 -0700 Subject: [PATCH 4/8] ci: publish lib-vt xcframework in tip releases Add a build-lib-vt-xcframework job to the release-tip workflow that builds the universal xcframework with ReleaseFast, zips it, signs it with minisign, and uploads it to both the GitHub Release and R2 blob storage. Consumers can pull the xcframework zip from the tip release or by commit hash from tip.files.ghostty.org. --- .github/workflows/release-tip.yml | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 28e56e7fb..bd6002607 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -260,6 +260,91 @@ jobs: libghostty-vt-source.tar.gz.minisig token: ${{ secrets.GH_RELEASE_TOKEN }} + build-lib-vt-xcframework: + needs: [setup] + if: | + needs.setup.outputs.should_skip != 'true' && + ( + github.event_name == 'workflow_dispatch' || + ( + github.repository_owner == 'ghostty-org' && + github.ref_name == 'main' + ) + ) + runs-on: namespace-profile-ghostty-macos-tahoe + env: + GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} + ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 + - uses: DeterminateSystems/nix-installer-action@main + with: + determinate: true + - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Xcode Select + run: sudo xcode-select -s /Applications/Xcode_26.3.app + + - name: Build XCFramework + run: nix develop -c zig build -Demit-lib-vt -Doptimize=ReleaseFast + + - name: Zip XCFramework + run: | + cd zig-out/lib + zip -9 -r ../../ghostty-vt.xcframework.zip ghostty-vt.xcframework + + - name: Sign XCFramework + run: | + echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key + echo -n "${{ secrets.MINISIGN_PASSWORD }}" > minisign.password + nix develop -c minisign -S -m ghostty-vt.xcframework.zip -s minisign.key < minisign.password + + - name: Update Release + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + with: + name: 'Ghostty Tip ("Nightly")' + prerelease: true + tag_name: tip + target_commitish: ${{ github.sha }} + files: | + ghostty-vt.xcframework.zip + ghostty-vt.xcframework.zip.minisig + token: ${{ secrets.GH_RELEASE_TOKEN }} + + - name: Prep R2 Storage + run: | + mkdir -p blob/${GHOSTTY_COMMIT_LONG} + cp ghostty-vt.xcframework.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-vt.xcframework.zip + - name: Upload to R2 + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 + with: + r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} + r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }} + r2-secret-access-key: ${{ secrets.CF_R2_TIP_SECRET_KEY }} + r2-bucket: ghostty-tip + source-dir: blob + destination-dir: ./ + + - name: Echo Release URLs + run: | + echo "Release URLs:" + echo " XCFramework: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-vt.xcframework.zip" + build-macos: needs: [setup] if: | From e1a0e40ec4cfd7ae6ed7d99b92db733cea95a2c0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 14:24:53 -0700 Subject: [PATCH 5/8] build: skip xcframework when cross-compiling Gate the xcframework build on the host being macOS in addition to the target, since xcodebuild is only available on macOS. --- build.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index d8e8bd86a..074a6675e 100644 --- a/build.zig +++ b/build.zig @@ -152,8 +152,10 @@ pub fn build(b: *std.Build) !void { ).step); } - // libghostty-vt xcframework (Apple only, universal binary) - if (config.target.result.os.tag.isDarwin()) { + // libghostty-vt xcframework (Apple only, universal binary). + // Only when building on macOS (not cross-compiling) since + // xcodebuild is required. + if (builtin.os.tag.isDarwin() and config.target.result.os.tag.isDarwin()) { const universal = try buildpkg.GhosttyLibVt.initStaticAppleUniversal( b, &config, From 9b281cde4324f9a4c993c6776829fb64ce601f77 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 14:31:18 -0700 Subject: [PATCH 6/8] build: add iOS slices to lib-vt xcframework Add iOS device and simulator slices to the xcframework, gated on SDK availability via std.zig.LibCInstallation.findNative. Refactor AppleLibs from a struct with named fields to an EnumMap keyed by ApplePlatform so that adding new platforms only requires extending the enum and its sdk_platforms table. tvOS, watchOS, and visionOS are listed as not yet supported due to Zig stdlib limitations (missing PATH_MAX, mcontext fields). --- build.zig | 4 +- src/build/GhosttyLibVt.zig | 98 +++++++++++++++++++++++++++++++------- 2 files changed, 84 insertions(+), 18 deletions(-) diff --git a/build.zig b/build.zig index 074a6675e..e8c784611 100644 --- a/build.zig +++ b/build.zig @@ -156,13 +156,13 @@ pub fn build(b: *std.Build) !void { // Only when building on macOS (not cross-compiling) since // xcodebuild is required. if (builtin.os.tag.isDarwin() and config.target.result.os.tag.isDarwin()) { - const universal = try buildpkg.GhosttyLibVt.initStaticAppleUniversal( + const apple_libs = try buildpkg.GhosttyLibVt.initStaticAppleUniversal( b, &config, &deps, &mod, ); - const xcframework = universal.xcframework(); + const xcframework = buildpkg.GhosttyLibVt.xcframework(&apple_libs, b); b.getInstallStep().dependOn(xcframework.step); } diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 904954650..e3e6cf8c1 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -102,13 +102,39 @@ pub fn initShared( return initLib(b, zig, .dynamic); } -/// Build a macOS universal (arm64 + x86_64) static library using lipo. +/// Apple platform targets for xcframework slices. +pub const ApplePlatform = enum { + macos_universal, + ios, + ios_simulator, + // tvOS, watchOS, and visionOS are not yet supported by Zig's + // standard library (missing PATH_MAX, mcontext fields, etc.). + + /// Platforms that have device + simulator pairs, gated on SDK detection. + const sdk_platforms = [_]struct { + os_tag: std.Target.Os.Tag, + device: ApplePlatform, + simulator: ApplePlatform, + }{ + .{ .os_tag = .ios, .device = .ios, .simulator = .ios_simulator }, + }; +}; + +/// Static libraries for each Apple platform, keyed by `ApplePlatform`. +pub const AppleLibs = std.EnumMap(ApplePlatform, GhosttyLibVt); + +/// Build static libraries for all available Apple platforms. +/// Always builds a macOS universal (arm64 + x86_64) fat binary. +/// Additional platforms are included if their SDK is detected. pub fn initStaticAppleUniversal( b: *std.Build, cfg: *const Config, deps: *const SharedDeps, zig: *const GhosttyZig, -) !GhosttyLibVt { +) !AppleLibs { + var result: AppleLibs = .{}; + + // macOS universal (arm64 + x86_64) const aarch64_zig = try zig.retarget( b, cfg, @@ -121,7 +147,6 @@ pub fn initStaticAppleUniversal( deps, Config.genericMacOSTarget(b, .x86_64), ); - const aarch64 = try initStatic(b, &aarch64_zig); const x86_64 = try initStatic(b, &x86_64_zig); const universal = LipoStep.create(b, .{ @@ -130,15 +155,38 @@ pub fn initStaticAppleUniversal( .input_a = aarch64.output, .input_b = x86_64.output, }); - - return .{ + result.put(.macos_universal, .{ .step = universal.step, .artifact = universal.step, .kind = .static, .output = universal.output, .dsym = null, .pkg_config = null, - }; + }); + + // Additional Apple platforms, each gated on SDK availability. + for (ApplePlatform.sdk_platforms) |p| { + const target_query: std.Target.Query = .{ + .cpu_arch = .aarch64, + .os_tag = p.os_tag, + .os_version_min = Config.osVersionMin(p.os_tag), + }; + if (detectAppleSDK(b.resolveTargetQuery(target_query).result)) { + const dev_zig = try zig.retarget(b, cfg, deps, b.resolveTargetQuery(target_query)); + result.put(p.device, try initStatic(b, &dev_zig)); + + const sim_zig = try zig.retarget(b, cfg, deps, b.resolveTargetQuery(.{ + .cpu_arch = .aarch64, + .os_tag = p.os_tag, + .os_version_min = Config.osVersionMin(p.os_tag), + .abi = .simulator, + .cpu_model = .{ .explicit = &std.Target.aarch64.cpu.apple_a17 }, + })); + result.put(p.simulator, try initStatic(b, &sim_zig)); + } + } + + return result; } fn initLib( @@ -336,13 +384,11 @@ fn requiresPrivate(b: *std.Build) []const u8 { return ""; } -/// Create an XCFramework bundle from the static library. +/// Create an XCFramework bundle from Apple platform static libraries. pub fn xcframework( - lib_vt: *const GhosttyLibVt, + apple_libs: *const AppleLibs, + b: *std.Build, ) *XCFrameworkStep { - assert(lib_vt.kind == .static); - const b = lib_vt.step.owner; - // Generate a headers directory with a module map for Swift PM. // We can't use include/ directly because it contains a module map // for GhosttyKit (the macOS app library). @@ -359,18 +405,38 @@ pub fn xcframework( \\} \\ ); + const headers = wf.getDirectory(); + + var libraries: [AppleLibs.len]XCFrameworkStep.Library = undefined; + var lib_count: usize = 0; + for (std.enums.values(ApplePlatform)) |platform| { + if (apple_libs.get(platform)) |lib| { + libraries[lib_count] = .{ + .library = lib.output, + .headers = headers, + .dsym = null, + }; + lib_count += 1; + } + } return XCFrameworkStep.create(b, .{ .name = "ghostty-vt", .out_path = b.pathJoin(&.{ b.install_prefix, "lib/ghostty-vt.xcframework" }), - .libraries = &.{.{ - .library = lib_vt.output, - .headers = wf.getDirectory(), - .dsym = null, - }}, + .libraries = libraries[0..lib_count], }); } +/// Returns true if the Apple SDK for the given target is installed. +fn detectAppleSDK(target: std.Target) bool { + _ = std.zig.LibCInstallation.findNative(.{ + .allocator = std.heap.page_allocator, + .target = &target, + .verbose = false, + }) catch return false; + return true; +} + pub fn install( self: *const GhosttyLibVt, step: *std.Build.Step, From 249aee70105facdfdf0e627be4f0c0d342ce08a0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 14:50:35 -0700 Subject: [PATCH 7/8] example/swift-vt-xcframework: fix buffer overflow --- example/swift-vt-xcframework/Sources/main.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/example/swift-vt-xcframework/Sources/main.swift b/example/swift-vt-xcframework/Sources/main.swift index e7d15fb89..d374f539f 100644 --- a/example/swift-vt-xcframework/Sources/main.swift +++ b/example/swift-vt-xcframework/Sources/main.swift @@ -1,3 +1,4 @@ +import Foundation import GhosttyVt // Create a terminal with a small grid @@ -38,7 +39,8 @@ guard allocResult == GHOSTTY_SUCCESS, let buf else { } print("Plain text (\(len) bytes):") -print(String(cString: buf)) +let data = Data(bytes: buf, count: len) +print(String(data: data, encoding: .utf8) ?? "") ghostty_free(nil, buf, len) ghostty_formatter_free(formatter) From 445e1945da573a5b63adb4e4e7294c135cb0e86a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 14:53:10 -0700 Subject: [PATCH 8/8] ci: upload lib-vt source tarball to R2 Add R2 upload steps to the source-tarball-lib-vt job in the tip release workflow, matching the pattern used by the xcframework job. The tarball is uploaded to the ghostty-tip R2 bucket keyed by commit hash, making it available at tip.files.ghostty.org//libghostty-vt-source.tar.gz. --- .github/workflows/release-tip.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index bd6002607..0bebc2ff7 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -219,6 +219,7 @@ jobs: ) runs-on: namespace-profile-ghostty-sm env: + GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: @@ -260,6 +261,25 @@ jobs: libghostty-vt-source.tar.gz.minisig token: ${{ secrets.GH_RELEASE_TOKEN }} + - name: Prep R2 Storage + run: | + mkdir -p blob/${GHOSTTY_COMMIT_LONG} + cp libghostty-vt-source.tar.gz blob/${GHOSTTY_COMMIT_LONG}/libghostty-vt-source.tar.gz + - name: Upload to R2 + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 + with: + r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} + r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }} + r2-secret-access-key: ${{ secrets.CF_R2_TIP_SECRET_KEY }} + r2-bucket: ghostty-tip + source-dir: blob + destination-dir: ./ + + - name: Echo Release URLs + run: | + echo "Release URLs:" + echo " Source Tarball: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/libghostty-vt-source.tar.gz" + build-lib-vt-xcframework: needs: [setup] if: |