Skip to content

Commit 37e8081

Browse files
kraenhansenclaude
andauthored
Bundle, build and run iOS test app in separate steps in production + ccache enabled (#427)
* Bundle, build and run iOS test app in separate steps in production * Boot the simulator and use an older xcode * Print module path on framework slicing failure * Add x86_64-apple-ios as a default target on an Apple host * Use ad-hoc signing * Build app for "iOS Simulator" * Bundle the test app before running pod install 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 * Pick an available iPhone simulator instead of hard-coding one "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 * Don't let xcbeautify mask a failing xcodebuild 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 * Disable explicit modules so the ccache build works 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 * Wire ccache through C_COMPILER_LAUNCHER instead of USE_CCACHE 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 * Retry pod install on the CocoaPods null-byte flake 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 * ci: stop piping the iOS xcodebuild through xcbeautify 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 * Trim the inline comments in the iOS job 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. * Apply React Native's ccache tuning to the Xcode build 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 * Give the iOS ccache room to hold a whole build 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f4f7afc commit 37e8081

5 files changed

Lines changed: 85 additions & 18 deletions

File tree

.changeset/spotty-readers-go.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-native-node-api": patch
3+
---
4+
5+
Print module path on framework slicing failure

.changeset/true-ideas-retire.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ferric-cli": patch
3+
---
4+
5+
Add x86_64-apple-ios as a default target on an Apple host

.github/workflows/check.yml

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ jobs:
117117
uses: android-actions/setup-android@v4
118118
with:
119119
packages: tools platform-tools ndk;${{ env.NDK_VERSION }}
120-
- run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim
120+
- run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim x86_64-apple-ios
121121
- run: pnpm install
122122
- run: pnpm run bootstrap
123123
- run: pnpm test
@@ -170,6 +170,19 @@ jobs:
170170
- uses: pnpm/action-setup@v6
171171
with:
172172
cache: true
173+
# Device names change with every Xcode release, so pick one dynamically.
174+
- name: Boot the simulator
175+
run: |
176+
SIMULATOR_UDID=$(xcrun simctl list devices available --json | node -e "
177+
const { devices } = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));
178+
const runtimes = Object.keys(devices).filter((r) => r.includes('iOS')).sort();
179+
const device = runtimes.flatMap((r) => devices[r]).find((d) => d.name.startsWith('iPhone'));
180+
if (!device) throw new Error('Found no available iPhone simulator');
181+
console.log(device.udid);
182+
")
183+
echo "Booting $SIMULATOR_UDID"
184+
xcrun simctl bootstatus "$SIMULATOR_UDID" -b
185+
echo "SIMULATOR_UDID=$SIMULATOR_UDID" >> $GITHUB_ENV
173186
- name: Setup cpp tools
174187
uses: aminya/setup-cpp@v1
175188
with:
@@ -178,28 +191,71 @@ jobs:
178191
uses: hendrikmuhs/ccache-action@v1.2.23
179192
with:
180193
key: ${{ github.job }}-${{ runner.os }}
181-
- name: Set up JDK 17
182-
uses: actions/setup-java@v5
183-
with:
184-
java-version: "17"
185-
distribution: "temurin"
186-
- name: Setup Android SDK
187-
uses: android-actions/setup-android@v4
188-
with:
189-
packages: tools platform-tools ndk;${{ env.NDK_VERSION }}
190-
- run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim
194+
# The action defaults to 500M, which a from-source React Native build
195+
# fills mid-build: it evicted 88 times in one run, discarding most of
196+
# what it had just cached.
197+
max-size: 3G
198+
- run: rustup target add aarch64-apple-ios-sim x86_64-apple-ios
191199
- run: pnpm install
192200
- run: pnpm run bootstrap
193201
env:
194202
CMAKE_RN_TRIPLETS: arm64;x86_64-apple-ios-sim
195-
FERRIC_TARGETS: aarch64-apple-ios-sim
196-
- run: pnpm run pod-install
203+
FERRIC_TARGETS: aarch64-apple-ios-sim,x86_64-apple-ios
204+
# Mirrors React Native's scripts/xcode/ccache.conf, which only applied
205+
# while ccache ran through their wrapper script (it sets CCACHE_CONFIGPATH
206+
# to it) and so no longer does now that it runs as a compiler launcher.
207+
# Without this sloppiness Xcode's modules, PCH and index-store flags leave
208+
# ccache treating almost every compile as uncacheable.
209+
- name: Tune ccache for Xcode
210+
run: |
211+
ccache --set-config sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
212+
ccache --set-config file_clone=true
213+
ccache --set-config depend_mode=true
214+
ccache --set-config inode_cache=true
215+
# Must precede `pod install`: react-native-test-app embeds the resources
216+
# declared in app.json when generating the workspace, skipping missing
217+
# ones, and the app would then expect a Metro dev server at runtime.
218+
# `--assets-dest dist` lands assets in dist/assets, as app.json expects.
219+
- name: Bundle test app
220+
run: pnpm exec react-native bundle --entry-file index.ts --platform ios --dev false --minify false --bundle-output dist/main.ios.jsbundle --assets-dest dist
197221
working-directory: apps/test-app
198-
- name: Run tests (iOS)
199-
run: pnpm run test:ios:allTests
200-
# TODO: Enable release mode when it works
201-
# run: pnpm run test:ios:allTests --mode Release
222+
# No USE_CCACHE: it points CC/CXX at React Native's ccache-clang.sh, whose
223+
# bare `clang` resolves to setup-cpp's LLVM instead of Xcode's, breaking
224+
# every system module. Ccache goes on the xcodebuild invocation instead.
225+
#
226+
# Retries the intermittent CocoaPods/CocoaPods#12866 "pathname contains
227+
# null byte" crash, and only that. Drop once CocoaPods > 1.17.0 fixes it.
228+
- name: Install pods
229+
shell: bash
202230
working-directory: apps/test-app
231+
run: |
232+
for attempt in 1 2 3; do
233+
if pnpm run pod-install 2>&1 | tee "$RUNNER_TEMP/pod-install.log"; then
234+
exit 0
235+
fi
236+
if ! grep -q 'pathname contains null byte' "$RUNNER_TEMP/pod-install.log"; then
237+
echo "::error::pod install failed for an unrelated reason"
238+
exit 1
239+
fi
240+
echo "::warning::pod install hit CocoaPods/CocoaPods#12866 (attempt ${attempt}/3), retrying"
241+
done
242+
echo "::error::pod install kept failing with CocoaPods/CocoaPods#12866"
243+
exit 1
244+
# Raw xcodebuild output, not piped through xcbeautify — see #391.
245+
# C_COMPILER_LAUNCHER keeps Xcode's own clang and just prefixes it with
246+
# ccache; explicit modules needs opting in when a launcher is used.
247+
- name: Build test app
248+
run: xcodebuild archive -workspace ReactTestApp.xcworkspace -configuration Release -scheme ReactTestApp -destination "generic/platform=iOS Simulator" -archivePath ./build/test-app.xcarchive CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=YES C_COMPILER_LAUNCHER=ccache CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER=YES
249+
working-directory: apps/test-app/ios
250+
- name: Run test app
251+
run: |
252+
# Install the app
253+
xcrun simctl install "$SIMULATOR_UDID" ./ios/build/test-app.xcarchive/Products/Applications/ReactTestApp.app
254+
# Run Mocha Remote wrapping the launch of the app
255+
pnpm exec mocha-remote --exit-on-error -- xcrun simctl launch --terminate-running-process --console-pty "$SIMULATOR_UDID" com.microsoft.ReactTestApp
256+
working-directory: apps/test-app
257+
env:
258+
MOCHA_REMOTE_CONTEXT: allTests
203259
test-macos:
204260
# Disabling this on main for now, as initializing the template takes a long time and
205261
# we don't have macOS-specific code yet. Currently also blocked on react-native-macos:

packages/ferric/src/build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ export const buildCommand = new Command("build")
207207
if (isAppleSupported()) {
208208
if (process.arch === "arm64") {
209209
targets.add("aarch64-apple-ios-sim");
210+
targets.add("x86_64-apple-ios");
210211
}
211212
}
212213
logNotice(

packages/host/src/node/cli/apple.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ export async function linkXcframework({
508508
});
509509
assert(
510510
framework,
511-
`Failed to find a framework slice matching: ${JSON.stringify(expectedSlice)}`,
511+
`Failed to find a framework slice of '${modulePath}' matching: ${JSON.stringify(expectedSlice)}`,
512512
);
513513

514514
const originalFrameworkPath = path.join(

0 commit comments

Comments
 (0)