From e1280ee1e37d2a4244ea064dcb4d8ce67ec18cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 14 Feb 2026 21:47:18 +0100 Subject: [PATCH 01/16] Bundle, build and run iOS test app in separate steps in production --- .github/workflows/check.yml | 41 ++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6032127f..977a9861 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -162,6 +162,9 @@ jobs: if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Apple 🍎') name: Test app (iOS) runs-on: macos-latest + env: + DEVELOPER_DIR: /Applications/Xcode_26.2.app + IOS_DEVICE_NAME: iPhone 16 steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 @@ -178,28 +181,38 @@ jobs: uses: hendrikmuhs/ccache-action@v1.2.23 with: key: ${{ github.job }}-${{ runner.os }} - - name: Set up JDK 17 - uses: actions/setup-java@v5 - with: - java-version: "17" - distribution: "temurin" - - name: Setup Android SDK - uses: android-actions/setup-android@v4 - with: - packages: tools platform-tools ndk;${{ env.NDK_VERSION }} - - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim + - run: rustup target add aarch64-apple-ios-sim - run: pnpm install - run: pnpm run bootstrap env: CMAKE_RN_TRIPLETS: arm64;x86_64-apple-ios-sim FERRIC_TARGETS: aarch64-apple-ios-sim + # Because the Xcode project injects its own CCACHE_CONFIGPATH, + # the configuration set by the ccache action doesn't propagate. + - name: Expose Ccache config to Xcode + run: | + echo "CCACHE_DIR=`ccache --get-config cache_dir`" >> $GITHUB_ENV + echo "CCACHE_MAXSIZE=`ccache --get-config max_size`" >> $GITHUB_ENV + echo "CCACHE_COMPILERCHECK=`ccache --get-config compiler_check`" >> $GITHUB_ENV - run: pnpm run pod-install working-directory: apps/test-app - - name: Run tests (iOS) - run: pnpm run test:ios:allTests - # TODO: Enable release mode when it works - # run: pnpm run test:ios:allTests --mode Release + env: + USE_CCACHE: 1 + - name: Bundle test app + 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/res working-directory: apps/test-app + - name: Build test app + run: xcodebuild archive -workspace ReactTestApp.xcworkspace -configuration Release -scheme ReactTestApp -destination generic/platform="iOS" -archivePath ./build/test-app.xcarchive | xcbeautify + working-directory: apps/test-app/ios + - name: Run test app + run: | + # Install the app + xcrun simctl install booted ./ios/build/test-app.xcarchive/Products/Applications/ReactTestApp.app + # Run Mocha Remote wrapping the launch of the app + pnpm exec mocha-remote --exit-on-error -- xcrun simctl launch --terminate-running-process --console-pty booted com.microsoft.ReactTestApp + working-directory: apps/test-app + env: + MOCHA_REMOTE_CONTEXT: allTests test-macos: # Disabling this on main for now, as initializing the template takes a long time and # we don't have macOS-specific code yet. Currently also blocked on react-native-macos: From 9da35a2f17ee50637f2c02174ccc2037d37a0c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 14 Feb 2026 22:44:13 +0100 Subject: [PATCH 02/16] Boot the simulator and use an older xcode --- .github/workflows/check.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 977a9861..27e746d9 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -162,9 +162,6 @@ jobs: if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Apple 🍎') name: Test app (iOS) runs-on: macos-latest - env: - DEVELOPER_DIR: /Applications/Xcode_26.2.app - IOS_DEVICE_NAME: iPhone 16 steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 @@ -173,6 +170,8 @@ jobs: - uses: pnpm/action-setup@v6 with: cache: true + - name: Boot the simulator + run: xcrun simctl boot 'iPhone 17' - name: Setup cpp tools uses: aminya/setup-cpp@v1 with: From d41fa5f0fc1fd0c23a40b6c4421af3d39d5a01f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 23 Feb 2026 19:47:19 +0100 Subject: [PATCH 03/16] Print module path on framework slicing failure --- .changeset/spotty-readers-go.md | 5 +++++ packages/host/src/node/cli/apple.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/spotty-readers-go.md diff --git a/.changeset/spotty-readers-go.md b/.changeset/spotty-readers-go.md new file mode 100644 index 00000000..7375bfd4 --- /dev/null +++ b/.changeset/spotty-readers-go.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Print module path on framework slicing failure diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index 1defac91..ee2fe3cb 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -508,7 +508,7 @@ export async function linkXcframework({ }); assert( framework, - `Failed to find a framework slice matching: ${JSON.stringify(expectedSlice)}`, + `Failed to find a framework slice of '${modulePath}' matching: ${JSON.stringify(expectedSlice)}`, ); const originalFrameworkPath = path.join( From 315e0c171cb1d3db9eb740b14a3b465753c2af28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 23 Feb 2026 19:47:47 +0100 Subject: [PATCH 04/16] Add x86_64-apple-ios as a default target on an Apple host --- .changeset/true-ideas-retire.md | 5 +++++ .github/workflows/check.yml | 6 +++--- packages/ferric/src/build.ts | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 .changeset/true-ideas-retire.md diff --git a/.changeset/true-ideas-retire.md b/.changeset/true-ideas-retire.md new file mode 100644 index 00000000..eece23ac --- /dev/null +++ b/.changeset/true-ideas-retire.md @@ -0,0 +1,5 @@ +--- +"ferric-cli": patch +--- + +Add x86_64-apple-ios as a default target on an Apple host diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 27e746d9..6745738c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -117,7 +117,7 @@ jobs: uses: android-actions/setup-android@v4 with: packages: tools platform-tools ndk;${{ env.NDK_VERSION }} - - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim + - 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 - run: pnpm install - run: pnpm run bootstrap - run: pnpm test @@ -180,12 +180,12 @@ jobs: uses: hendrikmuhs/ccache-action@v1.2.23 with: key: ${{ github.job }}-${{ runner.os }} - - run: rustup target add aarch64-apple-ios-sim + - run: rustup target add aarch64-apple-ios-sim x86_64-apple-ios - run: pnpm install - run: pnpm run bootstrap env: CMAKE_RN_TRIPLETS: arm64;x86_64-apple-ios-sim - FERRIC_TARGETS: aarch64-apple-ios-sim + FERRIC_TARGETS: aarch64-apple-ios-sim,x86_64-apple-ios # Because the Xcode project injects its own CCACHE_CONFIGPATH, # the configuration set by the ccache action doesn't propagate. - name: Expose Ccache config to Xcode diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 3e5236ac..6cda0582 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -207,6 +207,7 @@ export const buildCommand = new Command("build") if (isAppleSupported()) { if (process.arch === "arm64") { targets.add("aarch64-apple-ios-sim"); + targets.add("x86_64-apple-ios"); } } logNotice( From 8bd3545c346325d2605f9bee23ec14ab8009cce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 23 Feb 2026 23:23:32 +0100 Subject: [PATCH 05/16] Use ad-hoc signing --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6745738c..fadd8142 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -201,7 +201,7 @@ jobs: 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/res working-directory: apps/test-app - name: Build test app - run: xcodebuild archive -workspace ReactTestApp.xcworkspace -configuration Release -scheme ReactTestApp -destination generic/platform="iOS" -archivePath ./build/test-app.xcarchive | xcbeautify + run: xcodebuild archive -workspace ReactTestApp.xcworkspace -configuration Release -scheme ReactTestApp -destination generic/platform="iOS" -archivePath ./build/test-app.xcarchive CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=YES | xcbeautify working-directory: apps/test-app/ios - name: Run test app run: | From eba01c802698b5851f7cb75d679f871b78d27008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 24 Feb 2026 08:53:14 +0100 Subject: [PATCH 06/16] Build app for "iOS Simulator" --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index fadd8142..666003c5 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -201,7 +201,7 @@ jobs: 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/res working-directory: apps/test-app - name: Build test app - run: xcodebuild archive -workspace ReactTestApp.xcworkspace -configuration Release -scheme ReactTestApp -destination generic/platform="iOS" -archivePath ./build/test-app.xcarchive CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=YES | xcbeautify + 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 | xcbeautify working-directory: apps/test-app/ios - name: Run test app run: | From 3b9e683089471e34357bcc5f3df7fbf45e20e885 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 13:18:53 +0000 Subject: [PATCH 07/16] 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 and app.json declares dist/assets for iOS. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww --- .github/workflows/check.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 666003c5..9d8fa582 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -193,13 +193,20 @@ jobs: echo "CCACHE_DIR=`ccache --get-config cache_dir`" >> $GITHUB_ENV echo "CCACHE_MAXSIZE=`ccache --get-config max_size`" >> $GITHUB_ENV echo "CCACHE_COMPILERCHECK=`ccache --get-config compiler_check`" >> $GITHUB_ENV + # Bundling has to happen *before* `pod install`: react-native-test-app + # resolves the resources declared in app.json when it generates the + # workspace, warning about and skipping the ones that don't exist yet. + # Bundling afterwards leaves the app without an embedded JS bundle, so it + # expects a Metro dev server at runtime. + # `--assets-dest dist` places the assets in dist/assets, where app.json + # expects them (dist/res is the Android convention). + - name: Bundle test app + 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 + working-directory: apps/test-app - run: pnpm run pod-install working-directory: apps/test-app env: USE_CCACHE: 1 - - name: Bundle test app - 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/res - working-directory: apps/test-app - name: Build test app 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 | xcbeautify working-directory: apps/test-app/ios From f68caab5e72c04f1cb7fc2b57a4aea87cbc73f29 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 13:19:14 +0000 Subject: [PATCH 08/16] 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 Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww --- .github/workflows/check.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 9d8fa582..d4b87e8e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -170,8 +170,20 @@ jobs: - uses: pnpm/action-setup@v6 with: cache: true + # The device lineup changes with every Xcode release, so pick the first + # available iPhone instead of hard-coding one. - name: Boot the simulator - run: xcrun simctl boot 'iPhone 17' + run: | + SIMULATOR_UDID=$(xcrun simctl list devices available --json | node -e " + const { devices } = JSON.parse(require('node:fs').readFileSync(0, 'utf8')); + const runtimes = Object.keys(devices).filter((r) => r.includes('iOS')).sort(); + const device = runtimes.flatMap((r) => devices[r]).find((d) => d.name.startsWith('iPhone')); + if (!device) throw new Error('Found no available iPhone simulator'); + console.log(device.udid); + ") + echo "Booting $SIMULATOR_UDID" + xcrun simctl bootstatus "$SIMULATOR_UDID" -b + echo "SIMULATOR_UDID=$SIMULATOR_UDID" >> $GITHUB_ENV - name: Setup cpp tools uses: aminya/setup-cpp@v1 with: @@ -213,9 +225,9 @@ jobs: - name: Run test app run: | # Install the app - xcrun simctl install booted ./ios/build/test-app.xcarchive/Products/Applications/ReactTestApp.app + xcrun simctl install "$SIMULATOR_UDID" ./ios/build/test-app.xcarchive/Products/Applications/ReactTestApp.app # Run Mocha Remote wrapping the launch of the app - pnpm exec mocha-remote --exit-on-error -- xcrun simctl launch --terminate-running-process --console-pty booted com.microsoft.ReactTestApp + pnpm exec mocha-remote --exit-on-error -- xcrun simctl launch --terminate-running-process --console-pty "$SIMULATOR_UDID" com.microsoft.ReactTestApp working-directory: apps/test-app env: MOCHA_REMOTE_CONTEXT: allTests From 26bcbb12175d9a993105b3727510c16a5781d6b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 13:19:28 +0000 Subject: [PATCH 09/16] 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 Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww --- .github/workflows/check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d4b87e8e..6fe4c873 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -220,6 +220,9 @@ jobs: env: USE_CCACHE: 1 - name: Build test app + # `shell: bash` for pipefail, so xcbeautify's exit code can't mask a + # failing xcodebuild. A bare `run:` uses `bash -e` without pipefail. + shell: bash 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 | xcbeautify working-directory: apps/test-app/ios - name: Run test app From 796427ec575c4f89558c5f8a52b9fd7f59f4b8f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 04:54:13 +0000 Subject: [PATCH 10/16] 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 Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww --- .github/workflows/check.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6fe4c873..36df1a94 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -223,7 +223,15 @@ jobs: # `shell: bash` for pipefail, so xcbeautify's exit code can't mask a # failing xcodebuild. A bare `run:` uses `bash -e` without pipefail. shell: bash - 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 | xcbeautify + # CLANG_ENABLE_EXPLICIT_MODULES=NO because USE_CCACHE makes React Native + # point CC/CXX at its ccache-clang.sh wrappers, which Xcode's explicit + # modules doesn't recognise as a compiler ("Explicit modules is enabled + # but the compiler was not recognized"), and every system module then + # fails to build. Remove once React Native configures ccache through + # C_COMPILER_LAUNCHER instead of overriding CC/CXX, which would let us + # keep explicit modules via + # CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER=YES. + 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 CLANG_ENABLE_EXPLICIT_MODULES=NO | xcbeautify working-directory: apps/test-app/ios - name: Run test app run: | From 1b21477d9d2faa739abea40590404983589ed3c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 05:06:44 +0000 Subject: [PATCH 11/16] 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 Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww --- .github/workflows/check.yml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 36df1a94..87c921da 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -198,8 +198,8 @@ jobs: env: CMAKE_RN_TRIPLETS: arm64;x86_64-apple-ios-sim FERRIC_TARGETS: aarch64-apple-ios-sim,x86_64-apple-ios - # Because the Xcode project injects its own CCACHE_CONFIGPATH, - # the configuration set by the ccache action doesn't propagate. + # Pin the ccache action's configuration into the environment, so it + # applies to the ccache that Xcode launches during the build too. - name: Expose Ccache config to Xcode run: | echo "CCACHE_DIR=`ccache --get-config cache_dir`" >> $GITHUB_ENV @@ -215,23 +215,25 @@ jobs: - name: Bundle test app 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 working-directory: apps/test-app + # Deliberately *not* setting USE_CCACHE here: that makes React Native + # point CC/CXX/LD/LDPLUSPLUS at scripts/xcode/ccache-clang{,++}.sh, which + # run `exec $CCACHE_BINARY clang` — a bare `clang` off PATH. This job puts + # an LLVM toolchain on PATH (setup-cpp), so that resolves to a compiler + # which is not Xcode's, and building against the iOS SDK then trips + # Apple's mismatch trap: "module '_c_standard_library_obsolete' requires + # feature 'found_incompatible_headers__check_search_paths'", followed by + # every system module failing. Ccache is wired up on the xcodebuild + # invocation below instead. - run: pnpm run pod-install working-directory: apps/test-app - env: - USE_CCACHE: 1 - name: Build test app # `shell: bash` for pipefail, so xcbeautify's exit code can't mask a # failing xcodebuild. A bare `run:` uses `bash -e` without pipefail. shell: bash - # CLANG_ENABLE_EXPLICIT_MODULES=NO because USE_CCACHE makes React Native - # point CC/CXX at its ccache-clang.sh wrappers, which Xcode's explicit - # modules doesn't recognise as a compiler ("Explicit modules is enabled - # but the compiler was not recognized"), and every system module then - # fails to build. Remove once React Native configures ccache through - # C_COMPILER_LAUNCHER instead of overriding CC/CXX, which would let us - # keep explicit modules via - # CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER=YES. - 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 CLANG_ENABLE_EXPLICIT_MODULES=NO | xcbeautify + # C_COMPILER_LAUNCHER keeps Xcode's own clang and merely prefixes it + # with ccache, so the compiler still matches the SDK. Explicit modules + # needs opting in for a launcher, hence the second setting. + 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 | xcbeautify working-directory: apps/test-app/ios - name: Run test app run: | From 505df1f6b5fe7a8180f438073626fa63b967361b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 05:24:16 +0000 Subject: [PATCH 12/16] 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 Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww --- .github/workflows/check.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 87c921da..b7bb0adf 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -224,8 +224,31 @@ jobs: # feature 'found_incompatible_headers__check_search_paths'", followed by # every system module failing. Ccache is wired up on the xcodebuild # invocation below instead. - - run: pnpm run pod-install + # + # CocoaPods also dies intermittently with "pathname contains null byte" + # while generating the Pods project. That is an unresolved upstream bug + # specific to pnpm monorepos (CocoaPods/CocoaPods#12866, #12798) which hit + # roughly half the runs on this branch, so retry — but only on that exact + # failure, leaving a genuine pod install error to fail the step, and + # annotate each retry so the flake stays visible rather than silently + # absorbed. Remove once a CocoaPods release later than 1.17.0 (the latest + # at the time of writing, and the one that fails) carries a fix. + - name: Install pods + shell: bash working-directory: apps/test-app + run: | + for attempt in 1 2 3; do + if pnpm run pod-install 2>&1 | tee "$RUNNER_TEMP/pod-install.log"; then + exit 0 + fi + if ! grep -q 'pathname contains null byte' "$RUNNER_TEMP/pod-install.log"; then + echo "::error::pod install failed for an unrelated reason" + exit 1 + fi + echo "::warning::pod install hit CocoaPods/CocoaPods#12866 (attempt ${attempt}/3), retrying" + done + echo "::error::pod install kept failing with CocoaPods/CocoaPods#12866" + exit 1 - name: Build test app # `shell: bash` for pipefail, so xcbeautify's exit code can't mask a # failing xcodebuild. A bare `run:` uses `bash -e` without pipefail. From 2695e09c616aca165d4239868bb6663cbef00f14 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 06:11:47 +0000 Subject: [PATCH 13/16] ci: stop piping the iOS xcodebuild through xcbeautify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww --- .github/workflows/check.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b7bb0adf..89c31e7d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -250,13 +250,17 @@ jobs: echo "::error::pod install kept failing with CocoaPods/CocoaPods#12866" exit 1 - name: Build test app - # `shell: bash` for pipefail, so xcbeautify's exit code can't mask a - # failing xcodebuild. A bare `run:` uses `bash -e` without pipefail. - shell: bash + # Not piped through xcbeautify, for the reasons #391 gave when dropping + # it from the macOS job: on a failure it prints the summary but not the + # failing phase's own output, and block-buffers what it does print, so + # the log arrives truncated and out of order. Raw xcodebuild output is + # verbose but complete and correctly ordered, and the step's exit code + # is xcodebuild's own, so no `set -o pipefail` guard is needed. + # # C_COMPILER_LAUNCHER keeps Xcode's own clang and merely prefixes it # with ccache, so the compiler still matches the SDK. Explicit modules # needs opting in for a launcher, hence the second setting. - 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 | xcbeautify + 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 working-directory: apps/test-app/ios - name: Run test app run: | From da7fc9aa308a584cef1c60a8dcee1f76d9e0e66b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 06:14:06 +0000 Subject: [PATCH 14/16] Trim the inline comments in the iOS job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/check.yml | 52 ++++++++++--------------------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 89c31e7d..060bab73 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -170,8 +170,7 @@ jobs: - uses: pnpm/action-setup@v6 with: cache: true - # The device lineup changes with every Xcode release, so pick the first - # available iPhone instead of hard-coding one. + # Device names change with every Xcode release, so pick one dynamically. - name: Boot the simulator run: | SIMULATOR_UDID=$(xcrun simctl list devices available --json | node -e " @@ -198,41 +197,25 @@ jobs: env: CMAKE_RN_TRIPLETS: arm64;x86_64-apple-ios-sim FERRIC_TARGETS: aarch64-apple-ios-sim,x86_64-apple-ios - # Pin the ccache action's configuration into the environment, so it - # applies to the ccache that Xcode launches during the build too. + # So the ccache action's config applies to the ccache Xcode launches too. - name: Expose Ccache config to Xcode run: | echo "CCACHE_DIR=`ccache --get-config cache_dir`" >> $GITHUB_ENV echo "CCACHE_MAXSIZE=`ccache --get-config max_size`" >> $GITHUB_ENV echo "CCACHE_COMPILERCHECK=`ccache --get-config compiler_check`" >> $GITHUB_ENV - # Bundling has to happen *before* `pod install`: react-native-test-app - # resolves the resources declared in app.json when it generates the - # workspace, warning about and skipping the ones that don't exist yet. - # Bundling afterwards leaves the app without an embedded JS bundle, so it - # expects a Metro dev server at runtime. - # `--assets-dest dist` places the assets in dist/assets, where app.json - # expects them (dist/res is the Android convention). + # Must precede `pod install`: react-native-test-app embeds the resources + # declared in app.json when generating the workspace, skipping missing + # ones, and the app would then expect a Metro dev server at runtime. + # `--assets-dest dist` lands assets in dist/assets, as app.json expects. - name: Bundle test app 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 working-directory: apps/test-app - # Deliberately *not* setting USE_CCACHE here: that makes React Native - # point CC/CXX/LD/LDPLUSPLUS at scripts/xcode/ccache-clang{,++}.sh, which - # run `exec $CCACHE_BINARY clang` — a bare `clang` off PATH. This job puts - # an LLVM toolchain on PATH (setup-cpp), so that resolves to a compiler - # which is not Xcode's, and building against the iOS SDK then trips - # Apple's mismatch trap: "module '_c_standard_library_obsolete' requires - # feature 'found_incompatible_headers__check_search_paths'", followed by - # every system module failing. Ccache is wired up on the xcodebuild - # invocation below instead. + # No USE_CCACHE: it points CC/CXX at React Native's ccache-clang.sh, whose + # bare `clang` resolves to setup-cpp's LLVM instead of Xcode's, breaking + # every system module. Ccache goes on the xcodebuild invocation instead. # - # CocoaPods also dies intermittently with "pathname contains null byte" - # while generating the Pods project. That is an unresolved upstream bug - # specific to pnpm monorepos (CocoaPods/CocoaPods#12866, #12798) which hit - # roughly half the runs on this branch, so retry — but only on that exact - # failure, leaving a genuine pod install error to fail the step, and - # annotate each retry so the flake stays visible rather than silently - # absorbed. Remove once a CocoaPods release later than 1.17.0 (the latest - # at the time of writing, and the one that fails) carries a fix. + # Retries the intermittent CocoaPods/CocoaPods#12866 "pathname contains + # null byte" crash, and only that. Drop once CocoaPods > 1.17.0 fixes it. - name: Install pods shell: bash working-directory: apps/test-app @@ -249,17 +232,10 @@ jobs: done echo "::error::pod install kept failing with CocoaPods/CocoaPods#12866" exit 1 + # Raw xcodebuild output, not piped through xcbeautify — see #391. + # C_COMPILER_LAUNCHER keeps Xcode's own clang and just prefixes it with + # ccache; explicit modules needs opting in when a launcher is used. - name: Build test app - # Not piped through xcbeautify, for the reasons #391 gave when dropping - # it from the macOS job: on a failure it prints the summary but not the - # failing phase's own output, and block-buffers what it does print, so - # the log arrives truncated and out of order. Raw xcodebuild output is - # verbose but complete and correctly ordered, and the step's exit code - # is xcodebuild's own, so no `set -o pipefail` guard is needed. - # - # C_COMPILER_LAUNCHER keeps Xcode's own clang and merely prefixes it - # with ccache, so the compiler still matches the SDK. Explicit modules - # needs opting in for a launcher, hence the second setting. 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 working-directory: apps/test-app/ios - name: Run test app From 4dba11047742c0b6f5c246878dd72aedf6cf0bc0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 06:17:52 +0000 Subject: [PATCH 15/16] Apply React Native's ccache tuning to the Xcode build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww --- .github/workflows/check.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 060bab73..e4e591db 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -197,12 +197,17 @@ jobs: env: CMAKE_RN_TRIPLETS: arm64;x86_64-apple-ios-sim FERRIC_TARGETS: aarch64-apple-ios-sim,x86_64-apple-ios - # So the ccache action's config applies to the ccache Xcode launches too. - - name: Expose Ccache config to Xcode + # Mirrors React Native's scripts/xcode/ccache.conf, which only applied + # while ccache ran through their wrapper script (it sets CCACHE_CONFIGPATH + # to it) and so no longer does now that it runs as a compiler launcher. + # Without this sloppiness Xcode's modules, PCH and index-store flags leave + # ccache treating almost every compile as uncacheable. + - name: Tune ccache for Xcode run: | - echo "CCACHE_DIR=`ccache --get-config cache_dir`" >> $GITHUB_ENV - echo "CCACHE_MAXSIZE=`ccache --get-config max_size`" >> $GITHUB_ENV - echo "CCACHE_COMPILERCHECK=`ccache --get-config compiler_check`" >> $GITHUB_ENV + 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 file_clone=true + ccache --set-config depend_mode=true + ccache --set-config inode_cache=true # Must precede `pod install`: react-native-test-app embeds the resources # declared in app.json when generating the workspace, skipping missing # ones, and the app would then expect a Metro dev server at runtime. From ec19c8b36b3481aad89daa41cfd9028875e3dbcd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 07:18:28 +0000 Subject: [PATCH 16/16] 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 Claude-Session: https://claude.ai/code/session_01UxzZtpa5Nz3F8ACXzX7Hww --- .github/workflows/check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e4e591db..71fcf106 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -191,6 +191,10 @@ jobs: uses: hendrikmuhs/ccache-action@v1.2.23 with: key: ${{ github.job }}-${{ runner.os }} + # The action defaults to 500M, which a from-source React Native build + # fills mid-build: it evicted 88 times in one run, discarding most of + # what it had just cached. + max-size: 3G - run: rustup target add aarch64-apple-ios-sim x86_64-apple-ios - run: pnpm install - run: pnpm run bootstrap