diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6582b05515ea..024b4c96d4a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -343,6 +343,9 @@ jobs: - name: Validate universal nightly workflow run: bash ./tests/test_nightly_universal_build.sh + - name: Validate test compilation cache seeding + run: bash ./tests/test_ci_test_compilation_cache_seed.sh + - name: Validate cmux-tui client installation run: bash ./tests/test_install_cmux_tui_client.sh @@ -2248,7 +2251,11 @@ jobs: persist-credentials: false - name: Prepare isolated admission DerivedData - run: echo "CMUX_COMPILE_ADMISSION_DERIVED_DATA=$RUNNER_TEMP/cmux-derived-data-compile-admission" >> "$GITHUB_ENV" + run: | + set -euo pipefail + echo "CMUX_COMPILE_ADMISSION_DERIVED_DATA=$RUNNER_TEMP/cmux-derived-data-compile-admission" >> "$GITHUB_ENV" + # Outside DerivedData: the resolve step below recreates that directory. + echo "CMUX_COMPILE_ADMISSION_CAS=$RUNNER_TEMP/cmux-compile-admission-cas" >> "$GITHUB_ENV" - name: Select Xcode run: ./scripts/select-ci-xcode.sh @@ -2284,44 +2291,35 @@ jobs: - name: Resolve Swift packages run: | set -euo pipefail - SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages" rm -rf "$CMUX_COMPILE_ADMISSION_DERIVED_DATA" - mkdir -p "$SOURCE_PACKAGES_DIR" "$CMUX_COMPILE_ADMISSION_DERIVED_DATA" + scripts/ci/compile-app-host-test-product.sh resolve \ + "$CMUX_COMPILE_ADMISSION_DERIVED_DATA" "$PWD/.ci-source-packages" - for attempt in 1 2 3; do - if xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \ - -derivedDataPath "$CMUX_COMPILE_ADMISSION_DERIVED_DATA" \ - -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ - -resolvePackageDependencies; then - if [ -d "$SOURCE_PACKAGES_DIR/artifacts/sparkle/Sparkle/Sparkle.xcframework" ] \ - && [ -d "$SOURCE_PACKAGES_DIR/artifacts/sentry-cocoa/Sentry/Sentry.xcframework" ]; then - exit 0 - fi - echo "Resolve succeeded but binary artifacts are missing; clearing and retrying" >&2 - rm -rf "$SOURCE_PACKAGES_DIR" - fi - if [ "$attempt" -eq 3 ]; then - echo "Failed to resolve Swift packages after 3 attempts" >&2 - exit 1 - fi - echo "Package resolution failed on attempt $attempt, retrying..." - sleep $((attempt * 5)) - done + - name: Compute test compilation cache key + id: compilation-cache-key + run: | + set -euo pipefail + echo "fingerprint=$(scripts/ci/compile-app-host-test-product.sh fingerprint "$CMUX_COMPILE_ADMISSION_DERIVED_DATA")" >> "$GITHUB_OUTPUT" + + # Read-only on purpose: nightly.yml `refresh-test-compilation-cache` is the + # only writer. A cache saved here would be scoped to this pull request and + # would spend the cache budget that keeps the main seed alive. + - name: Restore test compilation cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ env.CMUX_COMPILE_ADMISSION_CAS }} + key: xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}-${{ github.event.pull_request.base.sha || github.sha }} + restore-keys: | + xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}- - name: Compile app-host test product run: | set -euo pipefail - # shellcheck disable=SC2016 # Xcode expands $(inherited), not the shell - for scheme in cmux-unit cmux-numeric-locale; do - xcodebuild -project cmux.xcodeproj -scheme "$scheme" -configuration Debug \ - -derivedDataPath "$CMUX_COMPILE_ADMISSION_DERIVED_DATA" \ - -clonedSourcePackagesDirPath "$PWD/.ci-source-packages" \ - -disableAutomaticPackageResolution \ - -destination "platform=macOS" \ - 'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) CMUX_CI_APP_HOST_ISOLATION_REQUIRED' \ - 'LD_RUNPATH_SEARCH_PATHS=$(inherited) @executable_path/../Frameworks /private/tmp/cmux-app-host-package-frameworks' \ - build-for-testing 2>&1 | tee -a "$RUNNER_TEMP/cmux-compile-admission.txt" - done + scripts/ci/compile-app-host-test-product.sh build \ + "$CMUX_COMPILE_ADMISSION_DERIVED_DATA" \ + "$PWD/.ci-source-packages" \ + "$CMUX_COMPILE_ADMISSION_CAS" \ + "$RUNNER_TEMP/cmux-compile-admission.txt" - name: Package compiled app-host test product id: package-products diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3b684656ecba..df18bcef92bd 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -352,6 +352,132 @@ jobs: path: build-universal/CompilationCache.noindex key: xcode-compilation-release-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.toolchain }}-${{ needs.decide.outputs.head_sha }} + refresh-test-compilation-cache: + needs: decide + # The only writer of the Debug compilation cache that ci.yml + # `macos-compile-admission` restores. Scheduled, not per push: the seed + # mostly saves the package layer, which changes slowly, and a build per + # merge would take macOS slots from the pull request queue. + if: github.event_name == 'schedule' && github.event.schedule == '17 */6 * * *' + # Match the admission job's runner and Xcode. The cache key carries the + # toolchain and the build paths, so a mismatch is a miss, not a wrong hit. + runs-on: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }} + timeout-minutes: 75 + env: + CMUX_CI_XCODE_APP: ${{ vars.CMUX_CI_XCODE_APP_MACOS_15 }} + CMUX_CI_REQUIRED_MACOS_SDK_MAJOR: "26" + CMUX_SKIP_ZIG_BUILD: "1" + steps: + - name: Clear stale git locks (self-hosted reused workspace) + shell: bash + run: | + ws="${GITHUB_WORKSPACE:-$PWD}" + rm -f "$ws/.git/index.lock" 2>/dev/null || true + if [ -d "$ws/.git/modules" ]; then + find "$ws/.git/modules" -type f -name "*.lock" -delete 2>/dev/null || true + fi + + - name: Checkout cache ref + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + ref: ${{ needs.decide.outputs.head_sha }} + submodules: recursive + + - name: Prepare admission build paths + run: | + set -euo pipefail + # The same paths as ci.yml `macos-compile-admission`: they are part of + # every cache entry this job writes. + echo "CMUX_COMPILE_ADMISSION_DERIVED_DATA=$RUNNER_TEMP/cmux-derived-data-compile-admission" >> "$GITHUB_ENV" + echo "CMUX_COMPILE_ADMISSION_CAS=$RUNNER_TEMP/cmux-compile-admission-cas" >> "$GITHUB_ENV" + + - name: Select Xcode + run: ./scripts/select-ci-xcode.sh + + - name: Compute test compilation cache key + id: compilation-cache-key + run: | + set -euo pipefail + echo "fingerprint=$(scripts/ci/compile-app-host-test-product.sh fingerprint "$CMUX_COMPILE_ADMISSION_DERIVED_DATA")" >> "$GITHUB_OUTPUT" + + - name: Restore test compilation cache + id: compilation-cache-restore + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ env.CMUX_COMPILE_ADMISSION_CAS }} + key: xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}-${{ needs.decide.outputs.head_sha }} + restore-keys: | + xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}- + + - name: Install compilation dependencies + if: steps.compilation-cache-restore.outputs.cache-hit != 'true' + run: | + ./scripts/install-rust-ci.sh + ./scripts/download-prebuilt-ghosttykit.sh + + - name: Cache Swift packages + if: steps.compilation-cache-restore.outputs.cache-hit != 'true' + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: .ci-source-packages + key: spm-${{ hashFiles('cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} + restore-keys: spm- + + - name: Sanitize Swift package cache + if: steps.compilation-cache-restore.outputs.cache-hit != 'true' + run: python3 scripts/ci/sanitize-xcode-source-packages-cache.py .ci-source-packages + + - name: Resolve Swift packages + if: steps.compilation-cache-restore.outputs.cache-hit != 'true' + run: | + set -euo pipefail + scripts/ci/compile-app-host-test-product.sh resolve \ + "$CMUX_COMPILE_ADMISSION_DERIVED_DATA" "$PWD/.ci-source-packages" + + - name: Refresh test compilation cache + if: steps.compilation-cache-restore.outputs.cache-hit != 'true' + run: | + set -euo pipefail + scripts/ci/compile-app-host-test-product.sh build \ + "$CMUX_COMPILE_ADMISSION_DERIVED_DATA" \ + "$PWD/.ci-source-packages" \ + "$CMUX_COMPILE_ADMISSION_CAS" + + - name: Bound test compilation cache size + id: compilation-cache-bound + if: steps.compilation-cache-restore.outputs.cache-hit != 'true' + run: | + set -euo pipefail + cache_path="$CMUX_COMPILE_ADMISSION_CAS" + max_cache_kib=$((5 * 1024 * 1024)) + if [ -d "$cache_path" ]; then + # See `refresh-compilation-cache`: a warm build leaves a dead CAS + # generation behind, and pruning it keeps the seed to one build. + python3 scripts/ci/prune-xcode-compilation-cache.py "$cache_path" \ + || echo "::warning::Xcode compilation cache pruning failed; measuring it unpruned" + cache_kib=$(du -sk "$cache_path" | awk '{print $1}') + else + cache_kib=0 + fi + echo "Test compilation cache size: ${cache_kib} KiB (limit: ${max_cache_kib} KiB)" + save=false + if [ "$cache_kib" -gt "$max_cache_kib" ]; then + echo "::warning::Test compilation cache exceeds 5 GiB; skipping cache save" + elif [ "$cache_kib" -gt 0 ]; then + save=true + else + echo "Test compilation cache is empty; skipping cache save" + fi + echo "save=${save}" >> "$GITHUB_OUTPUT" + + - name: Save test compilation cache + if: steps.compilation-cache-restore.outputs.cache-hit != 'true' && steps.compilation-cache-bound.outputs.save == 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ env.CMUX_COMPILE_ADMISSION_CAS }} + key: xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}-${{ needs.decide.outputs.head_sha }} + build-nightly-ghostty-cli-helper: needs: decide if: needs.decide.outputs.should_build == 'true' && (github.event_name != 'schedule' || github.event.schedule == '47 8 * * *') && needs.decide.outputs.build_only != 'true' diff --git a/scripts/ci/compile-app-host-test-product.sh b/scripts/ci/compile-app-host-test-product.sh new file mode 100755 index 000000000000..da9646a3de36 --- /dev/null +++ b/scripts/ci/compile-app-host-test-product.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# compile-app-host-test-product.sh fingerprint +# compile-app-host-test-product.sh resolve +# compile-app-host-test-product.sh build [log] +# +# Compiles the app-host test product with Xcode's compilation cache on. ci.yml +# `macos-compile-admission` restores that cache read-only and nightly.yml +# `refresh-test-compilation-cache` writes it. A cache entry is keyed on the +# whole compiler invocation and on absolute paths, so both jobs must build +# through this script or they stop sharing hits without anything failing. +# +# `fingerprint` hashes the toolchain and the build paths into the cache key. +# Runner pools lay the workspace out differently, and a seed built under +# another layout cannot hit, so it should be a cache miss and not a download. +set -euo pipefail + +usage() { + echo "usage: $0 fingerprint " >&2 + echo " $0 resolve " >&2 + echo " $0 build [log]" >&2 + exit 64 +} + +# Same limit as the Release seed in nightly.yml. +cache_limit_bytes=3221225472 + +fingerprint() { + local derived_data="$1" + { + xcodebuild -version + printf 'workspace=%s\n' "$PWD" + printf 'derived-data=%s\n' "$derived_data" + } | shasum -a 256 | cut -c1-32 +} + +# `build` disables package resolution, so a resolve that reports success +# without the Sparkle and Sentry binary artifacts would fail it. A restored +# source-packages cache can do that, and a failed resolve can leave a partial +# clone behind, so every retry starts from an empty package directory. +resolve() { + local derived_data="$1" source_packages="$2" attempt + for attempt in 1 2 3; do + mkdir -p "$source_packages" "$derived_data" + if xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \ + -derivedDataPath "$derived_data" \ + -clonedSourcePackagesDirPath "$source_packages" \ + -resolvePackageDependencies; then + if [ -d "$source_packages/artifacts/sparkle/Sparkle/Sparkle.xcframework" ] \ + && [ -d "$source_packages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework" ]; then + return 0 + fi + echo "Resolve succeeded but binary artifacts are missing" >&2 + fi + [ "$attempt" -lt 3 ] || break + echo "Package resolution failed on attempt $attempt; clearing packages and retrying" >&2 + rm -rf "$source_packages" + done + echo "Failed to resolve Swift packages after 3 attempts" >&2 + return 1 +} + +build() { + local derived_data="$1" source_packages="$2" cas_path="$3" log="${4:-/dev/null}" + mkdir -p "$cas_path" + + # shellcheck disable=SC2016 # Xcode expands $(inherited), not the shell + for scheme in cmux-unit cmux-numeric-locale; do + xcodebuild -project cmux.xcodeproj -scheme "$scheme" -configuration Debug \ + -derivedDataPath "$derived_data" \ + -clonedSourcePackagesDirPath "$source_packages" \ + -disableAutomaticPackageResolution \ + -destination "platform=macOS" \ + 'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) CMUX_CI_APP_HOST_ISOLATION_REQUIRED' \ + 'LD_RUNPATH_SEARCH_PATHS=$(inherited) @executable_path/../Frameworks /private/tmp/cmux-app-host-package-frameworks' \ + COMPILATION_CACHE_ENABLE_CACHING=YES \ + "COMPILATION_CACHE_CAS_PATH=$cas_path" \ + "COMPILATION_CACHE_LIMIT_SIZE=$cache_limit_bytes" \ + build-for-testing 2>&1 | tee -a "$log" + done +} + +case "${1:-}" in + fingerprint) + [ "$#" -eq 2 ] || usage + fingerprint "$2" + ;; + resolve) + [ "$#" -eq 3 ] || usage + resolve "$2" "$3" + ;; + build) + [ "$#" -ge 4 ] && [ "$#" -le 5 ] || usage + shift + build "$@" + ;; + *) + usage + ;; +esac diff --git a/tests/test_ci_change_areas.py b/tests/test_ci_change_areas.py index 3894bccbb01d..4fccc3115621 100644 --- a/tests/test_ci_change_areas.py +++ b/tests/test_ci_change_areas.py @@ -988,9 +988,12 @@ def test_macos_compile_admission_precedes_expensive_shards() -> None: assert "name: macOS compile admission" in admission assert " - changes" in admission assert " - linux-preflight" in admission - assert "build-for-testing" in admission - assert "cmux-unit" in admission - assert "cmux-numeric-locale" in admission + # The compile lives in one script so the nightly cache seeder runs the same + # invocation; see tests/test_ci_test_compilation_cache_seed.sh. + assert "scripts/ci/compile-app-host-test-product.sh build" in admission + compile_script = (ROOT / "scripts/ci/compile-app-host-test-product.sh").read_text(encoding="utf-8") + assert "build-for-testing" in compile_script + assert "for scheme in cmux-unit cmux-numeric-locale; do" in compile_script assert "actions/cache@27d5ce7" in admission assert "steps.upload-products.outputs.artifact-id" in admission assert "app_host_test_products.py stamp" in admission diff --git a/tests/test_ci_test_compilation_cache_seed.sh b/tests/test_ci_test_compilation_cache_seed.sh new file mode 100755 index 000000000000..0524a28f7db6 --- /dev/null +++ b/tests/test_ci_test_compilation_cache_seed.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env bash +# Regression test for the Debug compilation cache that nightly seeds from main +# and the pull request compile admission job restores. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CI_FILE="$ROOT_DIR/.github/workflows/ci.yml" +NIGHTLY_FILE="$ROOT_DIR/.github/workflows/nightly.yml" +SCRIPT="$ROOT_DIR/scripts/ci/compile-app-host-test-product.sh" + +# Prints one job's body. +job_body() { + awk -v job="$2" ' + $0 ~ "^ "job":" { in_job=1; next } + in_job && /^ [^[:space:]#][^:]*:[[:space:]]*(#.*)?$/ { in_job=0 } + in_job { print } + ' "$1" +} + +ADMISSION="$(job_body "$CI_FILE" "macos-compile-admission")" +SEEDER="$(job_body "$NIGHTLY_FILE" "refresh-test-compilation-cache")" + +if [ -z "$ADMISSION" ] || [ -z "$SEEDER" ]; then + echo "FAIL: expected ci.yml macos-compile-admission and nightly.yml refresh-test-compilation-cache" + exit 1 +fi + +# A cache entry is keyed on the whole compiler invocation. If either job builds +# the product by hand, the two drift and the seed stops hitting without any +# job failing. +for pair in "admission:$ADMISSION" "seeder:$SEEDER"; do + name="${pair%%:*}" + body="${pair#*:}" + if ! grep -Fq 'scripts/ci/compile-app-host-test-product.sh build' <<<"$body" \ + || ! grep -Fq 'scripts/ci/compile-app-host-test-product.sh fingerprint' <<<"$body"; then + echo "FAIL: the $name job must build and fingerprint through scripts/ci/compile-app-host-test-product.sh" + exit 1 + fi + if grep -Eq 'xcodebuild .*build-for-testing|^[[:space:]]+build-for-testing' <<<"$body"; then + echo "FAIL: the $name job must not hand-roll the app-host test build; change scripts/ci/compile-app-host-test-product.sh" + exit 1 + fi +done +echo "PASS: admission and the seeder build the app-host test product through one script" + +# The build paths are part of every cache entry, so both jobs must use the +# same ones. +for line in \ + 'CMUX_COMPILE_ADMISSION_DERIVED_DATA=$RUNNER_TEMP/cmux-derived-data-compile-admission' \ + 'CMUX_COMPILE_ADMISSION_CAS=$RUNNER_TEMP/cmux-compile-admission-cas'; do + if ! grep -Fq "$line" <<<"$ADMISSION" || ! grep -Fq "$line" <<<"$SEEDER"; then + echo "FAIL: admission and the seeder must both set $line" + exit 1 + fi +done +echo "PASS: admission and the seeder build from the same paths" + +KEY_PREFIX='xcode-compilation-test-${{ runner.os }}-${{ runner.arch }}-${{ steps.compilation-cache-key.outputs.fingerprint }}-' +for file in "$CI_FILE" "$NIGHTLY_FILE"; do + if ! grep -Fq -- "$KEY_PREFIX" "$file" \ + || grep -F 'xcode-compilation-test-' "$file" | grep -vqF -- "$KEY_PREFIX"; then + echo "FAIL: $(basename "$file") must use the shared test compilation cache key prefix" + exit 1 + fi +done +echo "PASS: admission and the seeder share one cache key prefix" + +# Pull requests restore and never save: a cache written from a pull request is +# scoped to it, so it helps nobody else and spends the budget that keeps the +# main seed from being evicted. +if ! awk ' + /uses: actions\/cache/ { uses=$0 } + /key: xcode-compilation-test-/ { saw=1; if (uses !~ /actions\/cache\/restore@/) bad=1 } + END { exit !(saw && !bad) } +' <<<"$ADMISSION"; then + echo "FAIL: macos-compile-admission must restore the test compilation cache with actions/cache/restore and never save it" + exit 1 +fi +echo "PASS: pull requests restore the test compilation cache read-only" + +if ! awk ' + /^ - name: Restore test compilation cache/ { step="restore" } + /^ - name: Bound test compilation cache size/ { step="bound" } + /^ - name: Save test compilation cache/ { step="save" } + step == "restore" && /id: compilation-cache-restore/ { saw_restore_id=1 } + step == "restore" && /needs\.decide\.outputs\.head_sha/ { saw_revision_key=1 } + step == "bound" && /prune-xcode-compilation-cache\.py/ { saw_prune=1 } + step == "save" && /uses: actions\/cache\/save@/ { saw_save=1 } + step == "save" && /cache-hit != '\''true'\'' && steps\.compilation-cache-bound\.outputs\.save == '\''true'\''/ { saw_gate=1 } + END { exit !(saw_restore_id && saw_revision_key && saw_prune && saw_save && saw_gate) } +' <<<"$SEEDER"; then + echo "FAIL: refresh-test-compilation-cache must key the seed by main revision, prune it, and save only a new, bounded cache" + exit 1 +fi +echo "PASS: the seeder rolls the cache forward by main revision and bounds what it saves" + +if ! grep -Eq "if: github\.event_name == 'schedule'" <<<"$SEEDER"; then + echo "FAIL: refresh-test-compilation-cache must stay on the cache-warming schedule so it does not take a macOS slot per merge" + exit 1 +fi +echo "PASS: the seeder runs on the cache-warming schedule" + +# Exercise the script against a stub xcodebuild. +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT +mkdir -p "$TMP_DIR/bin" "$TMP_DIR/work" +cat > "$TMP_DIR/bin/xcodebuild" <<'STUB' +#!/usr/bin/env bash +if [ "${1:-}" = "-version" ]; then + echo "Xcode ${STUB_XCODE_VERSION:-26.3}" + exit 0 +fi +printf '%s\n' "$@" >> "$STUB_XCODEBUILD_ARGS" +echo "---" >> "$STUB_XCODEBUILD_ARGS" +# Resolution fails for the first STUB_RESOLVE_FAILS_UNTIL attempts, then reports +# success, and writes the binary artifacts only from the attempt named by +# STUB_RESOLVE_ARTIFACTS_FROM. +packages="" +resolving=0 +while [ "$#" -gt 0 ]; do + case "$1" in + -clonedSourcePackagesDirPath) packages="$2"; shift ;; + -resolvePackageDependencies) resolving=1 ;; + esac + shift +done +if [ "$resolving" -eq 1 ]; then + echo x >> "$STUB_RESOLVE_ATTEMPTS" + if [ "$(wc -l < "$STUB_RESOLVE_ATTEMPTS")" -le "${STUB_RESOLVE_FAILS_UNTIL:-0}" ]; then + # A failed resolve that leaves a partial clone behind. + mkdir -p "$packages/checkouts/partial-clone" + exit 74 + fi + if [ "$(wc -l < "$STUB_RESOLVE_ATTEMPTS")" -ge "${STUB_RESOLVE_ARTIFACTS_FROM:-1}" ]; then + mkdir -p "$packages/artifacts/sparkle/Sparkle/Sparkle.xcframework" \ + "$packages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework" + fi +fi +STUB +chmod +x "$TMP_DIR/bin/xcodebuild" +export STUB_RESOLVE_ATTEMPTS="$TMP_DIR/resolve-attempts.txt" +export STUB_XCODEBUILD_ARGS="$TMP_DIR/args.txt" + +run_script() { + (cd "$TMP_DIR/work" && PATH="$TMP_DIR/bin:$PATH" "$SCRIPT" "$@") +} + +first="$(run_script fingerprint /tmp/derived-a)" +again="$(run_script fingerprint /tmp/derived-a)" +other_path="$(run_script fingerprint /tmp/derived-b)" +other_xcode="$(STUB_XCODE_VERSION=26.5 run_script fingerprint /tmp/derived-a)" +if [ "$first" != "$again" ] || [ "$first" = "$other_path" ] || [ "$first" = "$other_xcode" ] || [ -z "$first" ]; then + echo "FAIL: the fingerprint must be stable and must change with the build path and the toolchain" + exit 1 +fi +echo "PASS: the fingerprint follows the build path and the toolchain" + +run_script build "$TMP_DIR/derived" "$TMP_DIR/packages" "$TMP_DIR/cas" "$TMP_DIR/build.log" >/dev/null +for expected in \ + cmux-unit \ + cmux-numeric-locale \ + build-for-testing \ + COMPILATION_CACHE_ENABLE_CACHING=YES \ + "COMPILATION_CACHE_CAS_PATH=$TMP_DIR/cas" \ + "$TMP_DIR/derived" \ + "$TMP_DIR/packages"; do + if ! grep -Fxq -- "$expected" "$STUB_XCODEBUILD_ARGS"; then + echo "FAIL: the build must pass $expected to xcodebuild" + exit 1 + fi +done +if [ "$(grep -c '^---$' "$STUB_XCODEBUILD_ARGS")" -ne 2 ] || [ ! -d "$TMP_DIR/cas" ]; then + echo "FAIL: the build must run both schemes against an existing CAS directory" + exit 1 +fi +# `build` compiles no test files: the cmux-unit scheme marks cmuxTests +# buildForRunning=NO. +if grep -Fxq -- build "$STUB_XCODEBUILD_ARGS"; then + echo "FAIL: the app-host test product must be compiled with build-for-testing, not build" + exit 1 +fi +echo "PASS: the build compiles both schemes for testing with the compilation cache on" + +# A restored package cache can make resolution succeed without the binary +# artifacts, and the build cannot resolve again. +: > "$STUB_RESOLVE_ATTEMPTS" +mkdir -p "$TMP_DIR/stale-packages/checkouts" +if ! STUB_RESOLVE_ARTIFACTS_FROM=2 run_script resolve "$TMP_DIR/derived" "$TMP_DIR/stale-packages" >/dev/null 2>&1 \ + || [ "$(wc -l < "$STUB_RESOLVE_ATTEMPTS")" -ne 2 ] \ + || [ -d "$TMP_DIR/stale-packages/checkouts" ]; then + echo "FAIL: resolve must clear the package cache and retry when the binary artifacts are missing" + exit 1 +fi +: > "$STUB_RESOLVE_ATTEMPTS" +if ! STUB_RESOLVE_FAILS_UNTIL=1 STUB_RESOLVE_ARTIFACTS_FROM=2 run_script resolve "$TMP_DIR/derived" "$TMP_DIR/failed-packages" >/dev/null 2>&1 \ + || [ "$(wc -l < "$STUB_RESOLVE_ATTEMPTS")" -ne 2 ] \ + || [ -d "$TMP_DIR/failed-packages/checkouts/partial-clone" ]; then + echo "FAIL: resolve must clear the partial clone a failed attempt leaves and retry" + exit 1 +fi +: > "$STUB_RESOLVE_ATTEMPTS" +if STUB_RESOLVE_ARTIFACTS_FROM=9 run_script resolve "$TMP_DIR/derived" "$TMP_DIR/never-packages" >/dev/null 2>&1 \ + || [ "$(wc -l < "$STUB_RESOLVE_ATTEMPTS")" -ne 3 ]; then + echo "FAIL: resolve must fail after three attempts without the binary artifacts" + exit 1 +fi +for name_and_body in "macos-compile-admission:$ADMISSION" "refresh-test-compilation-cache:$SEEDER"; do + if ! grep -Fq 'scripts/ci/compile-app-host-test-product.sh resolve' <<<"${name_and_body#*:}"; then + echo "FAIL: the ${name_and_body%%:*} job must resolve packages through scripts/ci/compile-app-host-test-product.sh" + exit 1 + fi +done +echo "PASS: resolve retries until the binary artifacts exist, in both jobs" + +if run_script bogus >/dev/null 2>&1 || run_script build only-one-arg >/dev/null 2>&1; then + echo "FAIL: the script must reject unknown commands and short argument lists" + exit 1 +fi +echo "PASS: the script rejects bad usage"