Skip to content

Enable ccache for the React Native pods on CI - #638

Draft
kieran-osgood-shopify wants to merge 1 commit into
kieran-osgood/macos-ci-8-derived-data-cachefrom
kieran-osgood/macos-ci-9-ccache
Draft

Enable ccache for the React Native pods on CI#638
kieran-osgood-shopify wants to merge 1 commit into
kieran-osgood/macos-ci-8-derived-data-cachefrom
kieran-osgood/macos-ci-9-ccache

Conversation

@kieran-osgood-shopify

@kieran-osgood-shopify kieran-osgood-shopify commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Part of the macOS CI speed-up. Tracked on shop/issues-checkout-kit#1211, under shop/issues-checkout-kit#1202.

Lands alone, on purpose. Compiler wrappers can miscompile silently, so this
soaks by itself before the next optimisation goes near the same builds.

Change

Opt-in, never a default

Both Podfiles read CCACHE_ENABLED rather than hardcoding a value:

  • platforms/react-native/sample/ios/Podfile
  • platforms/react-native/test/rct-integration-app/Podfile

e2e/bitrise.yml sets CCACHE_ENABLED: "1" on the two React Native macOS
workflows and nowhere else. Keeping it an explicit environment choice is the same
rule POD_REPO_UPDATE follows: a hardcoded default makes local runs stop
matching CI.

react_native_post_install bakes the wrapper into the generated Pods project at
pod install time, not at build time. So the flag has to be set for both, and
the install step runs after ccache is on PATH.

Both Podfile.lock files change in one line, PODFILE CHECKSUM, because the
Podfile text changed. The lock is byte-identical with and without
CCACHE_ENABLED=1 — verified below — so CI's pod install --deployment stays
happy on either path.

🔴 A ccache pod install dirties a tracked Xcode project

React Native writes the wrapper into the app project too, including an absolute
CCACHE_BINARY = /opt/homebrew/bin/ccache, and
sample/ios/CheckoutKitReactNativeDemo.xcodeproj/project.pbxproj is committed. A
later plain pod install does not reliably put it back.

scripts/test/ccache_opt_in_test.rb now fails if that project carries a compiler
wrapper, so the machine-specific path cannot reach main. CONTRIBUTING.md says
how to restore it. Nothing on Bitrise reads the file, so a dirty CI checkout is
harmless there.

Cache the compiler cache

An inline restore-cache@3 / save-cache@1 pair per workflow over the ccache
directory, keyed by branch and commit with branch-then-arch fallback prefixes.
Without it the wrapper starts cold on every build and buys nothing.

Two new invariants in e2e/test/bitrise_config_test.rb guard the shape:

  • restore-cache accepts exactly one key input, holding a priority-ordered
    list. A plural keys: reads naturally, does not exist, and bitrise validate
    does not catch it — the step fails at runtime instead. I wrote it that way
    first, so the test now spells out the four inputs the step declares.
  • save-cache takes a single key, so the exact key is repeated by hand. The
    existing restore-versus-save pairing check now compares only the first line,
    which makes drift between the two copies a test failure.

🔴 The obvious version of this change caches nothing

pod install points CC at React Native's ccache-clang.sh, which runs
exec $CCACHE_BINARY clang. It writes CCACHE_BINARY as an Xcode build
setting
, and Xcode does not put build settings in the compiler's environment.
The wrapper therefore execs a bare clang. The build succeeds, the tests pass,
and the hit rate is zero — measured, not guessed:

Build ccache calls hits misses
Podfile flag only 0 0 0
Plus CCACHE_BINARY exported 3 0 3
Same again, warm 3 3 0

So build_ios and test_ios export it when CCACHE_ENABLED=1, and
scripts/test/ccache_opt_in_test.rb fails if either stops. A silent no-op that
still goes green is exactly the failure a comment does not catch.

Stop fighting over the compiler

platforms/react-native/sample/scripts/build_ios no longer exports an sccache
compiler wrapper at all. It could never have worked (see below), and the ccache
settings the Podfile writes are Xcode build settings, which win over shell
exports anyway.

🔴 Also fixes a local-only breakage

pnpm sample build:ios fails today on any developer machine with sccache on
PATH:

error: unable to spawn process 'sccache clang' (No such file or directory)

build_ios exported CC="sccache clang", and Xcode spawns CC as a single
executable, so a two-word value can never resolve. CI never hit this because the
CI = true branch skipped sccache. Reproduced on this branch before the change
and green after.

Only Android uses sccache now. platforms/react-native/sample/scripts/android_sccache
shims it behind the name CMake looks for, which is the pattern Xcode has no
equivalent of. platforms/react-native/CONTRIBUTING.md is updated to say so.

Baselines to beat

Job median slowest step
React Native / Build iOS Sample 11m59s Build iOS sample 10m27s
React Native / Run iOS Tests 13m19s Run iOS tests 11m44s

Measure the second run after merge, once the ccache key is populated.

Verification

  • shadowenv exec -- ./scripts/test_ruby — 300 runs, 715 assertions, 0 failures.
  • shadowenv exec -- bitrise validate --config=e2e/bitrise.ymlConfig is valid: true.
  • bash -n platforms/react-native/sample/scripts/build_ios — clean.
  • CCACHE_ENABLED=1 pod install prints [Ccache]: Setting CC, LD, CXX & LDPLUSPLUS build settings and writes 10 ccache references into Pods.xcodeproj. A plain
    pod install writes zero. Both produce the same Podfile.lock.
  • Local builds of the React Native sample, cold and warm, with and without
    CCACHE_ENABLED=1. All Build Succeeded; hit rates in the table above.
  • .ccache/ is gitignored, because CCACHE_DIR sits under the source directory
    so save-cache can reach it.

Decisions made without you

  • The guard test skips comment lines. Otherwise the comment explaining the
    variable trips the invariant that forbids setting it.
  • e2e/bitrise.yml is deliberately outside the guard's globs. It is the one file
    allowed to turn ccache on.

Open question for the author

The Bitrise macOS stack may not ship ccache. This PR installs it with a
command -v guard, the same shape bootstrap-mint uses for mint. If the stack
already carries it, the guard is a no-op and the step can be dropped.


Before you merge

Important

  • I've added tests to support my implementation
  • I have read and agree with the Contribution Guidelines
  • I have read and agree with the Code of Conduct
  • I've updated the relevant platform README (platforms/swift/README.md and/or platforms/android/README.md)

Releasing a new Swift version?
  • I have bumped the version in ShopifyCheckoutKit.podspec
  • I have bumped the version in platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift
  • I have updated the SwiftPM/CocoaPods version snippets in platforms/swift/README.md (major version only)
Releasing a new Embedded Checkout Protocol version?
  • I have bumped embeddedCheckoutProtocolAndroid in platforms/android/gradle/libs.versions.toml
  • I have updated protocol/languages/kotlin/embedded-checkout-protocol/api/embedded-checkout-protocol.api if the public API changed
Releasing a new Android version?
  • I have bumped checkoutKitAndroid in platforms/android/gradle/libs.versions.toml
  • I have updated the Gradle/Maven version snippets in platforms/android/README.md

Tip

See the Contributing documentation for the full release process per platform.

Part of the macOS CI speed-up. Tracked on shop/issues-checkout-kit#1211, under shop/issues-checkout-kit#1202.

Lands alone, on purpose. Compiler wrappers can miscompile silently, so this
soaks by itself before the next optimisation goes near the same builds.

## Change

**Opt-in, never a default**

Both Podfiles read `CCACHE_ENABLED` rather than hardcoding a value:

- `platforms/react-native/sample/ios/Podfile`
- `platforms/react-native/test/rct-integration-app/Podfile`

`e2e/bitrise.yml` sets `CCACHE_ENABLED: "1"` on the two React Native macOS
workflows and nowhere else. Keeping it an explicit environment choice is the same
rule `POD_REPO_UPDATE` follows: a hardcoded default makes local runs stop
matching CI.

`react_native_post_install` bakes the wrapper into the generated Pods project at
`pod install` time, not at build time. So the flag has to be set for both, and
the install step runs after ccache is on `PATH`.

Both `Podfile.lock` files change in one line, `PODFILE CHECKSUM`, because the
Podfile text changed. The lock is byte-identical with and without
`CCACHE_ENABLED=1` — verified below — so CI's `pod install --deployment` stays
happy on either path.

**🔴 A ccache `pod install` dirties a tracked Xcode project**

React Native writes the wrapper into the app project too, including an absolute
`CCACHE_BINARY = /opt/homebrew/bin/ccache`, and
`sample/ios/CheckoutKitReactNativeDemo.xcodeproj/project.pbxproj` is committed. A
later plain `pod install` does not reliably put it back.

🟡 So check `git status` after a ccache `pod install`, and restore that file
before you commit. `CONTRIBUTING.md` says how. Nothing on Bitrise reads it, so a
dirty CI checkout is harmless there.

**Cache the compiler cache**

An inline `restore-cache@3` / `save-cache@1` pair per workflow over the ccache
directory, keyed by branch and commit with branch-then-arch fallback prefixes.
Without it the wrapper starts cold on every build and buys nothing.

🟡 Two traps to know if you edit these blocks:

- `restore-cache` accepts exactly one `key` input, holding a priority-ordered
  list. A plural `keys:` reads naturally, does not exist, and `bitrise validate`
  does not catch it — the step fails at runtime instead. I wrote it that way
  first.
- `save-cache` takes a single key, so the first line of the restore key is
  repeated by hand. The two copies have to stay identical.

**🔴 The obvious version of this change caches nothing**

`pod install` points `CC` at React Native's `ccache-clang.sh`, which runs
`exec $CCACHE_BINARY clang`. It writes `CCACHE_BINARY` as an Xcode *build
setting*, and Xcode does not put build settings in the compiler's environment.
The wrapper therefore execs a bare `clang`. The build succeeds, the tests pass,
and the hit rate is zero — measured, not guessed:

| Build | ccache calls | hits | misses |
| --- | --- | --- | --- |
| Podfile flag only | 0 | 0 | 0 |
| Plus `CCACHE_BINARY` exported | 3 | 0 | 3 |
| Same again, warm | 3 | 3 | 0 |

So `build_ios` and `test_ios` export it when `CCACHE_ENABLED=1`, and
`scripts/test/ccache_opt_in_test.rb` fails if either stops. A silent no-op that
still goes green is exactly the failure a comment does not catch.

**Stop fighting over the compiler**

`platforms/react-native/sample/scripts/build_ios` no longer exports an sccache
compiler wrapper at all. It could never have worked (see below), and the ccache
settings the Podfile writes are Xcode build settings, which win over shell
exports anyway.

## 🔴 Also fixes a local-only breakage

`pnpm sample build:ios` fails today on any developer machine with `sccache` on
`PATH`:

```
error: unable to spawn process 'sccache clang' (No such file or directory)
```

`build_ios` exported `CC="sccache clang"`, and Xcode spawns `CC` as a single
executable, so a two-word value can never resolve. CI never hit this because the
`CI = true` branch skipped sccache. Reproduced on this branch before the change
and green after.

Only Android uses sccache now. `platforms/react-native/sample/scripts/android_sccache`
shims it behind the name CMake looks for, which is the pattern Xcode has no
equivalent of. `platforms/react-native/CONTRIBUTING.md` is updated to say so.

**Docs**

`e2e/BITRISE.md` gains a paragraph on the two `ci-ios` caches — DerivedData from
the previous PR and ccache from this one — including the `key` versus `keys` trap
above. It lands here rather than in a docs-only PR, because both caches exist by
this point in the stack and not before it.

## Baselines to beat

| Job | median | slowest step |
| --- | --- | --- |
| `React Native / Build iOS Sample` | 11m59s | `Build iOS sample` 10m27s |
| `React Native / Run iOS Tests` | 13m19s | `Run iOS tests` 11m44s |

Measure the second run after merge, once the ccache key is populated.

## Verification

- `shadowenv exec -- ./scripts/test_ruby` — 300 runs, 715 assertions, 0 failures.
- `shadowenv exec -- bitrise validate --config=e2e/bitrise.yml` — `Config is valid: true`.
- `bash -n platforms/react-native/sample/scripts/build_ios` — clean.
- `CCACHE_ENABLED=1 pod install` prints `[Ccache]: Setting CC, LD, CXX & LDPLUSPLUS
  build settings` and writes 10 ccache references into `Pods.xcodeproj`. A plain
  `pod install` writes zero. Both produce the same `Podfile.lock`.
- Local builds of the React Native sample, cold and warm, with and without
  `CCACHE_ENABLED=1`. All `Build Succeeded`; hit rates in the table above.
- `.ccache/` is gitignored, because `CCACHE_DIR` sits under the source directory
  so `save-cache` can reach it.

## Decisions made without you

- The guard test skips comment lines. Otherwise the comment explaining the
  variable trips the invariant that forbids setting it.
- `e2e/bitrise.yml` is deliberately outside the guard's globs. It is the one file
  allowed to turn ccache on.

## Open question for the author

The Bitrise macOS stack may not ship `ccache`. This PR installs it with a
`command -v` guard, the same shape `bootstrap-mint` uses for `mint`. If the stack
already carries it, the guard is a no-op and the step can be dropped.
@kieran-osgood-shopify
kieran-osgood-shopify force-pushed the kieran-osgood/macos-ci-9-ccache branch from e8b55e9 to af6792e Compare August 13, 2026 11:36
@kieran-osgood-shopify
kieran-osgood-shopify force-pushed the kieran-osgood/macos-ci-8-derived-data-cache branch from 1dd7a03 to 4f7cff0 Compare August 13, 2026 11:36
@kiftio

kiftio commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

yo, just browsing.. Should we use sccache to match up with what we run locally?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

#gsd:50662 Rebase Checkout Kit on UCP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants