Bundle, build and run iOS test app in separate steps in production + ccache enabled - #427
Merged
Conversation
react-native-test-app resolves the resources declared in app.json when it generates the workspace (validate_resources in ios/test_app.rb), warning about and skipping any that don't exist yet: CocoaPods will not include resources it cannot find: [...] The app will still build and run if they are served by the dev server. To include missing resources, make sure they exist, then run `pod install` again. Bundling after `pod install` therefore left dist/main.ios.jsbundle out of the app, so it expected a Metro dev server that CI never starts. Also pass `--assets-dest dist` rather than `dist/res`: dist/res is the Android convention, whereas React Native's iOS bundler writes into <assets-dest>/assets and app.json declares dist/assets for iOS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww
"iPhone 17" only exists on the Xcode versions that shipped it, and macos-latest rolls forward, so hard-coding a device name means the boot step breaks on a runner image update. Query simctl for the first available iPhone instead, wait for it with `bootstatus -b`, and address it by UDID in the later steps rather than relying on the "booted" alias. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww
GitHub Actions runs a bare `run:` step as `bash -e {0}`, without pipefail,
so `xcodebuild ... | xcbeautify` reports xcbeautify's exit code and a
failing build is swallowed. Naming the shell explicitly opts into
`bash --noprofile --norc -eo pipefail`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww
The Release build failed with every system module unbuildable:
module '_c_standard_library_obsolete' requires feature
'found_incompatible_headers__check_search_paths'
could not build module '_Builtin_float' / 'CoreFoundation' / 'Foundation'
preceded by 116 copies of:
note: Explicit modules is enabled but the compiler was not recognized;
disable explicit modules with CLANG_ENABLE_EXPLICIT_MODULES=NO, or use
C_COMPILER_LAUNCHER with CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER=YES
if using a compatible launcher
USE_CCACHE=1 makes React Native's Podfile integration point CC/CXX at its
scripts/xcode/ccache-clang{,++}.sh wrappers. Xcode does not recognise a
wrapper script as a compiler, so it cannot build explicit modules with it.
Of the two fixes Xcode suggests, this takes the first: it keeps ccache,
which is the point of the change. The second would need React Native to
configure ccache through C_COMPILER_LAUNCHER rather than by overriding
CC/CXX, which it does not do as of 0.81.4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww
The previous commit silenced the "Explicit modules is enabled but the
compiler was not recognized" notes but did not fix the build: the module
errors remained, so explicit modules was a symptom rather than the cause.
The cause is which compiler runs. USE_CCACHE makes React Native set
CC/CXX/LD/LDPLUSPLUS to scripts/xcode/ccache-clang{,++}.sh, whose last line
is:
exec $CCACHE_BINARY clang "$@"
That `clang` is unqualified, so it resolves off PATH rather than to Xcode's
toolchain. This job runs aminya/setup-cpp, which puts an LLVM toolchain on
PATH, so the build compiles against the iOS SDK with a compiler that does
not match it. Apple traps precisely this:
module '_c_standard_library_obsolete' requires feature
'found_incompatible_headers__check_search_paths'
and every system module (_Builtin_float, CoreFoundation, Foundation,
CoreGraphics, UIKit) then fails to build. main is unaffected because it
never sets USE_CCACHE for the app build.
So drop USE_CCACHE and use Xcode's compiler-launcher support instead, which
keeps Xcode's own clang and only prefixes it with ccache. Explicit modules
stays enabled, opted in for a launcher as Xcode's diagnostic describes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww
CocoaPods intermittently aborts while generating the Pods project: ArgumentError - pathname contains null byte cocoapods/project.rb:452 Pod::Project#group_for_path_in_group It hit 2 of the 4 pod install runs on this branch, twice ending the job before the build step ran at all. It is an unresolved upstream bug that CocoaPods' own issue matcher points at, specific to pnpm monorepos: CocoaPods/CocoaPods#12866 and #12798. 1.17.0 is the newest release on RubyGems (2026-07-06) and is the version that fails, so there is no fixed version to move to and a workaround is the only option here. The retry is deliberately narrow: only a failure whose output carries that exact signature is retried, so any genuine pod install error still fails the step immediately, and each retry emits a ::warning:: annotation so the flake stays visible in the run summary instead of being silently absorbed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww
Follows #391, which dropped xcbeautify from the macOS job and noted it was the only use of it in the repo. This branch forked before that landed, so rebasing brought it back for iOS — it should go the same way. Its stated failure mode showed up here too. Diagnosing the Release build on this branch, "** ARCHIVE FAILED **" and the failed-command list printed ~150 lines *before* the compiler errors that explained them, because xcbeautify's stdout is block-buffered through the pipe. The summary read as the whole story and pointed at the wrong cause. Raw xcodebuild output is verbose but complete and correctly ordered. It also makes the pipefail guard added earlier on this branch unnecessary: the step's exit code is now xcodebuild's own, so `shell: bash` goes too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww
Comment-only; the non-comment diff is empty. Keeps the load-bearing facts — why bundling precedes pod install, why USE_CCACHE is not set, the upstream issue behind the pod install retry and the condition for dropping it — and cuts the surrounding explanation.
The first green run cached almost nothing: 2160 of 2190 calls uncacheable, and the 30 hits came from the CMake bootstrap rather than Xcode. React Native ships scripts/xcode/ccache.conf for exactly this, but it only takes effect through their ccache-clang.sh wrapper, which sets CCACHE_CONFIGPATH to it. Moving ccache to a compiler launcher dropped the wrapper and with it that config, leaving ccache on its defaults — under which Xcode's -fmodules, precompiled headers, -index-store-path and -ivfsoverlay all make a compile "too hard" to cache. So set the same options directly. They land in the config file the ccache action already writes, which is where the launcher's ccache reads them from, so the previous CCACHE_DIR/MAXSIZE/COMPILERCHECK exports are now redundant and go. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww
With React Native's tuning applied, cacheable calls went from 30/2190 (1.37%) to 1082/2185 (49.52%). The cache could not hold the result: Cache size (GB): 0.5 / 0.5 (99.76%) Cleanups: 88 500M is the action's default. One build filled it and evicted 88 times, so most of the 1052 newly-cached objects were discarded before the build finished, and none survive for the next run to hit. Raised on the iOS job only, which is the one building React Native from source; the other jobs stay on the default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #360, which forked from
mainat80ae73band became unmergeable when the workspace migrated from npm to pnpm —package-lock.jsonwas deleted onmainand modified there, so GitHub could not compute a merge ref and CI had nothing to run against.This is #360 rebased onto current
main, plus three fixes to the iOS job. Getting some inspiration from prior art.Rebased from #360
The six commits are faithful ports; the only change inside them is
npm ci/npx→pnpm install/pnpm exec, forced by the pnpm migration.test:ios:allTests, so a build failure is distinguishable from a test failure.CCACHE_CONFIGPATH, so the config written byhendrikmuhs/ccache-actiondoesn't propagate —CCACHE_DIR/CCACHE_MAXSIZE/CCACHE_COMPILERCHECKare re-exported through$GITHUB_ENV, where they take precedence.x86_64-apple-iosas a default ferric target on Apple hosts. This is Rust's x86_64 iOS simulator target, so without it ferric produced an arm64-only slice whileCMAKE_RN_TRIPLETS: arm64;x86_64-apple-ios-simproduced a fat one, and linking failed on the mismatch.generic/platform=iOS Simulator.One commit from #360 is dropped:
623c875"Dedupe and update package lock (and upgrade prettier)". It was apackage-lock.jsonrewrite, and that file no longer exists. What survived it — the@prettier/plugin-oxc0.0.4 → 0.1.3bump and the reformat it caused incopy-examples.mts— would desyncpackage.jsonfrom the committedpnpm-lock.yamland breakpnpm install --frozen-lockfile. That upgrade is worth doing separately, with a regenerated lockfile.Additional fixes
Isolated in three commits on top, so any can be dropped independently.
pod install.react-native-test-appresolves the resources declared inapp.jsonwhen it generates the workspace (validate_resourcesinios/test_app.rb), warning about and skipping any that don't exist yet: "CocoaPods will not include resources it cannot find […] make sure they exist, then runpod installagain." Bundling afterwards leftdist/main.ios.jsbundleout of the app, so it expected a Metro dev server that CI never starts. Also--assets-dest distrather thandist/res—dist/resis the Android convention, whereas React Native's iOS bundler writes into<assets-dest>/assetsandapp.jsondeclaresdist/assetsfor iOS.iPhone 17, wait for it withsimctl bootstatus -b, and address it by UDID rather than thebootedalias.macos-latestrolls forward and the device lineup changes with each Xcode release.shell: bashon the xcodebuild step. A barerun:isbash -e {0}without pipefail, soxcodebuild … | xcbeautifyreported xcbeautify's exit code and a failing build was swallowed.Testing
Verified locally:
pnpm install --frozen-lockfile,pnpm run buildandpnpm run prettier:checkpass.Two things could not be verified in the Linux container this was prepared in, and are for CI to confirm:
pnpm run lintreports 4no-unsafe-*errors inapps/test-app/App.tsx, which need theweak-node-api/ferric-examplebootstrap that the Lint job runs.pnpm testfails 4path-utilspermission tests, which are root-user artifacts (chmod 000doesn't stop root). These come frommainand pass on CI.The iOS build and simulator run are not reproducible without an Apple toolchain, so the three fixes above are reasoned from
react-native-test-app's source and GitHub's shell semantics rather than observed green. This PR needs theApple 🍎label for theTest app (iOS)job to run.Generated by Claude Code