diff --git a/.github/workflows/_ci-clients.yml b/.github/workflows/_ci-clients.yml index 310ab676b7..465c4b93ef 100644 --- a/.github/workflows/_ci-clients.yml +++ b/.github/workflows/_ci-clients.yml @@ -121,11 +121,16 @@ jobs: mobile-swift: name: Mobile Swift - runs-on: macos-latest + runs-on: xcode-27 timeout-minutes: 30 if: inputs.lane == 'mobile-swift' && inputs.mobile steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Select pinned Xcode + run: | + export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer" + scripts/check-xcode.sh + echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV" - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - name: Install Flutter dependencies run: cd mobile && flutter pub get diff --git a/.github/workflows/_ci-desktop-macos.yml b/.github/workflows/_ci-desktop-macos.yml index d19ffa72eb..fc1038dfc2 100644 --- a/.github/workflows/_ci-desktop-macos.yml +++ b/.github/workflows/_ci-desktop-macos.yml @@ -23,18 +23,24 @@ env: jobs: desktop-build-macos: name: Desktop Build (macOS) - runs-on: macos-latest + runs-on: xcode-27 timeout-minutes: 45 if: github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust permissions: contents: read steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Select pinned Xcode + run: | + export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer" + scripts/check-xcode.sh + echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV" - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - name: Verify macOS icon representations run: scripts/macos-icon.sh --check - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 with: + key: xcode-${{ hashFiles('.xcode-version') }} workspaces: desktop/src-tauri save-if: ${{ github.event_name != 'pull_request' }} - name: Install desktop dependencies @@ -64,7 +70,7 @@ jobs: uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ${{ github.workspace }}/.cache/mesh-llama - key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }} + key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }} - name: Build mesh llama native libraries if: steps.llama_cache.outputs.cache-hit != 'true' env: @@ -88,7 +94,7 @@ jobs: uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ${{ github.workspace }}/.cache/mesh-llama - key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }} + key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }} - name: Build Tauri app run: cd desktop && pnpm tauri build env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1ff1c47fe..2f6f4293de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,8 @@ jobs: - '.github/workflows/_ci-desktop.yml' - 'scripts/ci-apt-retry.sh' - 'Justfile' + - '.xcode-version' + - 'scripts/check-xcode.sh' - 'scripts/generate-macos-icon.py' - 'scripts/macos-icon.sh' - 'bin/uv' @@ -86,6 +88,8 @@ jobs: - 'pnpm-lock.yaml' mobile: - 'mobile/**' + - '.xcode-version' + - 'scripts/check-xcode.sh' - 'scripts/mobile-release.sh' - 'scripts/mobile-worktree-overrides.sh' - 'scripts/mobile-worktree-clean.sh' @@ -108,7 +112,9 @@ jobs: scripts/test-postgres-test-discovery.sh scripts/test-postgres-test-wrapper.sh - name: Release workflow source contract - run: scripts/test-release-ref-contract.sh + run: | + scripts/test-release-ref-contract.sh + scripts/test-xcode-version.sh - name: Relay image eligibility contract run: scripts/test-relay-image-eligibility-workflow.sh - name: Desktop release candidate contract diff --git a/.github/workflows/desktop-release-cache-proof.yml b/.github/workflows/desktop-release-cache-proof.yml index 461e25a60f..aaaac46472 100644 --- a/.github/workflows/desktop-release-cache-proof.yml +++ b/.github/workflows/desktop-release-cache-proof.yml @@ -12,7 +12,7 @@ jobs: macos: name: Prove macOS ${{ matrix.target }} cache visibility if: github.repository == 'block/buzz' - runs-on: macos-latest + runs-on: xcode-27 timeout-minutes: 15 strategy: fail-fast: false @@ -28,6 +28,11 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false + - name: Select pinned Xcode + run: | + export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer" + scripts/check-xcode.sh + echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV" - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - name: Patch proof dependency graph run: | diff --git a/.github/workflows/macos-intel-canary.yml b/.github/workflows/macos-intel-canary.yml index 35b05313c9..cc4ba34af2 100644 --- a/.github/workflows/macos-intel-canary.yml +++ b/.github/workflows/macos-intel-canary.yml @@ -13,7 +13,7 @@ jobs: build: name: Build macOS Intel canary if: github.repository == 'block/buzz' - runs-on: macos-latest + runs-on: xcode-27 timeout-minutes: 60 env: TARGET: x86_64-apple-darwin @@ -31,6 +31,11 @@ jobs: with: persist-credentials: false + - name: Select pinned Xcode + run: | + export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer" + scripts/check-xcode.sh + echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV" - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - name: Add Rust target diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 040817361a..330508662c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,7 +52,7 @@ jobs: release: name: Release if: github.repository == 'block/buzz' - runs-on: macos-latest + runs-on: xcode-27 needs: setup timeout-minutes: 60 permissions: @@ -70,6 +70,11 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Select pinned Xcode + run: | + export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer" + scripts/check-xcode.sh + echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV" - name: Verify tag-bound release source run: scripts/verify-release-ref.sh desktop-v "$VERSION" @@ -108,7 +113,7 @@ jobs: uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ${{ github.workspace }}/.cache/mesh-llama - key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }} + key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }} - name: Build mesh llama native libraries if: steps.llama_cache.outputs.cache-hit != 'true' env: @@ -132,7 +137,7 @@ jobs: uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ${{ github.workspace }}/.cache/mesh-llama - key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }} + key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }} - name: Build unsigned Tauri app run: cd desktop && pnpm tauri build --verbose --no-sign --features mesh-llm --config src-tauri/tauri.release.conf.json @@ -265,7 +270,7 @@ jobs: release-macos-x64: name: Release macOS (Intel) if: github.repository == 'block/buzz' - runs-on: macos-latest + runs-on: xcode-27 needs: setup timeout-minutes: 60 permissions: @@ -284,6 +289,11 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Select pinned Xcode + run: | + export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer" + scripts/check-xcode.sh + echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV" - name: Verify tag-bound release source run: scripts/verify-release-ref.sh desktop-v "$VERSION" diff --git a/.github/workflows/signed-macos-canary.yml b/.github/workflows/signed-macos-canary.yml index 5957f4785d..eaf091049f 100644 --- a/.github/workflows/signed-macos-canary.yml +++ b/.github/workflows/signed-macos-canary.yml @@ -13,7 +13,7 @@ jobs: build: name: Build signed macOS canary if: github.repository == 'block/buzz' - runs-on: macos-latest + runs-on: xcode-27 timeout-minutes: 60 permissions: contents: read @@ -32,6 +32,11 @@ jobs: with: persist-credentials: false + - name: Select pinned Xcode + run: | + export DEVELOPER_DIR="/Applications/Xcode_$(cat .xcode-version).app/Contents/Developer" + scripts/check-xcode.sh + echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV" - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - name: Get pnpm store directory @@ -133,7 +138,7 @@ jobs: uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ${{ github.workspace }}/.cache/mesh-llama - key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }} + key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }} - name: Build mesh llama native libraries if: steps.llama_cache.outputs.cache-hit != 'true' @@ -158,7 +163,7 @@ jobs: uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ${{ github.workspace }}/.cache/mesh-llama - key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }} + key: mesh-llama-${{ runner.os }}-metal-${{ hashFiles('.xcode-version') }}-${{ steps.mesh_rev.outputs.rev }} - name: Build unsigned Tauri app run: cd desktop && pnpm tauri build --verbose --no-sign --features mesh-llm --config src-tauri/tauri.canary.conf.json diff --git a/.xcode-version b/.xcode-version new file mode 100644 index 0000000000..008c39a438 --- /dev/null +++ b/.xcode-version @@ -0,0 +1 @@ +27.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbe4ba5dd2..b20433b992 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,6 +102,29 @@ Hermit pins Rust, `just`, Node, pnpm, and other tools to the versions in upfront. If you don't use Hermit, ensure your toolchain meets the minimum versions in the table above. +#### macOS: Xcode + +Apple builds use the exact version in [`.xcode-version`](.xcode-version), +currently Xcode 27.0.0. Install that version and set `DEVELOPER_DIR` to its +`Xcode.app/Contents/Developer` directory for your shell. For example: + +```bash +export DEVELOPER_DIR=/Applications/Xcode_27.app/Contents/Developer +scripts/check-xcode.sh +``` + +Local Tauri build/dev commands and native `just` recipes reject a different +active version. Direct Cargo, Swift, Flutter, or Xcode invocations should run +`scripts/check-xcode.sh` first in the same environment. Nothing installs Xcode +or changes the system-wide `xcode-select` setting automatically. Xcode 27 +requires macOS 26.6 or later. + +GitHub Apple jobs use the `xcode-27` runner image and select the file's exact +version before compiling. That runner image is currently a GitHub public +preview, even though the selected Xcode 27.0 compiler is stable. When changing +the pin, verify runner availability and coordinate the internal release +pipeline's scheduling pin in `squareup/buzz-releases`. + #### Linux: Tauri system libraries Hermit pins language toolchains, not system libraries. On Linux, the desktop diff --git a/Justfile b/Justfile index 04c6e46e70..69edf3e155 100644 --- a/Justfile +++ b/Justfile @@ -220,25 +220,30 @@ _ensure-migrations: _ensure-services # the default graph covers the `#[cfg(not(feature = "mesh-llm"))]` arms and # the feature-enabled graph covers the mesh code. desktop-tauri-clippy: _ensure-sidecar-stubs + ./scripts/check-xcode.sh cargo clippy --manifest-path {{desktop_tauri_manifest}} --workspace --all-targets -- -D warnings cargo clippy --manifest-path {{desktop_tauri_manifest}} --workspace --all-targets --features mesh-llm -- -D warnings # Check the desktop Tauri Rust crate compiles desktop-tauri-check: _ensure-sidecar-stubs + ./scripts/check-xcode.sh cargo check --manifest-path {{desktop_tauri_manifest}} # Run desktop Tauri Rust unit tests desktop-tauri-test: _ensure-sidecar-stubs + ./scripts/check-xcode.sh cd desktop/src-tauri && cargo test --workspace # Run the desktop Tauri lib tests with the mesh-llm feature graph desktop-tauri-test-mesh: _ensure-sidecar-stubs + ./scripts/check-xcode.sh cargo test --manifest-path {{desktop_tauri_manifest}} --features mesh-llm --lib # Run the native terminal latency gate explicitly on a known-idle host. # This is intentionally excluded from shared CI: scheduler contention makes a # wall-clock assertion flaky, and the release profile is the shipped shape. desktop-terminal-performance-test: + ./scripts/check-xcode.sh cargo test --manifest-path desktop/src-tauri/crates/buzz-terminal/Cargo.toml --release --test latency g3_renderer_acquire_stays_within_frame_budget -- --ignored --exact --nocapture # Verify compiled-flag behavior in each compile state. @@ -255,6 +260,7 @@ desktop-terminal-performance-test: desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs #!/usr/bin/env bash set -euo pipefail + ./scripts/check-xcode.sh cd desktop/src-tauri unset BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY BUZZ_BUILD_DEMO_SLUG unset BUZZ_TEST_EXPECTED_AUTO_CONNECT_DEFAULT_RELAY BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY BUZZ_TEST_EXPECTED_DEMO_SLUG @@ -314,6 +320,7 @@ desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs desktop-release-build target="aarch64-apple-darwin": #!/usr/bin/env bash set -euo pipefail + ./scripts/check-xcode.sh TARGET={{target}} mkdir -p desktop/src-tauri/binaries touch "desktop/src-tauri/binaries/buzz-acp-$TARGET" @@ -331,6 +338,7 @@ desktop-release-build target="aarch64-apple-darwin": desktop-demo-build demo_name target="aarch64-apple-darwin": #!/usr/bin/env bash set -euo pipefail + ./scripts/check-xcode.sh TARGET={{target}} [[ "$(uname -s)" == "Darwin" && "$TARGET" == *-apple-darwin ]] || { echo "Demo DMGs require a macOS Apple target" >&2; exit 2; } CONFIG_PATH="$(mktemp "${TMPDIR:-/tmp}/buzz-demo-config.XXXXXX")" @@ -1015,6 +1023,7 @@ mobile-build-android: mobile-dev: #!/usr/bin/env bash set -euo pipefail + ./scripts/check-xcode.sh if ! pgrep -x Simulator &>/dev/null; then open -a Simulator sleep 3 diff --git a/desktop/scripts/tauri-command.mjs b/desktop/scripts/tauri-command.mjs index bc53d607e7..49178359e9 100644 --- a/desktop/scripts/tauri-command.mjs +++ b/desktop/scripts/tauri-command.mjs @@ -31,6 +31,17 @@ function runTauri(args, options = {}) { } export function runTauriCommand(args) { + if (process.platform === "darwin" && ["build", "dev"].includes(args[0])) { + const check = spawnSync( + "bash", + [path.join(desktopRoot, "../scripts/check-xcode.sh")], + { + stdio: "inherit", + }, + ); + if (check.error) throw check.error; + if (check.status !== 0) return check.status ?? 1; + } if (args[0] !== "build") return runTauri(args); // Tauri runs beforeBuildCommand and then consumes frontendDist. Give the diff --git a/scripts/check-xcode.sh b/scripts/check-xcode.sh new file mode 100755 index 0000000000..50f69d8d85 --- /dev/null +++ b/scripts/check-xcode.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Validate the active Apple toolchain without changing xcode-select globally. +set -euo pipefail +[[ "$(uname -s)" == Darwin ]] || exit 0 +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +expected="$(cat "$root/.xcode-version")" +[[ "$expected" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "Invalid .xcode-version: expected a major.minor.patch version" >&2 + exit 1 +} +if ! version="$(xcodebuild -version)"; then + echo "Buzz requires Xcode $expected. Set DEVELOPER_DIR to its Xcode.app/Contents/Developer." >&2 + exit 1 +fi +actual="$(printf '%s\n' "$version" | sed -n 's/^Xcode //p')" +[[ "$actual" == *.* ]] || actual="$actual.0" +[[ "$actual" == *.*.* ]] || actual="$actual.0" +if [[ "$actual" != "$expected" ]]; then + echo "Buzz requires Xcode $expected (.xcode-version); active toolchain reports: $version" >&2 + echo "Set DEVELOPER_DIR to the matching Xcode.app/Contents/Developer and retry." >&2 + exit 1 +fi +printf '%s\n' "$version" diff --git a/scripts/test-mobile-gateway-recipes.sh b/scripts/test-mobile-gateway-recipes.sh index ae6741eb1a..111a75a23d 100644 --- a/scripts/test-mobile-gateway-recipes.sh +++ b/scripts/test-mobile-gateway-recipes.sh @@ -24,6 +24,7 @@ cat > "$fixture/scripts/mobile-worktree-overrides.sh" <<'STUB' exit 0 STUB chmod +x "$fixture/bin/flutter" "$fixture/bin/pgrep" "$fixture/scripts/mobile-worktree-overrides.sh" +cp "$fixture/scripts/mobile-worktree-overrides.sh" "$fixture/scripts/check-xcode.sh" export PATH="$fixture/bin:$PATH" export CALL_LOG="$fixture/arguments" unset BUZZ_PUSH_GATEWAY_URL diff --git a/scripts/test-xcode-version.sh b/scripts/test-xcode-version.sh new file mode 100755 index 0000000000..54e251f782 --- /dev/null +++ b/scripts/test-xcode-version.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Exercise version enforcement without installing or switching a real Xcode. +set -euo pipefail +root="$(cd "$(dirname "$0")/.." && pwd)" +fixture="$(mktemp -d)" +trap 'rm -rf "$fixture"' EXIT +mkdir -p "$fixture/scripts" "$fixture/bin" +cp "$root/scripts/check-xcode.sh" "$fixture/scripts/" +cp "$root/.xcode-version" "$fixture/" +cat > "$fixture/bin/uname" <<'STUB' +#!/usr/bin/env bash +printf '%s\n' "${TEST_OS:-Darwin}" +STUB +cat > "$fixture/bin/xcodebuild" <<'STUB' +#!/usr/bin/env bash +[[ "${TEST_XCODE:-}" != broken ]] || exit 1 +printf 'Xcode %s\nBuild version fixture\n' "$TEST_XCODE" +STUB +chmod +x "$fixture/bin/"* +export PATH="$fixture/bin:$PATH" +check() { "$fixture/scripts/check-xcode.sh" >/dev/null 2>&1; } +export TEST_XCODE=27.0 +check +export TEST_XCODE=27 +check +for TEST_XCODE in 26.6 27.1 '27.0 beta' broken ''; do + export TEST_XCODE + if check; then echo "Accepted wrong Xcode: $TEST_XCODE" >&2; exit 1; fi +done +printf 'latest\n' > "$fixture/.xcode-version" +if check; then echo 'Accepted invalid pin' >&2; exit 1; fi +TEST_OS=Linux check +printf 'Xcode version enforcement passed\n'