diff --git a/apps/mobile/modules/t3-terminal/README.md b/apps/mobile/modules/t3-terminal/README.md index 32670b893c70..16f855dd6e6a 100644 --- a/apps/mobile/modules/t3-terminal/README.md +++ b/apps/mobile/modules/t3-terminal/README.md @@ -26,8 +26,10 @@ Vendored Ghostty revision and license details are in `THIRD_PARTY_NOTICES.md`. ## Rebuilding GhosttyKit -The checked-in `GhosttyKit.xcframework` is built from the Ghostty custom-I/O fork (https://github.com/Yash-Singh1/ghostty/tree/custom-io). -Set the directory to the cloned repository checked out on the `custom-io` branch to `GHOSTTY_SOURCE_DIR`. +The checked-in `GhosttyKit.xcframework` is built from a pinned revision of the Ghostty custom-I/O +fork. The build applies the patches in `scripts/libghostty-ios-patches`, including Ghostty's upstream +fix for display callbacks that outlive their renderer. Set `GHOSTTY_SOURCE_DIR` to use an existing +checkout; otherwise the script creates a pinned checkout under `~/.cache/t3code`. ```bash apps/mobile/modules/t3-terminal/scripts/build-libghostty-ios16.sh diff --git a/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md b/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md index b06f18eadce5..0d99a43d1c38 100644 --- a/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md +++ b/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md @@ -9,6 +9,7 @@ iOS 16 support fork. That fork was created from VVTerm's custom-I/O Ghostty fork - Custom-I/O base fork: https://github.com/wiedymi/ghostty/tree/custom-io - Vendored source fork: https://github.com/Yash-Singh1/ghostty/tree/custom-io - Vendored revision: `d36c3b8dffd0d756dd5e5f4933962f774a0e6753` +- Applied renderer lifetime fix: https://github.com/ghostty-org/ghostty/commit/4b4a5b2411091ccda2cd6373631ec7ccd184c577 - Reference integration: https://github.com/vivy-company/vvterm - License: MIT diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/libghostty-fat.a b/apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/libghostty-fat.a index 788a46839d13..e72772b9cc43 100644 Binary files a/apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/libghostty-fat.a and b/apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64-simulator/libghostty-fat.a differ diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/libghostty-fat.a b/apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/libghostty-fat.a index d35d909c34f9..eed6ff4caf96 100644 Binary files a/apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/libghostty-fat.a and b/apps/mobile/modules/t3-terminal/Vendor/libghostty/GhosttyKit.xcframework/ios-arm64/libghostty-fat.a differ diff --git a/apps/mobile/modules/t3-terminal/scripts/build-libghostty-ios16.sh b/apps/mobile/modules/t3-terminal/scripts/build-libghostty-ios16.sh index d2f1e19bc408..6c380c030bee 100755 --- a/apps/mobile/modules/t3-terminal/scripts/build-libghostty-ios16.sh +++ b/apps/mobile/modules/t3-terminal/scripts/build-libghostty-ios16.sh @@ -5,8 +5,12 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MODULE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" VENDOR_DIR="${MODULE_DIR}/Vendor/libghostty" +PATCH_DIR="${SCRIPT_DIR}/libghostty-ios-patches" +LAYER_LIFETIME_PATCH="${PATCH_DIR}/0001-clear-display-callback-before-layer-release.patch" -GHOSTTY_SOURCE_DIR="${GHOSTTY_SOURCE_DIR:-${HOME}/ghostty}" +GHOSTTY_REVISION="d36c3b8dffd0d756dd5e5f4933962f774a0e6753" +GHOSTTY_SOURCE_DIR="${GHOSTTY_SOURCE_DIR:-${HOME}/.cache/t3code/ghostty-${GHOSTTY_REVISION:0:8}}" +GHOSTTY_REPOSITORY_URL="${GHOSTTY_REPOSITORY_URL:-https://github.com/Yash-Singh1/ghostty.git}" GHOSTTY_ZIG_VERSION="${GHOSTTY_ZIG_VERSION:-0.15.2}" GHOSTTY_ZIG="${GHOSTTY_ZIG:-}" @@ -56,11 +60,67 @@ ensure_zig() { | tar -xJ --strip-components=1 -C "${cache_dir}" } +ensure_ghostty_source() { + if ! git -C "${GHOSTTY_SOURCE_DIR}" rev-parse --git-dir >/dev/null 2>&1; then + require_cmd git + log "cloning Ghostty ${GHOSTTY_REVISION}" + git clone --filter=blob:none --no-checkout "${GHOSTTY_REPOSITORY_URL}" \ + "${GHOSTTY_SOURCE_DIR}" + git -C "${GHOSTTY_SOURCE_DIR}" fetch --depth=1 origin "${GHOSTTY_REVISION}" + git -C "${GHOSTTY_SOURCE_DIR}" checkout --detach "${GHOSTTY_REVISION}" + fi + + local actual_revision + actual_revision="$(git -C "${GHOSTTY_SOURCE_DIR}" rev-parse HEAD)" + [[ "${actual_revision}" == "${GHOSTTY_REVISION}" ]] || \ + die "expected Ghostty ${GHOSTTY_REVISION}, found ${actual_revision}" +} + +apply_ghostty_patch() { + [[ -f "${LAYER_LIFETIME_PATCH}" ]] || \ + die "missing required patch: ${LAYER_LIFETIME_PATCH}" + + local patch_name + patch_name="$(basename "${LAYER_LIFETIME_PATCH}")" + if git -C "${GHOSTTY_SOURCE_DIR}" apply --reverse --check "${LAYER_LIFETIME_PATCH}" >/dev/null 2>&1; then + log "patch already applied: ${patch_name}" + return + fi + + log "applying patch: ${patch_name}" + git -C "${GHOSTTY_SOURCE_DIR}" apply --check "${LAYER_LIFETIME_PATCH}" + git -C "${GHOSTTY_SOURCE_DIR}" apply "${LAYER_LIFETIME_PATCH}" +} + +validate_ghostty_source() ( + umask 077 + + scratch_dir="$(mktemp -d "${TMPDIR:-/tmp}/t3code-libghostty-source.XXXXXX")" + trap 'rm -rf -- "${scratch_dir}"' EXIT + + export GIT_INDEX_FILE="${scratch_dir}/index" + git -C "${GHOSTTY_SOURCE_DIR}" read-tree "${GHOSTTY_REVISION}" + git -C "${GHOSTTY_SOURCE_DIR}" apply --cached "${LAYER_LIFETIME_PATCH}" + + local tracked_changes + local untracked_files + tracked_changes="$(git -C "${GHOSTTY_SOURCE_DIR}" diff --name-status)" + untracked_files="$(git -C "${GHOSTTY_SOURCE_DIR}" ls-files --others --exclude-standard)" + if [[ -n "${tracked_changes}" || -n "${untracked_files}" ]]; then + [[ -z "${tracked_changes}" ]] || printf '%s\n' "${tracked_changes}" >&2 + [[ -z "${untracked_files}" ]] || printf '%s\n' "${untracked_files}" >&2 + die "Ghostty source contains changes beyond the required patch" + fi +) + require_cmd git require_cmd xcodebuild require_cmd xcrun require_cmd rsync ensure_zig +ensure_ghostty_source +apply_ghostty_patch +validate_ghostty_source ghostty_ref="$(git -C "${GHOSTTY_SOURCE_DIR}" rev-parse HEAD)" log "using Ghostty source: ${GHOSTTY_SOURCE_DIR} @ ${ghostty_ref}" @@ -69,7 +129,7 @@ log "building GhosttyKit.xcframework" ( cd "${GHOSTTY_SOURCE_DIR}" - PATH="$(dirname "${GHOSTTY_ZIG}"):${PATH}" "${GHOSTTY_ZIG}" build \ + PATH="$(dirname "${GHOSTTY_ZIG}"):${PATH}" "${GHOSTTY_ZIG}" build -j1 \ -Dapp-runtime=none \ -Demit-xcframework=true \ -Demit-macos-app=false \ @@ -98,9 +158,9 @@ xcrun strip -S -x "${sim_archive}" log "copying iOS archives into ${VENDOR_DIR}/GhosttyKit.xcframework" cp "${ios_archive}" "${VENDOR_DIR}/GhosttyKit.xcframework/ios-arm64/libghostty-fat.a" cp "${sim_archive}" "${VENDOR_DIR}/GhosttyKit.xcframework/ios-arm64-simulator/libghostty-fat.a" -rsync -a --delete "${xcframework}/ios-arm64/Headers/" \ +rsync -a "${xcframework}/ios-arm64/Headers/" \ "${VENDOR_DIR}/GhosttyKit.xcframework/ios-arm64/Headers/" -rsync -a --delete "${xcframework}/ios-arm64-simulator/Headers/" \ +rsync -a "${xcframework}/ios-arm64-simulator/Headers/" \ "${VENDOR_DIR}/GhosttyKit.xcframework/ios-arm64-simulator/Headers/" log "done" diff --git a/apps/mobile/modules/t3-terminal/scripts/libghostty-ios-patches/0001-clear-display-callback-before-layer-release.patch b/apps/mobile/modules/t3-terminal/scripts/libghostty-ios-patches/0001-clear-display-callback-before-layer-release.patch new file mode 100644 index 000000000000..cd549634713e --- /dev/null +++ b/apps/mobile/modules/t3-terminal/scripts/libghostty-ios-patches/0001-clear-display-callback-before-layer-release.patch @@ -0,0 +1,12 @@ +diff --git a/src/renderer/metal/IOSurfaceLayer.zig b/src/renderer/metal/IOSurfaceLayer.zig +index 8e37754a0..0d0c2b9e5 100644 +--- a/src/renderer/metal/IOSurfaceLayer.zig ++++ b/src/renderer/metal/IOSurfaceLayer.zig +@@ -42,3 +42,7 @@ + pub fn release(self: *IOSurfaceLayer) void { ++ // The layer may be retained by the view after we release our reference. ++ // Clear the callback first so that a later display pass can't access the ++ // renderer that owned this wrapper after it has been freed. ++ self.setDisplayCallback(null, null); + self.layer.release(); + }