From 649e5f6e8822f0aeaaef309dbfdb1cba9b4fafea Mon Sep 17 00:00:00 2001 From: troZee <12766071+troZee@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:21:32 +0200 Subject: [PATCH 1/5] ci(ios): cache native compilation with ccache Most of the ~32 min iOS build is recompiling unchanged third-party pods (Reanimated, SVG, Screens, Worklets) in Release. Route clang through ccache via USE_CCACHE and persist the cache between runs, skip index store generation, and cache Ruby gems with setup-ruby. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ios.yml | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 245aacf5..aa10f845 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -26,6 +26,11 @@ jobs: concurrency: group: ${{ github.ref }}-ios cancel-in-progress: true + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_BASEDIR: ${{ github.workspace }} + CCACHE_MAXSIZE: 2G + CCACHE_COMPRESS: true steps: - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -39,6 +44,16 @@ jobs: run: bun install working-directory: example + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + ruby-version: '4.0' + bundler-cache: true + working-directory: example + + - name: Install ccache + run: brew install ccache + - name: Cache Pods id: cache-pods uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 @@ -48,9 +63,21 @@ jobs: path: example/ios/Pods key: ${{ hashFiles('example/ios/Podfile.lock') }} + # USE_CCACHE makes react_native_post_install route clang through ccache. - name: Install example Pods run: bun pods working-directory: example + env: + USE_CCACHE: 1 + + - name: Restore ccache + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ${{ env.CCACHE_DIR }} + key: ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}-${{ github.run_id }} + restore-keys: | + ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}- + ccache-ios- # Simulator.app attaches the Mac keyboard by default, which hides the # software keyboard and leaves no Return key for the keyboard tests. @@ -59,7 +86,17 @@ jobs: - name: Build iOS App run: | - bun example:ios:release + ccache --zero-stats + bun example:ios:release --extra-params "COMPILER_INDEX_STORE_ENABLE=NO" + ccache --show-stats + + # Saved right after the build so a failing e2e run still warms the cache. + - name: Save ccache + if: ${{ !cancelled() }} + uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ${{ env.CCACHE_DIR }} + key: ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}-${{ github.run_id }} - name: Install e2e dependencies run: bun install From c5eb758df72edab737bdde4c14b87e2eabd47356 Mon Sep 17 00:00:00 2001 From: troZee <12766071+troZee@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:03:09 +0200 Subject: [PATCH 2/5] ci(ios): disable explicit modules so Xcode uses the ccache wrapper Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ios.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index aa10f845..d1a0c1d1 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -74,7 +74,7 @@ jobs: uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ${{ env.CCACHE_DIR }} - key: ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}-${{ github.run_id }} + key: ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}-${{ github.run_id }}-${{ github.run_attempt }} restore-keys: | ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}- ccache-ios- @@ -87,7 +87,8 @@ jobs: - name: Build iOS App run: | ccache --zero-stats - bun example:ios:release --extra-params "COMPILER_INDEX_STORE_ENABLE=NO" + # Xcode 26 skips RN's ccache CC wrapper when explicit modules are on. + bun example:ios:release --extra-params "COMPILER_INDEX_STORE_ENABLE=NO CLANG_ENABLE_EXPLICIT_MODULES=NO" ccache --show-stats # Saved right after the build so a failing e2e run still warms the cache. @@ -96,7 +97,7 @@ jobs: uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ${{ env.CCACHE_DIR }} - key: ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}-${{ github.run_id }} + key: ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}-${{ github.run_id }}-${{ github.run_attempt }} - name: Install e2e dependencies run: bun install From daf9bc02b5d6a598853f58bbfb5411b0927100b7 Mon Sep 17 00:00:00 2001 From: troZee <12766071+troZee@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:08:19 +0200 Subject: [PATCH 3/5] ci(ios): patch RN ccache wrappers so ccache is actually invoked Xcode does not export the CCACHE_BINARY build setting to the compiler wrapper, so RN's ccache-clang.sh ran plain clang. Backport facebook/react-native#56612 via a bun patch and drop the explicit modules workaround. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ios.yml | 5 +-- example/bun.lock | 3 ++ example/package.json | 3 ++ example/patches/react-native@0.87.1.patch | 38 +++++++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 example/patches/react-native@0.87.1.patch diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index d1a0c1d1..07608648 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -64,6 +64,8 @@ jobs: key: ${{ hashFiles('example/ios/Podfile.lock') }} # USE_CCACHE makes react_native_post_install route clang through ccache. + # RN's wrapper scripts are patched (example/patches) to find ccache on + # PATH, see https://github.com/facebook/react-native/pull/56612. - name: Install example Pods run: bun pods working-directory: example @@ -87,8 +89,7 @@ jobs: - name: Build iOS App run: | ccache --zero-stats - # Xcode 26 skips RN's ccache CC wrapper when explicit modules are on. - bun example:ios:release --extra-params "COMPILER_INDEX_STORE_ENABLE=NO CLANG_ENABLE_EXPLICIT_MODULES=NO" + bun example:ios:release --extra-params "COMPILER_INDEX_STORE_ENABLE=NO" ccache --show-stats # Saved right after the build so a failing e2e run still warms the cache. diff --git a/example/bun.lock b/example/bun.lock index 5dc87e00..cccea1a6 100644 --- a/example/bun.lock +++ b/example/bun.lock @@ -39,6 +39,9 @@ }, }, }, + "patchedDependencies": { + "react-native@0.87.1": "patches/react-native@0.87.1.patch", + }, "overrides": { "use-latest-callback": "0.1.9", }, diff --git a/example/package.json b/example/package.json index d68e58cf..8a74e930 100644 --- a/example/package.json +++ b/example/package.json @@ -53,5 +53,8 @@ }, "engines": { "node": "^22.13.0 || ^24.3.0 || >=26.0.0" + }, + "patchedDependencies": { + "react-native@0.87.1": "patches/react-native@0.87.1.patch" } } diff --git a/example/patches/react-native@0.87.1.patch b/example/patches/react-native@0.87.1.patch new file mode 100644 index 00000000..7ca64d4a --- /dev/null +++ b/example/patches/react-native@0.87.1.patch @@ -0,0 +1,38 @@ +diff --git a/scripts/xcode/ccache-clang++.sh b/scripts/xcode/ccache-clang++.sh +index 54ff8ba5816dd9eb7000411871ccb3102fb097bb..b4dee6cfe74bda5a981afa6dfd2c4f985ceceac5 100755 +--- a/scripts/xcode/ccache-clang++.sh ++++ b/scripts/xcode/ccache-clang++.sh +@@ -11,4 +11,13 @@ REACT_NATIVE_CCACHE_CONFIGPATH=$SCRIPT_DIR/ccache.conf + # Provide our config file if none is already provided + export CCACHE_CONFIGPATH="${CCACHE_CONFIGPATH:-$REACT_NATIVE_CCACHE_CONFIGPATH}" + +-exec $CCACHE_BINARY clang++ "$@" ++# Xcode does not export user-defined build settings as environment variables ++# for invoked scripts, so $CCACHE_BINARY may be empty even when set in the ++# project. Fall back to a PATH lookup so ccache is still used. ++CCACHE_BINARY="${CCACHE_BINARY:-$(command -v ccache)}" ++ ++if [ -n "$CCACHE_BINARY" ]; then ++ exec "$CCACHE_BINARY" clang++ "$@" ++fi ++ ++exec clang++ "$@" +diff --git a/scripts/xcode/ccache-clang.sh b/scripts/xcode/ccache-clang.sh +index 9b1a355c2ccef049070060c0648ab020bf81df63..be632004cf10a114f0c286c8916b4b4af66c80d8 100755 +--- a/scripts/xcode/ccache-clang.sh ++++ b/scripts/xcode/ccache-clang.sh +@@ -11,4 +11,13 @@ REACT_NATIVE_CCACHE_CONFIGPATH=$SCRIPT_DIR/ccache.conf + # Provide our config file if none is already provided + export CCACHE_CONFIGPATH="${CCACHE_CONFIGPATH:-$REACT_NATIVE_CCACHE_CONFIGPATH}" + +-exec $CCACHE_BINARY clang "$@" ++# Xcode does not export user-defined build settings as environment variables ++# for invoked scripts, so $CCACHE_BINARY may be empty even when set in the ++# project. Fall back to a PATH lookup so ccache is still used. ++CCACHE_BINARY="${CCACHE_BINARY:-$(command -v ccache)}" ++ ++if [ -n "$CCACHE_BINARY" ]; then ++ exec "$CCACHE_BINARY" clang "$@" ++fi ++ ++exec clang "$@" From e43e5dccbb2bf77bd7e39e5a41f19f2d1d0c2c96 Mon Sep 17 00:00:00 2001 From: troZee <12766071+troZee@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:08:33 +0200 Subject: [PATCH 4/5] ci(ios): use canonical RN repo link Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ios.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 07608648..83f6cee0 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -65,7 +65,7 @@ jobs: # USE_CCACHE makes react_native_post_install route clang through ccache. # RN's wrapper scripts are patched (example/patches) to find ccache on - # PATH, see https://github.com/facebook/react-native/pull/56612. + # PATH, see https://github.com/react/react-native/pull/56612. - name: Install example Pods run: bun pods working-directory: example From 85354f0553bc3230d8c4986b0159b50097dd9cf5 Mon Sep 17 00:00:00 2001 From: troZee <12766071+troZee@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:43:38 +0200 Subject: [PATCH 5/5] ci(ios): use Xcode's compiler launcher for ccache Follow react-native-reanimated's setup: set ccache as C_COMPILER_LAUNCHER with explicit modules kept on, instead of RN's CC wrapper. This drops the react-native patch for the wrapper's CCACHE_BINARY bug. ccache is configured with --set-config since RN's ccache.conf only applies to the wrapper, and the cache is only saved from main (plus this PR, temporarily). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ios.yml | 44 ++++++++++++----------- example/bun.lock | 3 -- example/ios/Podfile | 20 +++++++++++ example/package.json | 3 -- example/patches/react-native@0.87.1.patch | 38 -------------------- 5 files changed, 44 insertions(+), 64 deletions(-) delete mode 100644 example/patches/react-native@0.87.1.patch diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 83f6cee0..7590bc71 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -26,11 +26,6 @@ jobs: concurrency: group: ${{ github.ref }}-ios cancel-in-progress: true - env: - CCACHE_DIR: ${{ github.workspace }}/.ccache - CCACHE_BASEDIR: ${{ github.workspace }} - CCACHE_MAXSIZE: 2G - CCACHE_COMPRESS: true steps: - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -51,8 +46,17 @@ jobs: bundler-cache: true working-directory: example - - name: Install ccache - run: brew install ccache + # Mirrors RN's scripts/xcode/ccache.conf, which only applies to its CC + # wrapper, not to the compiler launcher used here. + - name: Install and configure ccache + run: | + brew install ccache + ccache --set-config max_size=2G + ccache --set-config base_dir="$GITHUB_WORKSPACE" + ccache --set-config sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros + ccache --set-config depend_mode=true + ccache --set-config file_clone=true + ccache --set-config inode_cache=true - name: Cache Pods id: cache-pods @@ -63,23 +67,20 @@ jobs: path: example/ios/Pods key: ${{ hashFiles('example/ios/Podfile.lock') }} - # USE_CCACHE makes react_native_post_install route clang through ccache. - # RN's wrapper scripts are patched (example/patches) to find ccache on - # PATH, see https://github.com/react/react-native/pull/56612. + # Sets ccache as Xcode's C_COMPILER_LAUNCHER, see example/ios/Podfile. - name: Install example Pods run: bun pods working-directory: example env: - USE_CCACHE: 1 + USE_CCACHE_LAUNCHER: 1 - name: Restore ccache uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: - path: ${{ env.CCACHE_DIR }} - key: ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ~/Library/Caches/ccache + key: ccache-ios-Release-${{ github.sha }}-${{ github.run_attempt }} restore-keys: | - ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}- - ccache-ios- + ccache-ios-Release- # Simulator.app attaches the Mac keyboard by default, which hides the # software keyboard and leaves no Return key for the keyboard tests. @@ -90,15 +91,18 @@ jobs: run: | ccache --zero-stats bun example:ios:release --extra-params "COMPILER_INDEX_STORE_ENABLE=NO" - ccache --show-stats + ccache --show-stats -v - # Saved right after the build so a failing e2e run still warms the cache. + # Only main saves, PRs restore from it. Saved right after the build so a + # failing e2e run still warms the cache. + # TODO: drop the pull_request condition before merging, it's only here to + # measure warm builds on this PR. - name: Save ccache - if: ${{ !cancelled() }} + if: ${{ !cancelled() && (github.ref == 'refs/heads/main' || github.event_name == 'pull_request') }} uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: - path: ${{ env.CCACHE_DIR }} - key: ccache-ios-${{ hashFiles('example/ios/Podfile.lock') }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ~/Library/Caches/ccache + key: ccache-ios-Release-${{ github.sha }}-${{ github.run_attempt }} - name: Install e2e dependencies run: bun install diff --git a/example/bun.lock b/example/bun.lock index cccea1a6..5dc87e00 100644 --- a/example/bun.lock +++ b/example/bun.lock @@ -39,9 +39,6 @@ }, }, }, - "patchedDependencies": { - "react-native@0.87.1": "patches/react-native@0.87.1.patch", - }, "overrides": { "use-latest-callback": "0.1.9", }, diff --git a/example/ios/Podfile b/example/ios/Podfile index ab7e80f1..44469836 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -31,6 +31,26 @@ target 'PagerViewExample' do # :ccache_enabled => true ) + # Xcode's compiler launcher keeps explicit modules enabled, unlike RN's + # ccache CC wrapper (USE_CCACHE), so don't combine the two. + if ENV['USE_CCACHE_LAUNCHER'] == '1' + ccache_path = `command -v ccache`.strip + if ccache_path.empty? + Pod::UI.warn('[Ccache] USE_CCACHE_LAUNCHER=1 but ccache was not found on PATH') + else + Pod::UI.puts("[Ccache] Using #{ccache_path} as C_COMPILER_LAUNCHER") + projects = [installer.pods_project] + projects += installer.aggregate_targets.map(&:user_project).compact + projects.uniq { |project| project.path }.each do |project| + project.build_configurations.each do |config| + config.build_settings['C_COMPILER_LAUNCHER'] = ccache_path + config.build_settings['CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER'] = 'YES' + end + project.save + end + end + end + # Xcode rejects deployment targets below 15.0, and RN's own post_install # skips resource-bundle targets, so RNSVG's bundles keep the podspec's 12.4. installer.pods_project.targets.each do |target| diff --git a/example/package.json b/example/package.json index 8a74e930..d68e58cf 100644 --- a/example/package.json +++ b/example/package.json @@ -53,8 +53,5 @@ }, "engines": { "node": "^22.13.0 || ^24.3.0 || >=26.0.0" - }, - "patchedDependencies": { - "react-native@0.87.1": "patches/react-native@0.87.1.patch" } } diff --git a/example/patches/react-native@0.87.1.patch b/example/patches/react-native@0.87.1.patch deleted file mode 100644 index 7ca64d4a..00000000 --- a/example/patches/react-native@0.87.1.patch +++ /dev/null @@ -1,38 +0,0 @@ -diff --git a/scripts/xcode/ccache-clang++.sh b/scripts/xcode/ccache-clang++.sh -index 54ff8ba5816dd9eb7000411871ccb3102fb097bb..b4dee6cfe74bda5a981afa6dfd2c4f985ceceac5 100755 ---- a/scripts/xcode/ccache-clang++.sh -+++ b/scripts/xcode/ccache-clang++.sh -@@ -11,4 +11,13 @@ REACT_NATIVE_CCACHE_CONFIGPATH=$SCRIPT_DIR/ccache.conf - # Provide our config file if none is already provided - export CCACHE_CONFIGPATH="${CCACHE_CONFIGPATH:-$REACT_NATIVE_CCACHE_CONFIGPATH}" - --exec $CCACHE_BINARY clang++ "$@" -+# Xcode does not export user-defined build settings as environment variables -+# for invoked scripts, so $CCACHE_BINARY may be empty even when set in the -+# project. Fall back to a PATH lookup so ccache is still used. -+CCACHE_BINARY="${CCACHE_BINARY:-$(command -v ccache)}" -+ -+if [ -n "$CCACHE_BINARY" ]; then -+ exec "$CCACHE_BINARY" clang++ "$@" -+fi -+ -+exec clang++ "$@" -diff --git a/scripts/xcode/ccache-clang.sh b/scripts/xcode/ccache-clang.sh -index 9b1a355c2ccef049070060c0648ab020bf81df63..be632004cf10a114f0c286c8916b4b4af66c80d8 100755 ---- a/scripts/xcode/ccache-clang.sh -+++ b/scripts/xcode/ccache-clang.sh -@@ -11,4 +11,13 @@ REACT_NATIVE_CCACHE_CONFIGPATH=$SCRIPT_DIR/ccache.conf - # Provide our config file if none is already provided - export CCACHE_CONFIGPATH="${CCACHE_CONFIGPATH:-$REACT_NATIVE_CCACHE_CONFIGPATH}" - --exec $CCACHE_BINARY clang "$@" -+# Xcode does not export user-defined build settings as environment variables -+# for invoked scripts, so $CCACHE_BINARY may be empty even when set in the -+# project. Fall back to a PATH lookup so ccache is still used. -+CCACHE_BINARY="${CCACHE_BINARY:-$(command -v ccache)}" -+ -+if [ -n "$CCACHE_BINARY" ]; then -+ exec "$CCACHE_BINARY" clang "$@" -+fi -+ -+exec clang "$@"