Summary
replace-rncore-version.js (the [RNCore] Replace React Native Core for the right configuration script phase) silently keeps the Release prebuilt React.xcframework for a Debug build when no .last_build_configuration marker exists yet. The Release core is built with NDEBUG (RN_DEBUG_STRING_CONVERTIBLE=0), so a Debug app + from-source Fabric libraries fail to link.
This is a logic bug in the swap heuristic, not the (expected) fact that the prebuilt is shipped Release-by-default — the script is supposed to swap in the Debug variant and doesn't.
Environment
- react-native 0.85.3 (heuristic is identical on
main and 0.85-stable)
- Xcode 16 / iOS Simulator (arm64), New Architecture, Hermes
- Prebuilt React core in use (default:
RCT_USE_PREBUILT_RNCORE=1)
- Third-party Fabric libs built from source (expo-modules-core, react-native-gesture-handler, react-native-svg, react-native-screens, react-native-vision-camera, …)
Repro
- Fresh
pod install (extracts React-Core-prebuilt — the Release xcframework — and writes no .last_build_configuration).
- Build the app in Debug for the simulator.
Actual
Undefined symbols for architecture arm64:
"facebook::react::DebugStringConvertible::getDebugName() const", referenced from:
vtable for expo::ExpoViewShadowNode<...> in libExpoModulesCore.a
vtable for ...RNGestureHandlerButton... in libRNGestureHandler.a
"facebook::react::Sealable::ensureUnsealed() const", referenced from: ...
"typeinfo for facebook::react::BaseViewProps", ...
ld: symbol(s) not found for architecture arm64
Build log shows the swap script ran but decided to do nothing:
No previous build detected, but Debug Configuration. No need to replace React-Core-prebuilt
nm -gU React.framework/React | grep DebugStringConvertible → 0 symbols (Release core in place).
Root cause
scripts/replace-rncore-version.js:
// Assumption: if there is no stored last build, we assume that it was build for debug.
if (!fileExists && configuration === 'Debug') {
return false; // skip swap
}
The assumption is inverted from reality: rncore.rb extracts the Release variant by default, so "no marker" means Release on disk, not Debug. The first Debug build therefore keeps the Release core. (RN_DEBUG_STRING_CONVERTIBLE is derived from NDEBUG in react/debug/flags.h, so the Release core has these symbols compiled out.)
Because the bug depends on hidden marker / build-order state, it is non-deterministic: doing a Release build first (which writes marker=Release) makes the next Debug build swap correctly, masking the problem.
Why this matters / relation to #57166
This is the upstream trigger for the common REACT_NATIVE_PRODUCTION=1 post_install workaround people add to silence the link error — which in turn causes the ViewProps ABI runtime crash reported in #57166 (self-closed as "not a core bug" without identifying this heuristic). Fixing the swap removes the need for that flag entirely: the Debug core gets installed, so both the link error and the ABI crash go away.
Also see mrousavy/react-native-vision-camera#4018.
Proposed fix
Drop the unsafe assumption so the swap always runs when the marker is absent (one extra extraction on the first build only):
// No marker => unknown variant on disk (CocoaPods extracts Release by default),
// so replace unconditionally to guarantee the core matches `configuration`.
return true;
(Alternative, more conservative: have rncore.rb write .last_build_configuration reflecting the variant it actually extracts, so the heuristic is always accurate.)
I'm happy to open a PR with the one-line fix if this direction is acceptable.
Summary
replace-rncore-version.js(the[RNCore] Replace React Native Core for the right configurationscript phase) silently keeps the Release prebuiltReact.xcframeworkfor a Debug build when no.last_build_configurationmarker exists yet. The Release core is built withNDEBUG(RN_DEBUG_STRING_CONVERTIBLE=0), so a Debug app + from-source Fabric libraries fail to link.This is a logic bug in the swap heuristic, not the (expected) fact that the prebuilt is shipped Release-by-default — the script is supposed to swap in the Debug variant and doesn't.
Environment
mainand0.85-stable)RCT_USE_PREBUILT_RNCORE=1)Repro
pod install(extractsReact-Core-prebuilt— the Release xcframework — and writes no.last_build_configuration).Actual
Build log shows the swap script ran but decided to do nothing:
nm -gU React.framework/React | grep DebugStringConvertible→ 0 symbols (Release core in place).Root cause
scripts/replace-rncore-version.js:The assumption is inverted from reality:
rncore.rbextracts the Release variant by default, so "no marker" means Release on disk, not Debug. The first Debug build therefore keeps the Release core. (RN_DEBUG_STRING_CONVERTIBLEis derived fromNDEBUGinreact/debug/flags.h, so the Release core has these symbols compiled out.)Because the bug depends on hidden marker / build-order state, it is non-deterministic: doing a Release build first (which writes
marker=Release) makes the next Debug build swap correctly, masking the problem.Why this matters / relation to #57166
This is the upstream trigger for the common
REACT_NATIVE_PRODUCTION=1post_install workaround people add to silence the link error — which in turn causes the ViewProps ABI runtime crash reported in #57166 (self-closed as "not a core bug" without identifying this heuristic). Fixing the swap removes the need for that flag entirely: the Debug core gets installed, so both the link error and the ABI crash go away.Also see mrousavy/react-native-vision-camera#4018.
Proposed fix
Drop the unsafe assumption so the swap always runs when the marker is absent (one extra extraction on the first build only):
(Alternative, more conservative: have
rncore.rbwrite.last_build_configurationreflecting the variant it actually extracts, so the heuristic is always accurate.)I'm happy to open a PR with the one-line fix if this direction is acceptable.