From 7955522a8db0259f4e765d18364c2abe1ccbcfd7 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 20 Sep 2026 11:13:55 -0700 Subject: [PATCH 1/4] test: cover Release helper architecture handoff --- tests/test_ci_change_areas.py | 14 +- tests/test_ci_release_build_archs.sh | 22 +-- tests/test_ci_release_helper_archs.py | 200 ++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 tests/test_ci_release_helper_archs.py diff --git a/tests/test_ci_change_areas.py b/tests/test_ci_change_areas.py index e65a7f9eb36..81ee662df88 100644 --- a/tests/test_ci_change_areas.py +++ b/tests/test_ci_change_areas.py @@ -1522,18 +1522,16 @@ def test_required_macos_topology_collapses_display_and_release_helper_jobs() -> assert "/Applications/Xcode_16.4.app" not in package_block assert "Select helper Xcode" in package_block assert "CMUX_CI_REQUIRED_MACOS_SDK_MAJOR=15" in package_block - assert "Build universal Ghostty CLI helper" in package_block - assert "./scripts/build-ghostty-cli-helper.sh --universal --output ghostty-cli-helper/ghostty" in package_block + assert "Build Release Ghostty CLI helper" in package_block assert '[[ "$HELPER_SDK_VERSION" == 15.* ]]' in package_block assert "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in package_block - assert package_block.index("Select helper Xcode") < package_block.index("Build universal Ghostty CLI helper") - assert package_block.index("Build universal Ghostty CLI helper") < package_block.index("Select Xcode") - assert package_block.index("Upload universal Ghostty CLI helper") < package_block.index("Select Xcode") + assert package_block.index("Select helper Xcode") < package_block.index("Build Release Ghostty CLI helper") + assert package_block.index("Build Release Ghostty CLI helper") < package_block.index("Select Xcode") + assert package_block.index("Upload Release Ghostty CLI helper") < package_block.index("Select Xcode") assert " - swift-package-tests" in release_block - assert "Download universal Ghostty CLI helper" in release_block + assert "Download Release Ghostty CLI helper" in release_block assert "actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131" in release_block - assert "Install universal Ghostty CLI helper" in release_block - assert "./scripts/build-ghostty-cli-helper.sh --universal --output ghostty-cli-helper/ghostty" not in release_block + assert "Install Release helpers" in release_block def test_remote_tmux_layout_identity_uses_a_nontolerant_focused_gate() -> None: diff --git a/tests/test_ci_release_build_archs.sh b/tests/test_ci_release_build_archs.sh index f80bc9d0779..3a1a8e33ae2 100755 --- a/tests/test_ci_release_build_archs.sh +++ b/tests/test_ci_release_build_archs.sh @@ -33,17 +33,6 @@ for bad in "x86_64" "arm64 x86_64" "ARM64" "arm64;rm -rf /"; do fi done -if ! awk ' - /^ release-build:/ { in_job=1; next } - in_job && /^ [a-zA-Z0-9_-]+:/ { in_job=0 } - in_job && /scripts\/ci\/release-build-archs\.sh/ { saw_resolver=1 } - in_job && /ARCHS="\$RELEASE_ARCHS"/ { saw_build=1 } - in_job && /ARCHS="arm64/ { saw_literal=1 } - END { exit !(saw_resolver && saw_build && !saw_literal) } -' "$CI_FILE"; then - echo "FAIL: release-build must take its architectures from scripts/ci/release-build-archs.sh" - exit 1 -fi # Slice verification is exact: an arm64 check must reject a universal binary, # or a change that brings the Intel compile back would pass unnoticed. @@ -82,19 +71,12 @@ if verify "arm64"; then exit 1 fi -if ! awk ' - /^ release-build:/ { in_job=1; next } - in_job && /^ [a-zA-Z0-9_-]+:/ { in_job=0 } - in_job && /verify-binary-archs\.sh "\$RELEASE_ARCHS" "\$APP_BINARY" "\$CLI_BINARY" "\$CMUX_CUA_BINARY"/ { saw=1 } - END { exit !saw } -' "$CI_FILE"; then - echo "FAIL: release-build must check the built binaries against the exact resolved architectures" - exit 1 -fi if grep -n -E 'CI_RELEASE_BUILD_ARCHS|release-build-archs\.sh|release_archs' "$NIGHTLY_FILE"; then echo "FAIL: nightly builds what ships and must stay universal unconditionally" exit 1 fi +python3 "$ROOT_DIR/tests/test_ci_release_helper_archs.py" + echo "PASS: the CI Release check defaults to universal, arm64 is opt-in, and nightly cannot be narrowed" diff --git a/tests/test_ci_release_helper_archs.py b/tests/test_ci_release_helper_archs.py new file mode 100644 index 00000000000..18a44f3b77a --- /dev/null +++ b/tests/test_ci_release_helper_archs.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""Run the Release helper handoff with fake compilers and real installers/checks.""" +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = yaml.safe_load((ROOT / '.github/workflows/ci.yml').read_text()) + + +def expression(text, context): + # The exercised workflow uses dotted outputs and the standard input override. + if ' || ' in text: + for part in text.split(' || '): + value = expression(part, context) + if value: + return value + return value + if ' && ' in text: + value = True + for part in text.split(' && '): + if not value: + return value + value = expression(part, context) + return value + if ' != ' in text: + a, b = text.split(' != ', 1) + return expression(a, context) != expression(b, context) + if text.startswith("'") and text.endswith("'"): + return text[1:-1] + value = context + for part in text.strip().split('.'): + value = value.get(part, {}) + return value if not isinstance(value, dict) else '' + + +def render(value, context): + return re.sub(r'\$\{\{\s*(.*?)\s*\}\}', lambda m: str(expression(m[1], context)), str(value)) + + +class ReleaseHelperArchitectures(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='cmux-release-arch-') + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.bin = self.root / 'bin' + self.bin.mkdir() + self.app = self.root / 'build-universal/Build/Products/Release/cmux.app' + for name in ['scripts/ci/release-build-archs.sh', 'scripts/ci/verify-binary-archs.sh', + 'scripts/install-prebuilt-ghostty-cli-helper.sh']: + dest = self.root / name + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / name, dest) + dest.chmod(0o755) + self.tool('scripts/build-ghostty-cli-helper.sh', '''#!/bin/bash +set -eu +archs=""; out="" +while (( $# )); do + case "$1" in + --universal) archs="arm64 x86_64" ;; + --target) shift; [[ "$1" == aarch64-macos ]] || exit 1; archs=arm64 ;; + --output) shift; out="$1" ;; + *) exit 2 ;; + esac + shift +done +printf '%s\\n' "$archs" > "$out" +chmod +x "$out" +''') + self.tool('bin/lipo', '''#!/bin/bash +set -eu +if [[ "$1" == -archs ]]; then cat "$2"; exit; fi +file="$1"; shift +case "$1" in + -thin) + # Real lipo rejects -thin on a non-fat input. + [[ "$(cat "$file")" == *" "* ]] || exit 1 + arch="$2"; [[ " $(cat "$file") " == *" $arch "* ]] || exit 1 + [[ "$3" == -output ]] || exit 2; printf '%s\\n' "$arch" > "$4" ;; + -verify_arch) + shift + for arch in "$@"; do [[ " $(cat "$file") " == *" $arch "* ]] || exit 1; done ;; + *) exit 2 ;; +esac +''') + self.tool('bin/otool', '''#!/bin/bash +case "$2" in *ghostty-cli-helper*) sdk=15.5 ;; *) sdk=26.3 ;; esac +printf 'cmd LC_BUILD_VERSION\\n sdk %s\\n' "$sdk" +''') + self.tool('bin/xcodebuild', '''#!/bin/bash +set -eu +archs="" +for arg in "$@"; do case "$arg" in ARCHS=*) archs="${arg#ARCHS=}" ;; esac; done +[[ -n "$archs" ]] || exit 1 +app=build-universal/Build/Products/Release/cmux.app +mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources/bin" +for file in Contents/MacOS/cmux Contents/Resources/bin/cmux Contents/Resources/bin/cmux-cua Contents/Resources/bin/cmux-diff-sidecar; do + printf '%s\\n' "$archs" > "$app/$file"; chmod +x "$app/$file" +done +''') + self.tool('bin/codesign', '#!/bin/bash\nexit 0\n') + self.tool('scripts/verify-diff-sidecar-artifact.sh', '#!/bin/bash\nexit 0\n') + self.tool('tests/test_install_cmux_tui_client.sh', '#!/bin/bash\nexit 0\n') + self.tool('scripts/install-cmux-tui-client.sh', '''#!/bin/bash +set -eu +app="$1"; shift +[[ $# == 2 && "$1" == --arch ]] || exit 2 +case "$2" in arm64) archs=arm64 ;; universal) archs="arm64 x86_64" ;; *) exit 2 ;; esac +printf '%s\\n' "$archs" > "$app/Contents/Resources/bin/cmux-tui" +chmod +x "$app/Contents/Resources/bin/cmux-tui" +''') + self.env = dict(os.environ, PATH=str(self.bin) + ':' + os.environ['PATH']) + self.context = {'inputs': {'release_archs': 'default'}, 'vars': {'CI_RELEASE_BUILD_ARCHS': ''}, + 'steps': {}, 'needs': {}, 'github': {'token': 'fixture-no-token'}} + + def tool(self, name, content): + dest = self.root / name + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(content) + dest.chmod(0o755) + + def step(self, job, *, name=None, identifier=None): + matches = [s for s in WORKFLOW['jobs'][job]['steps'] + if (s.get('id') == identifier if identifier else name(s.get('name', '')))] + self.assertEqual(len(matches), 1) + return matches[0] + + def run_step(self, step): + output = self.root / 'github-output' + output.write_text('') + env = dict(self.env, GITHUB_OUTPUT=str(output)) + env.update({k: render(v, self.context) for k, v in step.get('env', {}).items()}) + result = subprocess.run(['/bin/bash', '-e', '-c', step['run']], cwd=self.root, + env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + self.assertEqual(result.returncode, 0, result.stdout) + if step.get('id'): + self.context['steps'][step['id']] = {'outputs': dict(line.split('=', 1) + for line in output.read_text().splitlines() if '=' in line)} + return result + + def produce(self, setting='', dispatch='default'): + self.context['vars']['CI_RELEASE_BUILD_ARCHS'] = setting + self.context['inputs']['release_archs'] = dispatch + self.run_step(self.step('swift-package-tests', identifier='release-archs')) + self.run_step(self.step('swift-package-tests', name=lambda n: n.startswith('Build ') and n.endswith('Ghostty CLI helper'))) + outputs = {k: render(v, self.context) for k, v in WORKFLOW['jobs']['swift-package-tests'].get('outputs', {}).items()} + self.context['needs']['swift-package-tests'] = {'outputs': outputs} + # Model a distinct consumer: producer step outputs are not in scope. + self.context['steps'] = {} + + def consume(self): + self.run_step(self.step('release-build', name=lambda n: n == 'Build app (Release)')) + self.run_step(self.step('release-build', name=lambda n: n.startswith('Install ') and ('Ghostty' in n or 'helpers' in n))) + self.run_step(self.step('release-build', name=lambda n: n == 'Validate Release artifact slices')) + + def test_architecture_policy_flows_through_producer_and_consumer(self): + for setting, dispatch, expected in [('', 'default', 'arm64 x86_64'), ('arm64', 'default', 'arm64'), + ('arm64', 'universal', 'arm64 x86_64'), ('universal', 'arm64', 'arm64')]: + with self.subTest(setting=setting, dispatch=dispatch): + self.produce(setting, dispatch) + self.consume() + for file in ['Contents/MacOS/cmux', 'Contents/Resources/bin/ghostty', 'Contents/Resources/bin/cmux-tui']: + self.assertEqual((self.app / file).read_text().strip(), expected) + + def test_wrong_architecture_helper_is_rejected(self): + self.produce('arm64') + (self.root / 'ghostty-cli-helper/ghostty').write_text('arm64 x86_64\n') + with self.assertRaises(AssertionError): + self.consume() + + def test_missing_producer_policy_fails_closed(self): + self.produce('arm64') + self.context['needs']['swift-package-tests']['outputs'] = {} + with self.assertRaises(AssertionError): + self.consume() + self.assertFalse(self.app.exists()) + + def test_invalid_policy_rejects_before_building(self): + with self.assertRaises(AssertionError): + self.produce('invalid') + self.assertFalse((self.root / 'ghostty-cli-helper/ghostty').exists()) + + def test_installer_default_still_requires_universal(self): + self.app.joinpath('Contents').mkdir(parents=True) + helper = self.root / 'helper' + for archs, success in [('arm64 x86_64', True), ('arm64', False)]: + helper.write_text(archs + '\n') + result = subprocess.run(['/bin/bash', str(self.root / 'scripts/install-prebuilt-ghostty-cli-helper.sh'), + str(helper), str(self.app)], env=self.env, capture_output=True) + self.assertEqual(result.returncode == 0, success) + + +if __name__ == '__main__': + unittest.main() From edac2108f1329006b2b4137fd2b1cd6006250cee Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 20 Sep 2026 11:13:56 -0700 Subject: [PATCH 2/4] ci: honor Release architectures for bundled helpers --- .github/workflows/ci.yml | 84 +++++++++++-------- .../install-prebuilt-ghostty-cli-helper.sh | 17 ++-- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ef3a2cb79a..342764abf7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2197,6 +2197,8 @@ jobs: # transitive skip otherwise marks every macOS job skipped even when # linux-preflight itself succeeds. Require the direct needs explicitly. if: ${{ !cancelled() && needs.changes.result == 'success' && needs.linux-preflight.result == 'success' && needs.changes.outputs.macos == 'true' && needs.changes.outputs.full_suite == 'true' }} + outputs: + release_archs: ${{ steps.release-archs.outputs.archs }} # Build the release helper with SDK 15, then run package tests with SDK 26. runs-on: ${{ vars.MACOS_RUNNER_DUAL_XCODE || 'blacksmith-6vcpu-macos-15' }} timeout-minutes: 40 @@ -2225,8 +2227,21 @@ jobs: # "Select package tests" diffs against. fetch-depth: 2 + # Resolve once for the helper producer and Release consumer. Nightly + # still builds the shipped universal app independently of this policy. + - name: Resolve Release check architectures + id: release-archs + if: ${{ needs.changes.outputs.release_build == 'true' }} + env: + REQUESTED_ARCHS: ${{ inputs.release_archs != 'default' && inputs.release_archs || vars.CI_RELEASE_BUILD_ARCHS }} + run: | + set -euo pipefail + archs="$(./scripts/ci/release-build-archs.sh "$REQUESTED_ARCHS")" + echo "Release check architectures: $archs" + echo "archs=$archs" >> "$GITHUB_OUTPUT" + # Only release-build consumes this artifact. Package/app-host tests use - # the prebuilt GhosttyKit framework and do not need the universal CLI. + # the prebuilt GhosttyKit framework and do not need the Release CLI. - name: Select helper Xcode if: ${{ needs.changes.outputs.release_build == 'true' }} run: | @@ -2247,22 +2262,32 @@ jobs: key: zig-packages-${{ hashFiles('ghostty/build.zig.zon', 'ghostty/build.zig.zon.json') }} restore-keys: zig-packages- - - name: Build universal Ghostty CLI helper + - name: Build Release Ghostty CLI helper if: ${{ needs.changes.outputs.release_build == 'true' }} + env: + RELEASE_ARCHS: ${{ steps.release-archs.outputs.archs }} run: | set -euo pipefail mkdir -p ghostty-cli-helper - ./scripts/build-ghostty-cli-helper.sh --universal --output ghostty-cli-helper/ghostty - lipo ghostty-cli-helper/ghostty -verify_arch arm64 x86_64 - for arch in arm64 x86_64; do - thin="ghostty-cli-helper/ghostty-$arch" - lipo ghostty-cli-helper/ghostty -thin "$arch" -output "$thin" + case "$RELEASE_ARCHS" in + arm64) helper_args=(--target aarch64-macos) ;; + "arm64 x86_64") helper_args=(--universal) ;; + *) echo "unsupported Release helper architectures: $RELEASE_ARCHS" >&2; exit 1 ;; + esac + ./scripts/build-ghostty-cli-helper.sh "${helper_args[@]}" --output ghostty-cli-helper/ghostty + ./scripts/ci/verify-binary-archs.sh "$RELEASE_ARCHS" ghostty-cli-helper/ghostty + for arch in $RELEASE_ARCHS; do + thin="ghostty-cli-helper/ghostty" + if [[ "$RELEASE_ARCHS" == "arm64 x86_64" ]]; then + thin="ghostty-cli-helper/ghostty-$arch" + lipo ghostty-cli-helper/ghostty -thin "$arch" -output "$thin" + fi HELPER_SDK_VERSION="$(otool -l "$thin" | awk '/LC_BUILD_VERSION/ { in_version=1; next } in_version && /sdk / { print $2; exit }')" echo "Ghostty helper $arch SDK version: $HELPER_SDK_VERSION" [[ "$HELPER_SDK_VERSION" == 15.* ]] done - - name: Upload universal Ghostty CLI helper + - name: Upload Release Ghostty CLI helper if: ${{ needs.changes.outputs.release_build == 'true' }} id: upload-ghostty-cli-helper continue-on-error: true @@ -2273,7 +2298,7 @@ jobs: if-no-files-found: error retention-days: 1 - - name: Retry universal Ghostty CLI helper upload + - name: Retry Release Ghostty CLI helper upload if: ${{ needs.changes.outputs.release_build == 'true' && steps.upload-ghostty-cli-helper.outcome == 'failure' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -3205,24 +3230,15 @@ jobs: go-version: '1.26.x' cache: false - # Nightly always builds the shipped universal app. A maintainer can set - # CI_RELEASE_BUILD_ARCHS=arm64 to drop the Intel whole-module compile from - # this pre-merge check, leaving Intel-only compile breaks to nightly. - - name: Resolve Release check architectures - id: release-archs - env: - REQUESTED_ARCHS: ${{ inputs.release_archs != 'default' && inputs.release_archs || vars.CI_RELEASE_BUILD_ARCHS }} - run: | - set -euo pipefail - archs="$(./scripts/ci/release-build-archs.sh "$REQUESTED_ARCHS")" - echo "Release check architectures: $archs" - echo "archs=$archs" >> "$GITHUB_OUTPUT" - - name: Build app (Release) env: - RELEASE_ARCHS: ${{ steps.release-archs.outputs.archs }} + RELEASE_ARCHS: ${{ needs.swift-package-tests.outputs.release_archs }} run: | set -euo pipefail + case "$RELEASE_ARCHS" in + arm64|"arm64 x86_64") ;; + *) echo "missing or invalid producer architectures: $RELEASE_ARCHS" >&2; exit 1 ;; + esac CMUX_SKIP_ZIG_BUILD=1 xcodebuild -project cmux.xcodeproj -scheme cmux -configuration Release -derivedDataPath build-universal \ -destination 'generic/platform=macOS' \ -clonedSourcePackagesDirPath .spm-cache \ @@ -3233,14 +3249,15 @@ jobs: COMPILER_INDEX_STORE_ENABLE=NO \ CODE_SIGNING_ALLOWED=NO ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly build - - name: Download universal Ghostty CLI helper + - name: Download Release Ghostty CLI helper uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: cmux-ghostty-cli-helper path: ghostty-cli-helper - - name: Install universal Ghostty CLI helper + - name: Install Release helpers env: + RELEASE_ARCHS: ${{ needs.swift-package-tests.outputs.release_archs }} # install-cmux-tui-client.sh verifies the manifest's build-provenance # attestation with gh before trusting it. GH_TOKEN: ${{ github.token }} @@ -3249,12 +3266,17 @@ jobs: /bin/bash ./tests/test_install_cmux_tui_client.sh ./scripts/install-prebuilt-ghostty-cli-helper.sh \ ghostty-cli-helper/ghostty \ - build-universal/Build/Products/Release/cmux.app - ./scripts/install-cmux-tui-client.sh build-universal/Build/Products/Release/cmux.app + build-universal/Build/Products/Release/cmux.app --archs "$RELEASE_ARCHS" + case "$RELEASE_ARCHS" in + arm64) client_arch=arm64 ;; + "arm64 x86_64") client_arch=universal ;; + *) echo "unsupported Release client architectures: $RELEASE_ARCHS" >&2; exit 1 ;; + esac + ./scripts/install-cmux-tui-client.sh build-universal/Build/Products/Release/cmux.app --arch "$client_arch" - name: Validate Release artifact slices env: - RELEASE_ARCHS: ${{ steps.release-archs.outputs.archs }} + RELEASE_ARCHS: ${{ needs.swift-package-tests.outputs.release_archs }} run: | set -euo pipefail APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux" @@ -3266,17 +3288,13 @@ jobs: test -x "$CLI_BINARY" TUI_CLIENT="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux-tui" test -x "$TUI_CLIENT" - lipo "$TUI_CLIENT" -verify_arch arm64 x86_64 test -x "$HELPER_BINARY" test -x "$CMUX_CUA_BINARY" test -x "$DIFF_SIDECAR" file "$APP_BINARY" "$CLI_BINARY" "$HELPER_BINARY" "$CMUX_CUA_BINARY" "$DIFF_SIDECAR" SDK_VERSION="$(otool -l "$APP_BINARY" | awk '/LC_BUILD_VERSION/ { in_version=1; next } in_version && /sdk / { print $2; exit }')" echo "App SDK version: $SDK_VERSION" - # The helper and the TUI client are prebuilt downloads, so they are - # universal whichever architectures this check compiled. - lipo "$HELPER_BINARY" -verify_arch arm64 x86_64 - ./scripts/ci/verify-binary-archs.sh "$RELEASE_ARCHS" "$APP_BINARY" "$CLI_BINARY" "$CMUX_CUA_BINARY" + ./scripts/ci/verify-binary-archs.sh "$RELEASE_ARCHS" "$APP_BINARY" "$CLI_BINARY" "$CMUX_CUA_BINARY" "$HELPER_BINARY" "$TUI_CLIENT" codesign --verify --strict --verbose=4 "$CMUX_CUA_BINARY" ./scripts/verify-diff-sidecar-artifact.sh "$DIFF_SIDECAR" --archs "$RELEASE_ARCHS" [[ "$SDK_VERSION" == 26.* ]] diff --git a/scripts/install-prebuilt-ghostty-cli-helper.sh b/scripts/install-prebuilt-ghostty-cli-helper.sh index 2042b526b19..9b2e95748ff 100755 --- a/scripts/install-prebuilt-ghostty-cli-helper.sh +++ b/scripts/install-prebuilt-ghostty-cli-helper.sh @@ -3,17 +3,22 @@ set -euo pipefail usage() { cat <<'EOF' -usage: scripts/install-prebuilt-ghostty-cli-helper.sh +usage: scripts/install-prebuilt-ghostty-cli-helper.sh [--archs "arm64 x86_64"] EOF } -if [[ $# -ne 2 ]]; then +if [[ $# -ne 2 && ( $# -ne 4 || "${3:-}" != --archs ) ]]; then usage >&2 exit 1 fi HELPER_PATH="$1" APP_PATH="$2" +EXPECTED_ARCHS="${4-arm64 x86_64}" +case "$EXPECTED_ARCHS" in + arm64|x86_64|"arm64 x86_64"|"x86_64 arm64") ;; + *) echo "error: unsupported helper architectures: $EXPECTED_ARCHS" >&2; exit 1 ;; +esac DEST_PATH="$APP_PATH/Contents/Resources/bin/ghostty" if [[ ! -f "$HELPER_PATH" ]]; then @@ -29,8 +34,6 @@ fi mkdir -p "$(dirname "$DEST_PATH")" install -m 755 "$HELPER_PATH" "$DEST_PATH" -# One arch per invocation: some lipo builds (Xcode 27 beta 4) consume only one -# arch after -verify_arch and read the second as an extra input file, failing -# with "requires exactly one input file". -for arch in arm64 x86_64; do lipo "$DEST_PATH" -verify_arch "$arch"; done -echo "Installed universal Ghostty CLI helper at $DEST_PATH" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +"$SCRIPT_DIR/ci/verify-binary-archs.sh" "$EXPECTED_ARCHS" "$DEST_PATH" +echo "Installed Ghostty CLI helper ($EXPECTED_ARCHS) at $DEST_PATH" From 01c294ea9585b5473f076c5b814b23948d16835a Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 20 Sep 2026 11:34:30 -0700 Subject: [PATCH 3/4] test(ci): align helper guards with Release architecture policy --- tests/test_ci_release_sdk_lane.sh | 8 ++++---- tests/test_ci_self_hosted_guard.sh | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_ci_release_sdk_lane.sh b/tests/test_ci_release_sdk_lane.sh index ffdaac1bb85..5ae1ca058e5 100755 --- a/tests/test_ci_release_sdk_lane.sh +++ b/tests/test_ci_release_sdk_lane.sh @@ -90,8 +90,8 @@ if [[ "$swift_package_section" == *"/Applications/Xcode_16.4.app"* ]]; then exit 1 fi -if [[ "$swift_package_section" != *"./scripts/build-ghostty-cli-helper.sh --universal --output ghostty-cli-helper/ghostty"* ]]; then - echo "FAIL: CI swift-package-tests must build the universal Ghostty CLI helper on the macOS 15 lane" >&2 +if [[ "$swift_package_section" != *'./scripts/build-ghostty-cli-helper.sh "${helper_args[@]}" --output ghostty-cli-helper/ghostty'* ]]; then + echo "FAIL: CI swift-package-tests must build the architecture-selected Ghostty CLI helper on the macOS 15 lane" >&2 exit 1 fi @@ -106,7 +106,7 @@ if [[ "$swift_package_before_xcode" != *"CMUX_CI_REQUIRED_MACOS_SDK_MAJOR=15"* ] exit 1 fi -if [[ "$swift_package_before_xcode" != *"./scripts/build-ghostty-cli-helper.sh --universal --output ghostty-cli-helper/ghostty"* ]]; then +if [[ "$swift_package_before_xcode" != *'./scripts/build-ghostty-cli-helper.sh "${helper_args[@]}" --output ghostty-cli-helper/ghostty'* ]]; then echo "FAIL: CI swift-package-tests must build the Ghostty helper before selecting the Xcode 26 SDK" >&2 exit 1 fi @@ -132,7 +132,7 @@ if [[ "$release_build_section" != *"- swift-package-tests"* ]]; then exit 1 fi -if [[ "$release_build_section" == *"./scripts/build-ghostty-cli-helper.sh --universal --output ghostty-cli-helper/ghostty"* ]]; then +if [[ "$release_build_section" == *"./scripts/build-ghostty-cli-helper.sh"* ]]; then echo "FAIL: CI release-build must not build the Ghostty helper on macOS 26" >&2 exit 1 fi diff --git a/tests/test_ci_self_hosted_guard.sh b/tests/test_ci_self_hosted_guard.sh index 296a59f3cdc..0920a172d9f 100755 --- a/tests/test_ci_self_hosted_guard.sh +++ b/tests/test_ci_self_hosted_guard.sh @@ -215,8 +215,8 @@ check_release_build_signal() { exit 1 fi - if ! grep -Fq 'lipo "$HELPER_BINARY" -verify_arch arm64 x86_64' "$CI_FILE"; then - echo "FAIL: release-build must verify the bundled Ghostty helper stays universal" + if ! grep -Fq './scripts/ci/verify-binary-archs.sh "$RELEASE_ARCHS" "$APP_BINARY" "$CLI_BINARY" "$CMUX_CUA_BINARY" "$HELPER_BINARY" "$TUI_CLIENT"' "$CI_FILE"; then + echo "FAIL: release-build must verify both bundled helpers contain exactly the producer-selected architectures" exit 1 fi @@ -255,16 +255,16 @@ check_release_helper_artifact_from_package_lane() { in_job && /- name: Select helper Xcode/ { saw_helper_select=1; next } in_job && /CMUX_CI_REQUIRED_MACOS_SDK_MAJOR=15/ { saw_helper_sdk_pin=1 } in_job && /- name: Select Xcode/ { saw_select=1; after_select=1; next } - in_job && /- name: Build universal Ghostty CLI helper/ { + in_job && /- name: Build Release Ghostty CLI helper/ { saw_build_step=1 if (after_select) { saw_build_after_select=1 } next } - in_job && /\.\/scripts\/build-ghostty-cli-helper\.sh --universal --output ghostty-cli-helper\/ghostty/ { saw_build=1 } - in_job && /lipo ghostty-cli-helper\/ghostty -verify_arch arm64 x86_64/ { saw_lipo=1 } - in_job && /- name: Upload universal Ghostty CLI helper/ { + in_job && index($0, "./scripts/build-ghostty-cli-helper.sh \"${helper_args[@]}\" --output ghostty-cli-helper/ghostty") { saw_build=1 } + in_job && /\.\/scripts\/ci\/verify-binary-archs\.sh "\$RELEASE_ARCHS" ghostty-cli-helper\/ghostty/ { saw_arch_validation=1 } + in_job && /- name: Upload Release Ghostty CLI helper/ { saw_upload_step=1 if (after_select) { saw_upload_after_select=1 @@ -276,7 +276,7 @@ check_release_helper_artifact_from_package_lane() { in_job && /\[\[ "\$HELPER_SDK_VERSION" == 15\.\* \]\]/ { saw_helper_sdk_validation=1 } END { - exit !(saw_dual_runner && saw_timeout && saw_helper_xcode_env && saw_helper_select && saw_helper_sdk_pin && saw_build_step && saw_build && saw_lipo && saw_helper_sdk_validation && saw_upload_step && saw_upload && saw_artifact_name && saw_select && !saw_build_after_select && !saw_upload_after_select) + exit !(saw_dual_runner && saw_timeout && saw_helper_xcode_env && saw_helper_select && saw_helper_sdk_pin && saw_build_step && saw_build && saw_arch_validation && saw_helper_sdk_validation && saw_upload_step && saw_upload && saw_artifact_name && saw_select && !saw_build_after_select && !saw_upload_after_select) } ' "$CI_FILE"; then echo "FAIL: swift-package-tests must use the dual-Xcode runner, then pin and validate the macOS 15 Ghostty helper before selecting Xcode 26" @@ -288,10 +288,10 @@ check_release_helper_artifact_from_package_lane() { in_job && /^ [^[:space:]#][^:]*:[[:space:]]*(#.*)?$/ { in_job=0 } in_job && /- swift-package-tests/ { saw_need=1 } - in_job && /- name: Download universal Ghostty CLI helper/ { saw_download_step=1; next } + in_job && /- name: Download Release Ghostty CLI helper/ { saw_download_step=1; next } in_job && /uses: actions\/download-artifact@/ { saw_download=1 } in_job && /name:[[:space:]]*cmux-ghostty-cli-helper/ { saw_artifact_name=1 } - in_job && /- name: Install universal Ghostty CLI helper/ { saw_install_step=1; next } + in_job && /- name: Install Release helpers/ { saw_install_step=1; next } in_job && /\.\/scripts\/install-prebuilt-ghostty-cli-helper\.sh/ { saw_install=1 } END { From 11e4575fa51f09186199f13caff7528f6434a003 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 20 Sep 2026 13:53:19 -0700 Subject: [PATCH 4/4] ci: track architecture helpers as package test inputs --- scripts/ci/select_package_tests.py | 2 ++ 1 file changed, 2 insertions(+) mode change 100755 => 100644 scripts/ci/select_package_tests.py diff --git a/scripts/ci/select_package_tests.py b/scripts/ci/select_package_tests.py old mode 100755 new mode 100644 index db71d97c660..2d2d9fdae4c --- a/scripts/ci/select_package_tests.py +++ b/scripts/ci/select_package_tests.py @@ -27,9 +27,11 @@ GLOBAL_INPUTS = ( ".github/workflows/ci.yml", "scripts/build-ghostty-cli-helper.sh", + "scripts/ci/release-build-archs.sh", "scripts/ci/run-swift-testing-suites.sh", "scripts/ci/run_with_timeout.py", "scripts/ci/select_package_tests.py", + "scripts/ci/verify-binary-archs.sh", "scripts/download-prebuilt-ghosttykit.sh", "scripts/install-rust-ci.sh", "scripts/install-zig-ci.sh",