Description
scripts/replace-rncore-version.js (the "[RNDeps] Replace React Native Core for the right configuration" build phase for the prebuilt iOS core) decides whether to swap the extracted React-Core-prebuilt flavor by comparing the requested configuration against the .last_build_configuration marker file. When the marker file is missing, it assumes the on-disk flavor is Debug:
// Assumption: if there is no stored last build, we assume that it was build for debug.
if (!fileExists && configuration === 'Debug') {
console.log(
'No previous build detected, but Debug Configuration. No need to replace React-Core-prebuilt',
);
return false;
}
Two problems compound here:
- The skip branch never writes the marker. If the assumption is wrong — the framework on disk is actually the Release flavor — every subsequent Debug build takes this same branch forever. The state is self-perpetuating: nothing ever corrects it, including
xcodebuild clean.
- The swap is not atomic.
main() runs replaceRNCoreConfiguration() (tar extraction) first and updateLastBuildConfiguration() (marker write) after. A build cancelled/killed between the two leaves a Release-flavored framework with no marker — exactly the state that trips problem 1 on the next Debug build.
Both are still present on main as of today (the extraction itself was recently hardened with temp-dir extraction and verification, but the missing-marker assumption and the extract-then-mark ordering are unchanged).
Why the resulting failure is hard to diagnose
A Debug link against the Release-flavored core fails with undefined C++ symbols:
Undefined symbols for architecture arm64:
"VTT for facebook::react::Props" / "VTT for facebook::react::BaseViewProps" /
"VTT for facebook::react::YogaStylableProps" /
"vtable for facebook::react::DebugStringConvertible"
referenced from: construction vtables in libRNSVG.a, libRNGestureHandler.a,
libExpoModulesCore.a, ... (any Fabric component archive)
"_OBJC_CLASS_$_RCTPackagerConnection", "_OBJC_CLASS_$_RCTPerfMonitor",
facebook::hermes::inspector_modern::chrome::enableDebugging(...), ...
Mechanism: without NDEBUG, react/debug/flags.h defines REACT_NATIVE_DEBUG → RN_DEBUG_STRING_CONVERTIBLE=1, which gives Props/DebugStringConvertible out-of-line virtual methods. Debug-compiled third-party Fabric pods therefore emit external references to those vtables/VTTs — and only the debug flavor of the prebuilt core exports them (verified with nm: the release flavor exports neither the three VTTs nor DebugStringConvertible's vtable, nor the debug-only ObjC/inspector classes). Release-configuration compiles never reference them (the vtables are emitted with vague linkage into every archive), which is why Release builds keep working — making it look like the last added native dependency broke Debug builds. In our case it was misattributed to react-native-svg for a full day.
Steps to reproduce
In any RN 0.81+ iOS app using the prebuilt core (RCT_USE_PREBUILT_RNCORE=1, the default with prebuilt artifacts present):
- Build Release for simulator once — the phase swaps
Pods/React-Core-prebuilt to the release flavor and writes .last_build_configuration = Release.
- Simulate the interrupted-swap window (or any cleanup tool touching Pods):
rm ios/Pods/React-Core-prebuilt/.last_build_configuration
- Build Debug. The phase logs
No previous build detected, but Debug Configuration. No need to replace React-Core-prebuilt and the link fails with the undefined facebook::react vtables/VTTs above.
- Every subsequent Debug build fails identically;
clean does not help. (Recovery is also confounded by the stale copy under Build/Products/…/XCFrameworkIntermediates/React-Core-prebuilt, which the CocoaPods copy-xcframeworks phase can skip refreshing and which clean never removes — but the root defect is the marker logic above.)
Conversely, an interrupted Debug→Release swap silently ships the debug-flavored core in a Release build (links fine, assertions on) — the same defect with a quieter failure mode.
Suggested fix
Any of these would close the hole:
- Write the marker at initial extraction time (podspec
prepare_command extracts the debug tarball — writing Debug there makes "marker missing" impossible in a healthy tree), and/or
- Treat a missing marker as unknown and perform the replace instead of assuming Debug (one redundant extraction on first build vs. an unrecoverable wedge), and
- Write the marker's previous value → remove-before-extract or write marker only after successful extraction within the same guarded operation, so an interrupted swap re-runs rather than wedges.
React Native Version
0.81.5 (Expo SDK 54; repro'd on macOS 26.5.1 / Xcode 26.6, new architecture enabled). The relevant code is unchanged on main.
Affected Platforms
Build - iOS (simulator and device Debug builds)
Description
scripts/replace-rncore-version.js(the "[RNDeps] Replace React Native Core for the right configuration" build phase for the prebuilt iOS core) decides whether to swap the extractedReact-Core-prebuiltflavor by comparing the requested configuration against the.last_build_configurationmarker file. When the marker file is missing, it assumes the on-disk flavor is Debug:Two problems compound here:
xcodebuild clean.main()runsreplaceRNCoreConfiguration()(tar extraction) first andupdateLastBuildConfiguration()(marker write) after. A build cancelled/killed between the two leaves a Release-flavored framework with no marker — exactly the state that trips problem 1 on the next Debug build.Both are still present on
mainas of today (the extraction itself was recently hardened with temp-dir extraction and verification, but the missing-marker assumption and the extract-then-mark ordering are unchanged).Why the resulting failure is hard to diagnose
A Debug link against the Release-flavored core fails with undefined C++ symbols:
Mechanism: without
NDEBUG,react/debug/flags.hdefinesREACT_NATIVE_DEBUG→RN_DEBUG_STRING_CONVERTIBLE=1, which givesProps/DebugStringConvertibleout-of-line virtual methods. Debug-compiled third-party Fabric pods therefore emit external references to those vtables/VTTs — and only the debug flavor of the prebuilt core exports them (verified withnm: the release flavor exports neither the three VTTs norDebugStringConvertible's vtable, nor the debug-only ObjC/inspector classes). Release-configuration compiles never reference them (the vtables are emitted with vague linkage into every archive), which is why Release builds keep working — making it look like the last added native dependency broke Debug builds. In our case it was misattributed toreact-native-svgfor a full day.Steps to reproduce
In any RN 0.81+ iOS app using the prebuilt core (
RCT_USE_PREBUILT_RNCORE=1, the default with prebuilt artifacts present):Pods/React-Core-prebuiltto the release flavor and writes.last_build_configuration=Release.rm ios/Pods/React-Core-prebuilt/.last_build_configurationNo previous build detected, but Debug Configuration. No need to replace React-Core-prebuiltand the link fails with the undefinedfacebook::reactvtables/VTTs above.cleandoes not help. (Recovery is also confounded by the stale copy underBuild/Products/…/XCFrameworkIntermediates/React-Core-prebuilt, which the CocoaPods copy-xcframeworks phase can skip refreshing and whichcleannever removes — but the root defect is the marker logic above.)Conversely, an interrupted Debug→Release swap silently ships the debug-flavored core in a Release build (links fine, assertions on) — the same defect with a quieter failure mode.
Suggested fix
Any of these would close the hole:
prepare_commandextracts the debug tarball — writingDebugthere makes "marker missing" impossible in a healthy tree), and/orReact Native Version
0.81.5 (Expo SDK 54; repro'd on macOS 26.5.1 / Xcode 26.6, new architecture enabled). The relevant code is unchanged on
main.Affected Platforms
Build - iOS (simulator and device Debug builds)