From 4b4845e0218d799d9312c608c2aeafce60b12112 Mon Sep 17 00:00:00 2001 From: Arach Tchoupani Date: Wed, 16 Sep 2026 13:19:25 -0400 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A7=AA=20Add=20experimental=20Ghostty?= =?UTF-8?q?=20frame=20export=20and=20credit=20gating?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patches/ghostty/0.1.6/FRAME-EXPORT.md | 74 ++++++ .../ghostty-ios-simulator-universal.patch | 2 +- .../0.1.6/ghostty-terminal-frame-export.patch | 234 ++++++++++++++++++ scripts/build-ghosttykit.sh | 14 +- 4 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 patches/ghostty/0.1.6/FRAME-EXPORT.md create mode 100644 patches/ghostty/0.1.6/ghostty-terminal-frame-export.patch diff --git a/patches/ghostty/0.1.6/FRAME-EXPORT.md b/patches/ghostty/0.1.6/FRAME-EXPORT.md new file mode 100644 index 0000000..00bb5c8 --- /dev/null +++ b/patches/ghostty/0.1.6/FRAME-EXPORT.md @@ -0,0 +1,74 @@ +# Experimental macOS frame export + +This source patch supports Hudson's isolated terminal proof. It adds no SwiftUI +code and preserves the layout of existing Ghostty configuration structs. It is +a private extension to pinned Ghostty source, not an upstream API. No published +binary pin changes in this branch. + +## Ownership contract + +`ghostty_surface_new_with_frame_export` creates an offscreen macOS surface with +an AppKit NSView as a platform anchor. No window is required. Pixel dimensions +come from the resize mailbox; local layer presentation is skipped, and rendering +uses event scheduling instead of a local display link. + +The export callback runs after terminal render passes are encoded and before +their Metal command buffer is committed. Its texture and command buffer are +borrowed. Append a GPU copy into a host-owned bounded IOSurface pool. Never commit +or wait on the command buffer, modify the source, call surface APIs, wait for +external consumers, or access the source after GPU completion. Keep userdata +alive until `ghostty_surface_free` returns. + +Engine target reuse waits for that same command buffer, including the export +copy. The host owns the destination's separate lease: publish after successful +producer completion and release after consumer GPU completion. The optional +`has_credit_cb` runs before GPU encoding: return false when the pool is full to +preserve dirty state and skip GPU frame work while parsing continues. The export +callback must still handle unavailable credit. On the engine app thread, call +`ghostty_surface_request_frame_export` when credit returns to schedule a current +frame even after output becomes idle. It performs no synchronous GPU wait. + +This adds a GPU copy, avoids CPU frame serialization, and avoids holding engine +targets for an external process. Its latency/bandwidth cost needs a matched +benchmark before selecting a production export design. + +## Build and test + +Base source: `07d31666e73bce337b9cece60a884c67fe8906f4`. With a clean source checkout: + +```sh +scripts/build-ghosttykit.sh --ghostty-dir /path/to/isolated/ghostty \ + --ref 07d31666e73bce337b9cece60a884c67fe8906f4 \ + --xcframework-target native +``` + +Subsequent builds omit `--ref`; the script refuses to switch dirty source. +`native` builds the current Mac architecture; the existing default is universal. +The simulator patch also repairs its final hunk count so plain `git apply` works. +All three patches were applied in order to a temporary index of the pinned source. + +Zig 0.15.2 and Apple's Metal toolchain are required. This Zig linker has a known +failure with Xcode 26.4+ SDK libSystem stubs: +[Ghostty #11991](https://github.com/ghostty-org/ghostty/issues/11991), +[Zig #31658](https://codeberg.org/ziglang/zig/issues/31658). +Prefer a compatible Xcode/SDK selected per build via `DEVELOPER_DIR`. + +The local 2026-09-16 proof used a private SDK overlay adding `arm64-macos` to +`arm64e-macos` target groups in the libSystem text stub. Local evidence records +the original stub hash and transformation. This enables a native diagnostic +build; it is **not release-toolchain qualification**. Installed SDKs are unchanged; +no modified SDK or generated binary is committed. Release needs a supported +build toolchain and macOS/iOS regression checks. + +Hudson's `Tools/TerminalIsolationProbe/run.ts --terminal` uses the built native +XCFramework in an app-bundled XPC helper. It tests real PTY output/input, exported +glyph pixels, AppKit/Metal presentation, a stalled host main thread, frame-credit +saturation/recovery, and explicit PTY teardown. + +The AppKit helper requires `XPCService.RunLoopType = NSRunLoop`. The default +`dispatch_main` can execute main-queue callbacks on a dispatch worker thread. +The fixture asserts AppKit initialization runs on the actual main thread. + +Remaining product gates include input/IME/selection/accessibility, dynamic resize +pool generations, lifecycle recovery, peer signing, multiple panes, a second +consumer, and matched performance/soak tests. This does not enable Scout cutover. diff --git a/patches/ghostty/0.1.6/ghostty-ios-simulator-universal.patch b/patches/ghostty/0.1.6/ghostty-ios-simulator-universal.patch index 75638e3..b0830e0 100644 --- a/patches/ghostty/0.1.6/ghostty-ios-simulator-universal.patch +++ b/patches/ghostty/0.1.6/ghostty-ios-simulator-universal.patch @@ -44,7 +44,7 @@ index a19dd18af..3d67ac1a1 100644 // Generate a headers directory with only ghostty.h and the module // map. We can't use include/ directly because it also contains the -@@ -81,9 +98,9 @@ pub fn init( +@@ -81,8 +98,8 @@ pub fn init( .dsym = ios.dsym, }, .{ diff --git a/patches/ghostty/0.1.6/ghostty-terminal-frame-export.patch b/patches/ghostty/0.1.6/ghostty-terminal-frame-export.patch new file mode 100644 index 0000000..0c320af --- /dev/null +++ b/patches/ghostty/0.1.6/ghostty-terminal-frame-export.patch @@ -0,0 +1,234 @@ +diff --git a/include/ghostty.h b/include/ghostty.h +index dbd13cfef..cec277d7c 100644 +--- a/include/ghostty.h ++++ b/include/ghostty.h +@@ -1110,6 +1110,28 @@ GHOSTTY_API ghostty_surface_config_s ghostty_surface_config_new(); + + GHOSTTY_API ghostty_surface_t ghostty_surface_new(ghostty_app_t, + const ghostty_surface_config_s*); ++// Experimental macOS offscreen export constructor. Existing surface config ABI ++// is unchanged. The NSView is a geometry/platform anchor; no window is required. ++// encode_cb runs on a renderer thread after render encoders end, before commit. ++// texture and command_buffer are borrowed id/id. ++// Encode a GPU copy to an owned, bounded IOSurface pool and publish that buffer ++// only from a successful command completion handler. Never block, commit/wait, ++// call surface APIs, or modify the source texture from this callback. Skip the ++// copy when no frame credit is available. userdata must outlive surface_free. ++typedef struct { ++ void* userdata; ++ void (*encode_cb)(void* userdata, void* texture, void* command_buffer); ++ // Optional fast check before GPU encoding. Return false when the owned pool ++ // has no credit. Preserve dirty state until request_frame_export is called. ++ bool (*has_credit_cb)(void* userdata); ++} ghostty_frame_export_s; ++GHOSTTY_API ghostty_surface_t ghostty_surface_new_with_frame_export( ++ ghostty_app_t, const ghostty_surface_config_s*, const ghostty_frame_export_s*); ++ ++// Call on the engine app thread after export credit returns. Schedules a ++// current-state frame even if the terminal became idle while exports were full. ++GHOSTTY_API void ghostty_surface_request_frame_export(ghostty_surface_t); ++ + GHOSTTY_API void ghostty_surface_free(ghostty_surface_t); + GHOSTTY_API void* ghostty_surface_userdata(ghostty_surface_t); + GHOSTTY_API ghostty_app_t ghostty_surface_app(ghostty_surface_t); +diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig +index 7fc979d03..93d14fb0b 100644 +--- a/src/apprt/embedded.zig ++++ b/src/apprt/embedded.zig +@@ -244,12 +244,16 @@ pub const App = struct { + + /// Create a new surface for the app. + fn newSurface(self: *App, opts: Surface.Options) !*Surface { ++ return self.newSurfaceWithFrameExport(opts, null); ++ } ++ ++ fn newSurfaceWithFrameExport(self: *App, opts: Surface.Options, frame_export: ?Surface.FrameExport) !*Surface { + // Grab a surface allocation because we're going to need it. + var surface = try self.core_app.alloc.create(Surface); + errdefer self.core_app.alloc.destroy(surface); + + // Create the surface +- try surface.init(self, opts); ++ try surface.initWithFrameExport(self, opts, frame_export); + errdefer surface.deinit(); + + return surface; +@@ -412,6 +416,15 @@ pub const EnvVar = extern struct { + }; + + pub const Surface = struct { ++ /// Experimental macOS export hook. The callback may append commands to the ++ /// borrowed command buffer but may not commit, wait, or retain the source ++ /// texture past GPU completion. It must never wait for external consumers. ++ pub const FrameExport = extern struct { ++ userdata: ?*anyopaque, ++ encode: *const fn (?*anyopaque, ?*anyopaque, ?*anyopaque) callconv(.c) void, ++ has_credit: ?*const fn (?*anyopaque) callconv(.c) bool, ++ }; ++ frame_export: ?FrameExport = null, + app: *App, + platform: Platform, + userdata: ?*anyopaque = null, +@@ -469,8 +482,13 @@ pub const Surface = struct { + }; + + pub fn init(self: *Surface, app: *App, opts: Options) !void { ++ return self.initWithFrameExport(app, opts, null); ++ } ++ ++ fn initWithFrameExport(self: *Surface, app: *App, opts: Options, frame_export: ?FrameExport) !void { + self.* = .{ + .app = app, ++ .frame_export = frame_export, + .platform = try .init(opts.platform_tag, opts.platform), + .userdata = opts.userdata, + .core_surface = undefined, +@@ -489,6 +507,9 @@ pub const Surface = struct { + // Shallow copy the config so that we can modify it. + var config = try apprt.surface.newConfig(app.core_app, &app.config, opts.context); + defer config.deinit(); ++ // Remote presentation uses the renderer's event/timer scheduling, not ++ // a display link tied to a local window. No fake visible window needed. ++ if (frame_export != null) config.@"window-vsync" = false; + + // If we have a working directory from the options then we set it. + if (opts.working_directory) |c_wd| { +@@ -1563,6 +1584,30 @@ pub const CAPI = struct { + }; + } + ++ /// Separate constructor keeps the existing surface configuration ABI intact. ++ export fn ghostty_surface_new_with_frame_export( ++ app: *App, ++ opts: *const apprt.Surface.Options, ++ frame_export: *const Surface.FrameExport, ++ ) ?*Surface { ++ if (comptime builtin.os.tag != .macos) return null; ++ if (opts.platform_tag != @intFromEnum(PlatformTag.macos)) return null; ++ return app.newSurfaceWithFrameExport(opts.*, frame_export.*) catch |err| { ++ log.err("error initializing remote surface err={}", .{err}); ++ return null; ++ }; ++ } ++ ++ export fn ghostty_surface_request_frame_export(surface: *Surface) void { ++ if (comptime builtin.os.tag != .macos) return; ++ if (surface.frame_export == null) return; ++ const core_renderer = &surface.core_surface.renderer; ++ core_renderer.draw_mutex.lock(); ++ core_renderer.cells_rebuilt = true; ++ core_renderer.draw_mutex.unlock(); ++ surface.refresh(); ++ } ++ + fn surface_new_( + app: *App, + opts: *const apprt.Surface.Options, +diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig +index 6c7432d21..5c4b95022 100644 +--- a/src/renderer/Metal.zig ++++ b/src/renderer/Metal.zig +@@ -39,6 +39,8 @@ pub const swap_chain_count = 3; + const log = std.log.scoped(.metal); + + layer: IOSurfaceLayer, ++frame_export: ?apprt.embedded.Surface.FrameExport = null, ++remote_size: rendererpkg.ScreenSize = .{ .width = 0, .height = 0 }, + + /// MTLDevice + device: objc.Object, +@@ -147,6 +149,8 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { + + return .{ + .layer = layer, ++ .frame_export = opts.rt_surface.frame_export, ++ .remote_size = .{ .width = opts.rt_surface.size.width, .height = opts.rt_surface.size.height }, + .device = device, + .queue = queue, + .blending = opts.config.blending, +@@ -162,6 +166,7 @@ pub fn deinit(self: *Metal) void { + } + + pub fn loopEnter(self: *Metal) void { ++ if (self.frame_export != null) return; + const renderer: *align(1) Renderer = @fieldParentPtr("api", self); + self.layer.setDisplayCallback( + @ptrCast(&displayCallback), +@@ -214,6 +219,9 @@ pub fn initShaders( + + /// Get the current size of the runtime surface. + pub fn surfaceSize(self: *const Metal) !struct { width: u32, height: u32 } { ++ if (self.frame_export != null) { ++ return .{ .width = @min(self.remote_size.width, self.max_texture_size), .height = @min(self.remote_size.height, self.max_texture_size) }; ++ } + const bounds = self.layer.layer.getProperty(graphics.Rect, "bounds"); + const scale = self.layer.layer.getProperty(f64, "contentsScale"); + +@@ -232,6 +240,19 @@ pub fn surfaceSize(self: *const Metal) !struct { width: u32, height: u32 } { + }; + } + ++// Called under the renderer draw mutex from its resize mailbox. ++pub fn setScreenSize(self: *Metal, size: rendererpkg.Size) void { ++ if (self.frame_export != null) self.remote_size = size.screen; ++} ++ ++// Cheap host credit check before any GPU frame work. Keep pending dirty state ++// until the host requests another frame after returning a buffer credit. ++pub fn canDraw(self: *const Metal) bool { ++ const exporter = self.frame_export orelse return true; ++ const has_credit = exporter.has_credit orelse return true; ++ return has_credit(exporter.userdata); ++} ++ + /// Initialize a new render target which can be presented by this API. + pub fn initTarget(self: *const Metal, width: usize, height: usize) !Target { + return Target.init(.{ +@@ -251,6 +272,7 @@ pub fn initTarget(self: *const Metal, width: usize, height: usize) !Target { + + /// Present the provided target. + pub inline fn present(self: *Metal, target: Target, sync: bool) !void { ++ if (self.frame_export != null) return; + if (sync) { + self.layer.setSurfaceSync(target.surface); + } else { +diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig +index 0f4a294bc..ba56e5b41 100644 +--- a/src/renderer/generic.zig ++++ b/src/renderer/generic.zig +@@ -1487,6 +1487,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { + try self.api.presentLastTarget(); + return; + } ++ if (@hasDecl(GraphicsAPI, "canDraw")) { ++ if (!self.api.canDraw()) return; ++ } + self.cells_rebuilt = false; + + // Wait for a frame to be available. +@@ -1923,6 +1926,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + ++ if (@hasDecl(GraphicsAPI, "setScreenSize")) self.api.setScreenSize(size); ++ + // We only actually need the padding from this, + // everything else is derived elsewhere. + self.size.padding = size.padding; +diff --git a/src/renderer/metal/Frame.zig b/src/renderer/metal/Frame.zig +index 388b4f9ed..368ceee76 100644 +--- a/src/renderer/metal/Frame.zig ++++ b/src/renderer/metal/Frame.zig +@@ -106,6 +106,12 @@ pub inline fn renderPass( + /// + /// If `sync` is true, this will block until the frame is presented. + pub inline fn complete(self: *Self, sync: bool) void { ++ // Append an export copy after all terminal passes, before commit. Engine ++ // target reuse waits for this same command buffer, so the copy cannot race ++ // a subsequent frame. The host owns the destination pool and its leases. ++ if (self.block.renderer.api.frame_export) |exporter| { ++ exporter.encode(exporter.userdata, self.block.target.texture.value, self.buffer.value); ++ } + // If we don't need to complete synchronously, + // we add our block as a completion handler. + // diff --git a/scripts/build-ghosttykit.sh b/scripts/build-ghosttykit.sh index 4949bf5..e1eab09 100755 --- a/scripts/build-ghosttykit.sh +++ b/scripts/build-ghosttykit.sh @@ -9,6 +9,7 @@ GHOSTTY_DIR="${GHOSTTY_DIR:-${REPO_ROOT}/vendor/ghostty}" GHOSTTY_REPO="${GHOSTTY_REPO:-https://github.com/ghostty-org/ghostty.git}" GHOSTTY_REF="${GHOSTTY_REF:-}" GHOSTTY_OPTIMIZE="${GHOSTTY_OPTIMIZE:-ReleaseFast}" +GHOSTTY_XCFRAMEWORK_TARGET="${GHOSTTY_XCFRAMEWORK_TARGET:-universal}" GHOSTTY_PATCH_DIR="${GHOSTTY_PATCH_DIR:-${REPO_ROOT}/patches/ghostty/0.1.6}" SHOULD_FETCH=0 @@ -21,6 +22,7 @@ Options: --ref REF Git ref to check out before building. --fetch Fetch origin before checking out REF. --optimize MODE Zig optimize mode. Default: ${GHOSTTY_OPTIMIZE} + --xcframework-target TARGET native (current Mac) or universal. Default: ${GHOSTTY_XCFRAMEWORK_TARGET} --patch-dir PATH Patches for the pinned Ghostty source. Default: ${GHOSTTY_PATCH_DIR} Env: @@ -28,6 +30,7 @@ Env: GHOSTTY_REPO GHOSTTY_REF GHOSTTY_OPTIMIZE + GHOSTTY_XCFRAMEWORK_TARGET GHOSTTY_PATCH_DIR EOF } @@ -114,6 +117,10 @@ while [[ $# -gt 0 ]]; do GHOSTTY_OPTIMIZE="$2" shift 2 ;; + --xcframework-target) + GHOSTTY_XCFRAMEWORK_TARGET="$2" + shift 2 + ;; --patch-dir) GHOSTTY_PATCH_DIR="$2" shift 2 @@ -130,6 +137,11 @@ while [[ $# -gt 0 ]]; do esac done +case "${GHOSTTY_XCFRAMEWORK_TARGET}" in + native|universal) ;; + *) echo "error: xcframework target must be native or universal" >&2; exit 1 ;; +esac + require_cmd git require_cmd zig @@ -145,7 +157,7 @@ apply_patches -Demit-macos-app=false \ -Demit-exe=false \ -Doptimize="${GHOSTTY_OPTIMIZE}" \ - -Dxcframework-target=universal + -Dxcframework-target="${GHOSTTY_XCFRAMEWORK_TARGET}" ) "${REPO_ROOT}/scripts/install-ghosttykit.sh" "${GHOSTTY_DIR}/macos/GhosttyKit.xcframework" From 592fef427397998627b674a047e1dc0663c2313d Mon Sep 17 00:00:00 2001 From: Arach Tchoupani Date: Wed, 16 Sep 2026 13:35:22 -0400 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A7=AA=20Validate=20Ghostty=20with=20?= =?UTF-8?q?stock=20Xcode=2026.3=20and=20pinned=20Zig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ghostty-toolchain.yml | 49 +++++++++++++++ scripts/validate-ghostty-toolchain.sh | 84 +++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 .github/workflows/ghostty-toolchain.yml create mode 100755 scripts/validate-ghostty-toolchain.sh diff --git a/.github/workflows/ghostty-toolchain.yml b/.github/workflows/ghostty-toolchain.yml new file mode 100644 index 0000000..c4d6b72 --- /dev/null +++ b/.github/workflows/ghostty-toolchain.yml @@ -0,0 +1,49 @@ +name: Ghostty compiler validation + +on: + pull_request: + paths: + - 'patches/ghostty/**' + - 'scripts/validate-ghostty-toolchain.sh' + - '.github/workflows/ghostty-toolchain.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + native: + runs-on: macos-15 + timeout-minutes: 35 + env: + DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/checkout@v4 + with: + repository: ghostty-org/ghostty + ref: 07d31666e73bce337b9cece60a884c67fe8906f4 + path: build/ghostty + persist-credentials: false + - name: Install pinned Zig + shell: bash + run: | + set -euo pipefail + test "$(uname -m)" = arm64 + curl --fail --location --retry 3 --output "$RUNNER_TEMP/zig.tar.xz" \ + https://ziglang.org/download/0.15.2/zig-aarch64-macos-0.15.2.tar.xz + echo "3cc2bab367e185cdfb27501c4b30b1b0653c28d9f73df8dc91488e66ece5fa6b $RUNNER_TEMP/zig.tar.xz" | shasum -a 256 --check + tar -xf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/zig-aarch64-macos-0.15.2" >> "$GITHUB_PATH" + - name: Validate stock compiler and build native engine + run: scripts/validate-ghostty-toolchain.sh build/ghostty build/evidence + - name: Upload compiler evidence and native framework + if: always() + uses: actions/upload-artifact@v4 + with: + name: ghostty-xcode26.3-zig0.15.2-arm64 + path: build/evidence + if-no-files-found: error + retention-days: 14 diff --git a/scripts/validate-ghostty-toolchain.sh b/scripts/validate-ghostty-toolchain.sh new file mode 100755 index 0000000..325518f --- /dev/null +++ b/scripts/validate-ghostty-toolchain.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Build the pinned engine against an unmodified Xcode 26.3 SDK in a clean checkout. +# Does not install a framework or change the selected system developer directory. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SOURCE_DIR="$(cd "${1:?usage: $0 CLEAN_GHOSTTY_SOURCE EVIDENCE_DIR}" && pwd)" +mkdir -p "${2:?usage: $0 CLEAN_GHOSTTY_SOURCE EVIDENCE_DIR}" +EVIDENCE_DIR="$(cd "$2" && pwd)" +export SOURCE_DIR EVIDENCE_DIR REPO_ROOT +exec > >(tee "${EVIDENCE_DIR}/validation.log") 2>&1 + +[[ "$(uname -m)" == arm64 ]] +[[ "$(zig version)" == 0.15.2 ]] +[[ "$(command -v xcrun)" == /usr/bin/xcrun ]] +[[ -n "${DEVELOPER_DIR:-}" ]] +[[ "$(/usr/bin/xcodebuild -version)" == $'Xcode 26.3\nBuild version 17C529' ]] +[[ "$(git -C "${SOURCE_DIR}" rev-parse HEAD)" == 07d31666e73bce337b9cece60a884c67fe8906f4 ]] +[[ -z "$(git -C "${SOURCE_DIR}" status --porcelain)" ]] +[[ ! -d "${SOURCE_DIR}/.zig-cache" && ! -d "${SOURCE_DIR}/macos/GhosttyKit.xcframework" ]] +unset SDKROOT +SDK_PATH="$(/usr/bin/xcrun --sdk macosx --show-sdk-path)" +case "${SDK_PATH}" in + "${DEVELOPER_DIR}"/Platforms/MacOSX.platform/Developer/SDKs/*) ;; + *) echo "Expected the selected Xcode's stock SDK, got ${SDK_PATH}"; exit 1 ;; +esac +export SDK_PATH +/usr/bin/xcodebuild -version +/usr/bin/xcrun --sdk macosx --show-sdk-version +/usr/bin/xcrun metal --version +zig version + +# A fresh local cache ensures the build runner is linked with this toolchain. +export ZIG_LOCAL_CACHE_DIR="${SOURCE_DIR}/.zig-cache" +for patch in "${REPO_ROOT}"/patches/ghostty/0.1.6/*.patch; do + git -C "${SOURCE_DIR}" apply --check "${patch}" + git -C "${SOURCE_DIR}" apply "${patch}" +done +( + cd "${SOURCE_DIR}" + zig build -j3 -Dapp-runtime=none -Demit-xcframework=true \ + -Demit-macos-app=false -Demit-exe=false -Doptimize=ReleaseFast \ + -Dxcframework-target=native +) + +# Verify the two new C exports and package the exact bytes for local runtime checks. +FRAMEWORK="${SOURCE_DIR}/macos/GhosttyKit.xcframework" +LIBRARY="${FRAMEWORK}/macos-arm64/libghostty-internal-fat.a" +[[ "$(/usr/bin/lipo -archs "${LIBRARY}")" == arm64 ]] +/usr/bin/nm -gU "${LIBRARY}" > "${EVIDENCE_DIR}/symbols.txt" +for symbol in ghostty_surface_new_with_frame_export ghostty_surface_request_frame_export; do + grep -q " T _${symbol}$" "${EVIDENCE_DIR}/symbols.txt" +done +/usr/bin/tar -czf "${EVIDENCE_DIR}/GhosttyKit.xcframework.tar.gz" -C "${SOURCE_DIR}/macos" GhosttyKit.xcframework +python3 - <<'PY' +import hashlib, json, os, pathlib, subprocess +root = pathlib.Path(os.environ['REPO_ROOT']) +source = pathlib.Path(os.environ['SOURCE_DIR']) +evidence = pathlib.Path(os.environ['EVIDENCE_DIR']) +framework = source / 'macos/GhosttyKit.xcframework' +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() +def output(*args): + return subprocess.check_output(args, text=True).strip() +result = { + 'status': 'PASS', + 'scope': 'Clean native arm64 engine build and exported symbols; runtime validation is separate', + 'sourceCommit': output('git', '-C', str(source), 'rev-parse', 'HEAD'), + 'consumerCommit': os.environ.get('GITHUB_SHA'), + 'runURL': 'https://github.com/' + os.environ.get('GITHUB_REPOSITORY', '') + '/actions/runs/' + os.environ.get('GITHUB_RUN_ID', ''), + 'xcode': output('/usr/bin/xcodebuild', '-version'), + 'sdkPath': os.environ['SDK_PATH'], + 'sdkVersion': output('/usr/bin/xcrun', '--sdk', 'macosx', '--show-sdk-version'), + 'sdkLibSystemSHA256': sha(pathlib.Path(os.environ['SDK_PATH']) / 'usr/lib/libSystem.tbd'), + 'zig': output('zig', 'version'), + 'metal': output('/usr/bin/xcrun', 'metal', '--version'), + 'sdkOverlay': False, + 'patches': [{'name': p.name, 'sha256': sha(p)} for p in sorted((root / 'patches/ghostty/0.1.6').glob('*.patch'))], + 'librarySHA256': sha(framework / 'macos-arm64/libghostty-internal-fat.a'), + 'archiveSHA256': sha(evidence / 'GhosttyKit.xcframework.tar.gz'), +} +(evidence / 'provenance.json').write_text(json.dumps(result, indent=2) + '\n') +print(json.dumps(result, indent=2)) +PY From 3d5a49c3749009146ae23cabcdf604c94084d908 Mon Sep 17 00:00:00 2001 From: Arach Tchoupani Date: Wed, 16 Sep 2026 13:44:25 -0400 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20Record=20validated=20Xcode?= =?UTF-8?q?=2026.3=20terminal=20engine=20baseline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patches/ghostty/0.1.6/FRAME-EXPORT.md | 35 +++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/patches/ghostty/0.1.6/FRAME-EXPORT.md b/patches/ghostty/0.1.6/FRAME-EXPORT.md index 00bb5c8..13a3302 100644 --- a/patches/ghostty/0.1.6/FRAME-EXPORT.md +++ b/patches/ghostty/0.1.6/FRAME-EXPORT.md @@ -57,8 +57,9 @@ The local 2026-09-16 proof used a private SDK overlay adding `arm64-macos` to `arm64e-macos` target groups in the libSystem text stub. Local evidence records the original stub hash and transformation. This enables a native diagnostic build; it is **not release-toolchain qualification**. Installed SDKs are unchanged; -no modified SDK or generated binary is committed. Release needs a supported -build toolchain and macOS/iOS regression checks. +no modified SDK or generated binary is committed. The subsequent stock-toolchain +validation below supersedes that overlay build for native arm64 evidence. +Universal packaging and macOS/iOS release regressions remain open. Hudson's `Tools/TerminalIsolationProbe/run.ts --terminal` uses the built native XCFramework in an app-bundled XPC helper. It tests real PTY output/input, exported @@ -72,3 +73,33 @@ The fixture asserts AppKit initialization runs on the actual main thread. Remaining product gates include input/IME/selection/accessibility, dynamic resize pool generations, lifecycle recovery, peer signing, multiple panes, a second consumer, and matched performance/soak tests. This does not enable Scout cutover. + +## Validated compiler baseline — 2026-09-16 + +Use **Xcode 26.3 (17C529), its stock macOS 26.2 SDK, and Zig 0.15.2** for +this pinned engine. Xcode 27 is not a project requirement. A fresh arm64 GitHub +runner built the same source and patches without any SDK overlay: +[successful compiler validation](https://github.com/arach/Termini/actions/runs/35129139473). +Apple Metal reported version `32023.864`. + +The workflow `.github/workflows/ghostty-toolchain.yml` invokes +`scripts/validate-ghostty-toolchain.sh` on a clean pinned checkout, verifies both +new C exports, and retains the native framework plus toolchain/source/patch hashes. +The Zig download has a fixed SHA-256. No engine build cache is restored. + +The exact downloaded library was verified by SHA-256 before linking into Hudson's +local terminal helper. Its PTY/GPU fixture passed: 122 presentations, 8 completions +within the 300 ms host-main-stall sample, continued parsing with all three credits +held, immutable held frames, idle recovery, stale ACK rejection and PTY/helper +cleanup. These are correctness checks, not a throughput comparison with the +previous 121-frame run. The host fixture was compiled locally using the installed +CLT and macOS 26.5 SDK, targeting macOS 14; the engine was compiled by the pinned +Xcode 26.3 CI toolchain. This does not establish runtime compatibility on every +supported macOS release. + +Native library SHA-256: +`8e8c285383241c6c62b6d256f2771e300f7ffa09410fbfa1e129eacc387a3cb2`. + +This closes the stock compiler/SDK gate for **native macOS arm64 engine builds**. +Intel, universal/iOS builds, full product integration, release signing and the +wider OS/runtime regression matrix are not covered. No release binary pin changed.