From 9e743315eb38607cb0ae66a58093955d9f6b9587 Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Thu, 13 Aug 2026 12:35:27 +0100 Subject: [PATCH] Cache React Native iOS builds with sccache Assisted-By: devx/669fe7a5-1428-4dee-a8b4-995403d59a5c --- .gitignore | 3 + e2e/BITRISE.md | 2 + e2e/bitrise.yml | 64 +++++++++++++++++++ platforms/react-native/CONTRIBUTING.md | 19 ++++-- .../react-native/sample/ios/Config.xcconfig | 1 + .../react-native/sample/scripts/build_ios | 19 +----- .../react-native/sample/scripts/ios_sccache | 20 ++++++ .../react-native/sample/scripts/test_ios | 4 +- 8 files changed, 111 insertions(+), 21 deletions(-) create mode 100755 platforms/react-native/sample/scripts/ios_sccache diff --git a/.gitignore b/.gitignore index 72c4616eb..8fa095703 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ e2e/.env.local # Decrypted ejson output must never be committed. config/secrets/*.json + +# SCCACHE_DIR on the Bitrise React Native iOS workflows. +.sccache/ diff --git a/e2e/BITRISE.md b/e2e/BITRISE.md index 0433db851..eead8be8f 100644 --- a/e2e/BITRISE.md +++ b/e2e/BITRISE.md @@ -200,4 +200,6 @@ The pipeline uses Bitrise cache steps for key-based pnpm/CocoaPods/Gradle cache Do not add `activate-build-cache-for-xcode` or `activate-build-cache-for-gradle`; the Bitrise Build Cache add-on is disabled for Shopify Bitrise apps. +The React Native iOS workflows also persist sccache between builds. Its keys prefer the current commit, then the latest entry from the branch, then the architecture. Each workflow gives `restore-sccache` a cache namespace; that bundle derives the exact save key and ordered restore keys once, and `save-sccache` reuses the generated exact key. The iOS build scripts start the server before Xcode and configure the compiler launcher using sccache's documented Xcode settings. + Ruby and Node versions are pinned in `e2e/bitrise.yml` via the Bitrise `tools:` configuration (`ruby: "3.4:installed"`, `nodejs: 22.14.0`), which Bitrise installs before each workflow runs. The `:installed` suffix tells each stack to use its own preinstalled 3.4.x rather than compiling one from source. Pin exact versions that the target stacks preinstall so setup stays fast and reproducible; a version the stack does not ship is installed on demand and is slower. pnpm is pinned separately through Corepack via the `packageManager` field in `platforms/react-native/package.json`. diff --git a/e2e/bitrise.yml b/e2e/bitrise.yml index e9699875a..1055041eb 100644 --- a/e2e/bitrise.yml +++ b/e2e/bitrise.yml @@ -241,6 +241,52 @@ step_bundles: simulator_uuid="$(xcrun simctl create CIDevice "$CI_IOS_SIMULATOR_DEVICE")" xcrun simctl bootstatus "$simulator_uuid" -b envman add --key CURRENT_SIMULATOR_UUID --value "$simulator_uuid" + install-sccache: + steps: + - script@1: + title: Install sccache + inputs: + - content: |- + set -euo pipefail + if ! command -v sccache >/dev/null 2>&1; then + brew install sccache + fi + sccache --version + restore-sccache: + inputs: + - cache_namespace: "" + steps: + - script@1: + title: Configure sccache cache keys + inputs: + - content: |- + set -euo pipefail + : "${cache_namespace:?cache_namespace input is required}" + : "${BITRISE_GIT_BRANCH:?BITRISE_GIT_BRANCH is required}" + : "${BITRISE_GIT_COMMIT:?BITRISE_GIT_COMMIT is required}" + cache_os="$(uname -s | tr '[:upper:]' '[:lower:]')" + cache_arch="$(uname -m)" + cache_prefix="${cache_namespace}-sccache-${cache_os}-${cache_arch}" + cache_key="${cache_prefix}-${BITRISE_GIT_BRANCH}-${BITRISE_GIT_COMMIT}" + cache_restore_keys="$(printf '%s\n%s\n%s' "$cache_key" "${cache_prefix}-${BITRISE_GIT_BRANCH}-" "${cache_prefix}-")" + envman add --key SCCACHE_CACHE_KEY --value "$cache_key" + envman add --key SCCACHE_RESTORE_KEYS --value "$cache_restore_keys" + - restore-cache@3: + inputs: + - key: $SCCACHE_RESTORE_KEYS + save-sccache: + steps: + - script@1: + title: Validate sccache cache configuration + inputs: + - content: |- + set -euo pipefail + : "${SCCACHE_CACHE_KEY:?restore-sccache must run before save-sccache}" + : "${SCCACHE_DIR:?SCCACHE_DIR is required}" + - save-cache@1: + inputs: + - key: $SCCACHE_CACHE_KEY + - paths: $SCCACHE_DIR bootstrap-mint: inputs: - mintfile_dir: "" @@ -368,6 +414,9 @@ workflows: bitrise.io: stack: *macos_stack machine_type_id: g2.mac.4large + envs: + - SCCACHE_ENABLED: "1" + - SCCACHE_DIR: $BITRISE_SOURCE_DIR/.sccache steps: - git-clone@8: {} - bundle::create-ios-simulator: {} @@ -384,6 +433,7 @@ workflows: scripts/check_published_podfile_lock sample/ios/Podfile.lock - bundle::install-node-modules: {} - bundle::install-ruby-gems: {} + - bundle::install-sccache: {} - bundle::install-cocoapods: {} # The 89 pods here are the React Native New Architecture C++ core. They dominate the # build and only change when the lockfile or the Swift package pins do. @@ -391,6 +441,9 @@ workflows: inputs: - key: &rn_sample_derived_data_cache_key |- rn-sample-derived-data-{{ .OS }}-{{ .Arch }}-{{ checksum "platforms/react-native/sample/ios/Podfile.lock" }}-{{ checksum "Package.resolved" }} + - bundle::restore-sccache: + inputs: + - cache_namespace: rn-sample - script@1: title: Build the React Native iOS sample timeout: 5400 @@ -401,16 +454,21 @@ workflows: cd platforms/react-native pnpm module build pnpm sample build:ios + sccache --show-stats - save-cache@1: inputs: - key: *rn_sample_derived_data_cache_key - paths: ~/Library/Developer/Xcode/DerivedData + - bundle::save-sccache: {} ci-ios-react-native-test-ios: meta: bitrise.io: stack: *macos_stack machine_type_id: g2.mac.4large + envs: + - SCCACHE_ENABLED: "1" + - SCCACHE_DIR: $BITRISE_SOURCE_DIR/.sccache steps: - git-clone@8: {} - bundle::create-ios-simulator: {} @@ -432,10 +490,14 @@ workflows: # Not the install-cocoapods bundle: that one owns the sample app's Pods tree. # test_ios runs `pod install` for the integration app itself, so this pair only # carries the resulting Pods directory across builds. + - bundle::install-sccache: {} - restore-cache@3: inputs: - key: &rct_integration_cocoapods_cache_key |- rct-integration-cocoapods-{{ .OS }}-{{ .Arch }}-{{ checksum "platforms/react-native/test/rct-integration-app/Podfile.lock" }} + - bundle::restore-sccache: + inputs: + - cache_namespace: rct-integration - script@1: title: Run the React Native iOS tests timeout: 5400 @@ -446,10 +508,12 @@ workflows: cd platforms/react-native pnpm module build pnpm sample test:ios + sccache --show-stats - save-cache@1: inputs: - key: *rct_integration_cocoapods_cache_key - paths: platforms/react-native/test/rct-integration-app/Pods + - bundle::save-sccache: {} ci-ios-report: steps: diff --git a/platforms/react-native/CONTRIBUTING.md b/platforms/react-native/CONTRIBUTING.md index e8e8d834b..cca5a7b15 100644 --- a/platforms/react-native/CONTRIBUTING.md +++ b/platforms/react-native/CONTRIBUTING.md @@ -97,7 +97,7 @@ Local mode has to stay an explicit command-line choice. Hardcoding it makes loca ## Optional: Speed up builds with sccache -For faster native compilation (especially on incremental builds), you can install [sccache](https://github.com/mozilla/sccache), a shared compilation cache: +Install [sccache](https://github.com/mozilla/sccache), a shared compilation cache: ```sh # macOS (using Homebrew) @@ -109,14 +109,25 @@ cargo install sccache # Other systems: see https://github.com/mozilla/sccache#installation ``` -The build scripts will automatically detect and use sccache if available. On Android, React Native's CMake files look for a command named `ccache`, so the sample Android scripts put an sccache-backed compatibility command first on `PATH`. If you encounter any build issues, you can temporarily disable it: +The Android build scripts automatically detect and use sccache if available. React Native's CMake files look for a command named `ccache`, so the sample Android scripts put an sccache-backed compatibility command first on `PATH`. If you encounter any build issues, you can temporarily disable it: ```sh -# Disable sccache for a single build -SCCACHE=false pnpm sample ios SCCACHE=false pnpm sample android ``` +The iOS integration is opt-in: + +```sh +SCCACHE_ENABLED=1 pnpm sample build:ios +SCCACHE_ENABLED=1 pnpm sample test:ios +sccache --show-stats +``` + +The build scripts start the sccache server before invoking Xcode and use Xcode's +`C_COMPILER_LAUNCHER`. They disable Clang header modules and response files +because sccache cannot cache those compiler invocations. CI enables this only on +the Bitrise React Native iOS workflows. + ## Making changes to the Native Module If your intentions are to modify the TS code for the Native Module under diff --git a/platforms/react-native/sample/ios/Config.xcconfig b/platforms/react-native/sample/ios/Config.xcconfig index 8cd6af16a..03f826e96 100644 --- a/platforms/react-native/sample/ios/Config.xcconfig +++ b/platforms/react-native/sample/ios/Config.xcconfig @@ -1,2 +1,3 @@ APPLE_PAY_MERCHANT_IDENTIFIER = +OTHER_LDFLAGS = $(inherited) -framework CoreMotion #include? "Storefront.xcconfig" diff --git a/platforms/react-native/sample/scripts/build_ios b/platforms/react-native/sample/scripts/build_ios index df5aae42a..2efc75677 100755 --- a/platforms/react-native/sample/scripts/build_ios +++ b/platforms/react-native/sample/scripts/build_ios @@ -3,22 +3,8 @@ set -ex set -eo pipefail -# Add sccache for faster compilation outside of CI environments -# Can be disabled with SCCACHE=false for debugging or matching CI behavior locally -if [ "$CI" = "true" ] || [ "$SCCACHE" = "false" ]; then - # CI detected or sccache explicitly disabled - skipping sccache for clean builds - if command -v sccache >/dev/null 2>&1; then - sccache --stop-server 2>/dev/null || true - fi -else - if command -v sccache >/dev/null 2>&1; then - export RUSTC_WRAPPER=sccache - export CC="sccache clang" - export CXX="sccache clang++" - fi -fi - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/ios_sccache" source "$SCRIPT_DIR/simulator" dest="$(get_sim_destination)" @@ -39,7 +25,8 @@ run_xcodebuild() { -disableAutomaticPackageResolution \ GCC_PRECOMPILE_PREFIX_HEADER=YES \ ASSETCATALOG_COMPILER_OPTIMIZATION=time \ - COMPILER_INDEX_STORE_ENABLE=NO + COMPILER_INDEX_STORE_ENABLE=NO \ + "${IOS_SCCACHE_XCODEBUILD_ARGS[@]}" } if command -v xcbeautify >/dev/null 2>&1; then diff --git a/platforms/react-native/sample/scripts/ios_sccache b/platforms/react-native/sample/scripts/ios_sccache new file mode 100755 index 000000000..92fc150bf --- /dev/null +++ b/platforms/react-native/sample/scripts/ios_sccache @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +IOS_SCCACHE_XCODEBUILD_ARGS=() + +if [ "${SCCACHE_ENABLED:-0}" = "1" ]; then + if ! SCCACHE_BINARY="$(command -v sccache)"; then + echo "sccache is required when SCCACHE_ENABLED=1" >&2 + exit 1 + fi + + if ! sccache --start-server 2>/dev/null; then + sccache --show-stats >/dev/null + fi + + IOS_SCCACHE_XCODEBUILD_ARGS=( + "C_COMPILER_LAUNCHER=$SCCACHE_BINARY" + "CLANG_ENABLE_MODULES=NO" + "CLANG_USE_RESPONSE_FILE=NO" + ) +fi diff --git a/platforms/react-native/sample/scripts/test_ios b/platforms/react-native/sample/scripts/test_ios index a01e852af..18e92937b 100755 --- a/platforms/react-native/sample/scripts/test_ios +++ b/platforms/react-native/sample/scripts/test_ios @@ -3,6 +3,7 @@ set -e -o pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/ios_sccache" REACT_NATIVE_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" REPO_ROOT="$(cd "$REACT_NATIVE_DIR/../.." && pwd)" TEST_APP_DIR="$REACT_NATIVE_DIR/test/rct-integration-app" @@ -48,7 +49,8 @@ run_xcodebuild() { -disableAutomaticPackageResolution \ -sdk iphonesimulator \ ASSETCATALOG_COMPILER_OPTIMIZATION=time \ - COMPILER_INDEX_STORE_ENABLE=NO + COMPILER_INDEX_STORE_ENABLE=NO \ + "${IOS_SCCACHE_XCODEBUILD_ARGS[@]}" } if command -v xcbeautify >/dev/null 2>&1; then