Skip to content

Commit a772a7c

Browse files
adityasingh2400meta-codesync[bot]
authored andcommitted
fix: make an interrupted React-Core-prebuilt swap recoverable (#57831)
Summary: `replace-rncore-version.js` writes `.last_build_configuration` only after it has already replaced `React.xcframework`, so a build cancelled between those two steps leaves the marker naming a flavor that is no longer on disk, and when the marker is absent entirely the script assumes the on-disk flavor is Debug. Either state makes the next build for that configuration take the "no need to replace" path and link against the other configuration's core, which fails with undefined C++ symbols for `Props`, `DebugStringConvertible` and the Fabric vtables, and nothing corrects it afterwards because the pod directory survives a clean. This change writes the marker before the framework is touched, using a value that is not a valid configuration, so a later run can tell that the previous swap did not finish and replaces again. The fresh install path now also records the configuration it assumed instead of leaving that state implicit, which is sound because a swap can no longer leave the marker missing. Fixes #57598. ## Changelog: [IOS] [FIXED] - Recover React-Core-prebuilt configuration swaps that were interrupted before the marker was updated Pull Request resolved: #57831 Test Plan: Four new cases in `packages/react-native/scripts/__tests__/replace-rncore-version-test.js`, driven through the real CLI entry point the podspec `[RNCore]` build phase uses, so no new export was needed and the production diff is logic only. Against unmodified upstream two of them fail, one with `ENOENT` on `.last_build_configuration` because the skip path never wrote the marker, and one showing the marker still naming the stale flavor after a failed swap. With the change the file is 8 passed, including all 4 pre-existing tests. `prettier --check` is clean on both files. The actual iOS link failure needs a prebuilt-core CocoaPods install and an Xcode build, so that was not reproduced locally. Reviewed By: cipolleschi Differential Revision: D114900617 Pulled By: fabriziocucci fbshipit-source-id: 5d7f9b7be9cf1dd1728daa1e98ff51b87b924d2c
1 parent 5821fca commit a772a7c

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

packages/react-native/scripts/__tests__/replace-rncore-version-test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ const path = require('node:path');
1919
const VERSION = '0.87.0-test';
2020
const SLICE = 'ios-arm64_x86_64-simulator';
2121
const BINARY = path.join(SLICE, 'React.framework', 'React');
22+
const SCRIPT = require.resolve('../replace-rncore-version.js');
23+
const MARKER = path.join('React-Core-prebuilt', '.last_build_configuration');
24+
25+
// Runs the script the way the "[RNCore] Replace React Native Core for the right
26+
// configuration" build phase does, from Pods/ and through the CLI entry point.
27+
function runScriptPhase(podsRoot, configuration) {
28+
return execFileSync(
29+
process.execPath,
30+
[SCRIPT, '-c', configuration, '-r', VERSION, '-p', podsRoot],
31+
{cwd: podsRoot, encoding: 'utf8'},
32+
);
33+
}
2234

2335
function writeFile(filePath, contents) {
2436
fs.mkdirSync(path.dirname(filePath), {recursive: true});
@@ -119,6 +131,73 @@ describe('replaceRNCoreConfiguration', () => {
119131
expect(fs.readFileSync(expoModuleMap, 'utf8')).toBe('module React {}\n');
120132
});
121133

134+
// Regression tests for #57598. The marker used to be written only after the
135+
// framework had already been replaced, so a build cancelled in between left it
136+
// naming a flavor that was no longer on disk. Every later build for that
137+
// flavor then took the "nothing to do" path and linked against the other
138+
// configuration's core, which fails with undefined C++ symbols and which a
139+
// clean does not undo because the pod directory survives it.
140+
describe('marker bookkeeping', () => {
141+
const marker = () => path.join(podsRoot, MARKER);
142+
const binary = () =>
143+
fs.readFileSync(path.join(pod, 'React.xcframework', BINARY), 'utf8');
144+
145+
it('records the configuration when it skips a fresh install', () => {
146+
// `pod install` leaves the debug flavor and no marker, so a Debug build
147+
// has nothing to swap. It still has to write down what is on disk,
148+
// otherwise the state stays implicit and stays unverifiable.
149+
expect(fs.existsSync(marker())).toBe(false);
150+
151+
runScriptPhase(podsRoot, 'Debug');
152+
153+
expect(fs.readFileSync(marker(), 'utf8')).toBe('Debug');
154+
expect(binary()).toBe('binary-Debug');
155+
});
156+
157+
it('invalidates the marker before it touches the framework', () => {
158+
fs.writeFileSync(marker(), 'Debug');
159+
// Drop the tarball so the Release run fails once it is already under way,
160+
// standing in for a build cancelled part way through the swap.
161+
fs.rmSync(
162+
path.join(
163+
podsRoot,
164+
'ReactNativeCore-artifacts',
165+
`reactnative-core-${VERSION.toLowerCase()}-release.tar.gz`,
166+
),
167+
);
168+
169+
expect(() => runScriptPhase(podsRoot, 'Release')).toThrow();
170+
171+
// The marker must no longer claim Debug: the framework may already have
172+
// been swapped, and a Debug build that trusts it would silently skip.
173+
expect(fs.readFileSync(marker(), 'utf8')).not.toBe('Debug');
174+
});
175+
176+
it('replaces the framework when the marker shows an unfinished swap', () => {
177+
buildTarball(podsRoot, 'Debug');
178+
// A swap that was interrupted: the Release flavor is on disk and the
179+
// marker never got its final value.
180+
replaceRNCoreConfiguration('Release', VERSION, podsRoot);
181+
fs.writeFileSync(marker(), 'in-progress');
182+
expect(binary()).toBe('binary-Release');
183+
184+
runScriptPhase(podsRoot, 'Debug');
185+
186+
expect(binary()).toBe('binary-Debug');
187+
expect(fs.readFileSync(marker(), 'utf8')).toBe('Debug');
188+
});
189+
190+
it('still skips when the marker already matches the configuration', () => {
191+
fs.writeFileSync(marker(), 'Release');
192+
const before = binary();
193+
194+
const output = runScriptPhase(podsRoot, 'Release');
195+
196+
expect(output).toContain('No need to replace React-Core-prebuilt');
197+
expect(binary()).toBe(before);
198+
});
199+
});
200+
122201
it('fails when the tarball has no React.xcframework', () => {
123202
const stage = fs.mkdtempSync(path.join(podsRoot, 'stage-bad-'));
124203
writeFile(path.join(stage, 'unrelated.txt'), 'nope');

packages/react-native/scripts/replace-rncore-version.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const yargs = require('yargs');
1818

1919
const LAST_BUILD_FILENAME = 'React-Core-prebuilt/.last_build_configuration';
2020

21+
// Not a valid configuration, so finding it means the swap did not finish.
22+
const REPLACEMENT_IN_PROGRESS = 'in-progress';
23+
2124
function validateBuildConfiguration(configuration /*: string */) {
2225
if (!['Debug', 'Release'].includes(configuration)) {
2326
throw new Error(`Invalid configuration ${configuration}`);
@@ -42,10 +45,12 @@ function shouldReplaceRnCoreConfiguration(configuration /*: string */) {
4245
);
4346
return false;
4447
}
48+
return true;
4549
}
4650

47-
// Assumption: if there is no stored last build, we assume that it was build for debug.
48-
if (!fileExists && configuration === 'Debug') {
51+
// With no marker the on-disk flavor is Debug: the podspec installs the debug
52+
// tarball (see resolve_podspec_source in scripts/cocoapods/rncore.rb).
53+
if (configuration === 'Debug') {
4954
console.log(
5055
'No previous build detected, but Debug Configuration. No need to replace React-Core-prebuilt',
5156
);
@@ -130,6 +135,10 @@ function updateLastBuildConfiguration(configuration /*: string */) {
130135
fs.writeFileSync(LAST_BUILD_FILENAME, configuration);
131136
}
132137

138+
function markReplacementInProgress() /*: void */ {
139+
fs.writeFileSync(LAST_BUILD_FILENAME, REPLACEMENT_IN_PROGRESS);
140+
}
141+
133142
function main(
134143
configuration /*: string */,
135144
version /*: string */,
@@ -139,9 +148,16 @@ function main(
139148
validateVersion(version);
140149

141150
if (!shouldReplaceRnCoreConfiguration(configuration)) {
151+
// A fresh install leaves no marker; record the flavor we skipped on.
152+
if (!fs.existsSync(LAST_BUILD_FILENAME)) {
153+
updateLastBuildConfiguration(configuration);
154+
}
142155
return;
143156
}
144157

158+
// Invalidate before touching the framework so an interrupted swap is
159+
// detectable on the next run.
160+
markReplacementInProgress();
145161
replaceRNCoreConfiguration(configuration, version, podsRoot);
146162
updateLastBuildConfiguration(configuration);
147163
console.log('Done replacing React Native prebuilt');

0 commit comments

Comments
 (0)