Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ const path = require('node:path');
const VERSION = '0.87.0-test';
const SLICE = 'ios-arm64_x86_64-simulator';
const BINARY = path.join(SLICE, 'React.framework', 'React');
const SCRIPT = require.resolve('../replace-rncore-version.js');
const MARKER = path.join('React-Core-prebuilt', '.last_build_configuration');

// Runs the script the way the "[RNCore] Replace React Native Core for the right
// configuration" build phase does, from Pods/ and through the CLI entry point.
function runScriptPhase(podsRoot, configuration) {
return execFileSync(
process.execPath,
[SCRIPT, '-c', configuration, '-r', VERSION, '-p', podsRoot],
{cwd: podsRoot, encoding: 'utf8'},
);
}

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

// Regression tests for #57598. The marker used to be written only after the
// framework had already been replaced, so a build cancelled in between left it
// naming a flavor that was no longer on disk. Every later build for that
// flavor then took the "nothing to do" path and linked against the other
// configuration's core, which fails with undefined C++ symbols and which a
// clean does not undo because the pod directory survives it.
describe('marker bookkeeping', () => {
const marker = () => path.join(podsRoot, MARKER);
const binary = () =>
fs.readFileSync(path.join(pod, 'React.xcframework', BINARY), 'utf8');

it('records the configuration when it skips a fresh install', () => {
// `pod install` leaves the debug flavor and no marker, so a Debug build
// has nothing to swap. It still has to write down what is on disk,
// otherwise the state stays implicit and stays unverifiable.
expect(fs.existsSync(marker())).toBe(false);

runScriptPhase(podsRoot, 'Debug');

expect(fs.readFileSync(marker(), 'utf8')).toBe('Debug');
expect(binary()).toBe('binary-Debug');
});

it('invalidates the marker before it touches the framework', () => {
fs.writeFileSync(marker(), 'Debug');
// Drop the tarball so the Release run fails once it is already under way,
// standing in for a build cancelled part way through the swap.
fs.rmSync(
path.join(
podsRoot,
'ReactNativeCore-artifacts',
`reactnative-core-${VERSION.toLowerCase()}-release.tar.gz`,
),
);

expect(() => runScriptPhase(podsRoot, 'Release')).toThrow();

// The marker must no longer claim Debug: the framework may already have
// been swapped, and a Debug build that trusts it would silently skip.
expect(fs.readFileSync(marker(), 'utf8')).not.toBe('Debug');
});

it('replaces the framework when the marker shows an unfinished swap', () => {
buildTarball(podsRoot, 'Debug');
// A swap that was interrupted: the Release flavor is on disk and the
// marker never got its final value.
replaceRNCoreConfiguration('Release', VERSION, podsRoot);
fs.writeFileSync(marker(), 'in-progress');
expect(binary()).toBe('binary-Release');

runScriptPhase(podsRoot, 'Debug');

expect(binary()).toBe('binary-Debug');
expect(fs.readFileSync(marker(), 'utf8')).toBe('Debug');
});

it('still skips when the marker already matches the configuration', () => {
fs.writeFileSync(marker(), 'Release');
const before = binary();

const output = runScriptPhase(podsRoot, 'Release');

expect(output).toContain('No need to replace React-Core-prebuilt');
expect(binary()).toBe(before);
});
});

it('fails when the tarball has no React.xcframework', () => {
const stage = fs.mkdtempSync(path.join(podsRoot, 'stage-bad-'));
writeFile(path.join(stage, 'unrelated.txt'), 'nope');
Expand Down
30 changes: 28 additions & 2 deletions packages/react-native/scripts/replace-rncore-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ const yargs = require('yargs');

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

// Stored in LAST_BUILD_FILENAME while the framework on disk is being swapped.
// It is not a valid configuration, so a run that finds it knows the previous
// swap did not finish and the flavor on disk cannot be trusted.
const REPLACEMENT_IN_PROGRESS = 'in-progress';

function validateBuildConfiguration(configuration /*: string */) {
if (!['Debug', 'Release'].includes(configuration)) {
throw new Error(`Invalid configuration ${configuration}`);
Expand All @@ -42,13 +47,24 @@ function shouldReplaceRnCoreConfiguration(configuration /*: string */) {
);
return false;
}
// Anything else, including REPLACEMENT_IN_PROGRESS left by a swap that was
// cancelled or killed, means the flavor on disk is not the requested one or
// is unknown. Replace it.
return true;
}

// Assumption: if there is no stored last build, we assume that it was build for debug.
if (!fileExists && configuration === 'Debug') {
// No marker means `pod install` has just laid the pod down and nothing has
// swapped it since, because a swap records REPLACEMENT_IN_PROGRESS before it
// touches the framework. The podspec source is always the debug tarball (see
// resolve_podspec_source in scripts/cocoapods/rncore.rb), so the flavor on
// disk is Debug and a Debug build has nothing to do.
if (configuration === 'Debug') {
console.log(
'No previous build detected, but Debug Configuration. No need to replace React-Core-prebuilt',
);
// Record the assumption rather than leaving it implicit, so the state is
// readable and a later run never has to make it again.
updateLastBuildConfiguration(configuration);
return false;
}

Expand Down Expand Up @@ -130,6 +146,10 @@ function updateLastBuildConfiguration(configuration /*: string */) {
fs.writeFileSync(LAST_BUILD_FILENAME, configuration);
}

function markReplacementInProgress() /*: void */ {
fs.writeFileSync(LAST_BUILD_FILENAME, REPLACEMENT_IN_PROGRESS);
}

function main(
configuration /*: string */,
version /*: string */,
Expand All @@ -142,6 +162,12 @@ function main(
return;
}

// Invalidate the marker before the framework is touched. A build cancelled
// between the swap and the update used to leave the marker naming a flavor
// that was no longer on disk, and every later build for that flavor then took
// the skip path and linked against the wrong core, which a clean does not
// undo. Recording the swap first makes an interrupted run recoverable.
markReplacementInProgress();
replaceRNCoreConfiguration(configuration, version, podsRoot);
updateLastBuildConfiguration(configuration);
console.log('Done replacing React Native prebuilt');
Expand Down