From d8f7639ce09818599b37051877be8193df84690d Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Tue, 3 Mar 2026 03:14:49 -0800 Subject: [PATCH] Fix silent tar extraction failure on EdenFS in replace-rncore-version (#55797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: On EdenFS, `tar -xf` extraction directly into an EdenFS-mounted directory silently produces incomplete results — only the first slice of the tarball (ios-arm64) gets extracted, while the remaining entries (catalyst, simulator, top-level Headers) have their directories created but files are missing. The `replace-rncore-version.js` script didn't check tar's exit code, so it would write the config file and subsequent builds would skip re-extraction. This fix extracts the tarball to a temporary directory on a regular filesystem first, verifies the extraction succeeded (exit code + expected file check), then moves the results into the final location. This is adapted from D94625297 (which targeted 0.83) to work with the current main branch, preserving the v0.84 behavior of only deleting subdirectories in `React-Core-prebuilt` (keeping files like `React-VFS.yaml`). ## Changelog: [IOS] [FIXED] - Fix silent tar extraction failure on EdenFS in replace-rncore-version.js by extracting to a temp directory first Pull Request resolved: https://github.com/facebook/react-native/pull/55797 Test Plan: Validated by the original patch author on EdenFS in D94625297. The logic is equivalent, adapted for the v0.84 code which preserves files in the target directory. Reviewed By: cortinico Differential Revision: D94653392 Pulled By: cipolleschi fbshipit-source-id: f2f1956b434586f0a83e1ec648a03dbac25333f5 --- .../scripts/replace-rncore-version.js | 87 +++++++++++++++---- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/packages/react-native/scripts/replace-rncore-version.js b/packages/react-native/scripts/replace-rncore-version.js index b2bcad5d9141..98d4798c6609 100644 --- a/packages/react-native/scripts/replace-rncore-version.js +++ b/packages/react-native/scripts/replace-rncore-version.js @@ -12,6 +12,8 @@ const {spawnSync} = require('child_process'); const fs = require('fs'); +const os = require('os'); +const path = require('path'); const yargs = require('yargs'); const LAST_BUILD_FILENAME = 'React-Core-prebuilt/.last_build_configuration'; @@ -63,22 +65,77 @@ function replaceRNCoreConfiguration( const finalLocation = 'React-Core-prebuilt'; - // Delete all directories - not files, since we want to keep the React-VFS.yaml file - const dirs = fs - .readdirSync(finalLocation, {withFileTypes: true}) - .filter(dirent => dirent.isDirectory()); - for (const dirent of dirs) { - const direntName = - typeof dirent.name === 'string' ? dirent.name : dirent.name.toString(); - const dirPath = `${finalLocation}/${direntName}`; - console.log('Removing directory', dirPath); - fs.rmSync(dirPath, {force: true, recursive: true}); - } + // Extract to a temporary directory on a regular filesystem first, then move + // into the final location. This avoids issues with partial tar extraction on + // certain filesystems (e.g. EdenFS) where extracting directly can silently + // produce incomplete results. + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rncore-')); + const tmpExtractDir = path.join(tmpDir, 'React-Core-prebuilt'); + fs.mkdirSync(tmpExtractDir, {recursive: true}); + + try { + console.log('Extracting the tarball to temp dir', tarballURLPath); + const result = spawnSync( + 'tar', + ['-xf', tarballURLPath, '-C', tmpExtractDir], + { + stdio: 'inherit', + }, + ); + + if (result.status !== 0) { + throw new Error(`tar extraction failed with exit code ${result.status}`); + } - console.log('Extracting the tarball', tarballURLPath); - spawnSync('tar', ['-xf', tarballURLPath, '-C', finalLocation], { - stdio: 'inherit', - }); + // Verify extraction produced the expected xcframework structure + const xcfwPath = path.join(tmpExtractDir, 'React.xcframework'); + const modulemapPath = path.join(xcfwPath, 'Modules', 'module.modulemap'); + if (!fs.existsSync(modulemapPath)) { + throw new Error( + `Extraction verification failed: ${modulemapPath} not found`, + ); + } + + // Delete all directories in finalLocation - not files, since we want to + // keep the React-VFS.yaml file + const dirs = fs + .readdirSync(finalLocation, {withFileTypes: true}) + .filter(dirent => dirent.isDirectory()); + for (const dirent of dirs) { + const direntName = + typeof dirent.name === 'string' ? dirent.name : dirent.name.toString(); + const dirPath = `${finalLocation}/${direntName}`; + console.log('Removing directory', dirPath); + fs.rmSync(dirPath, {force: true, recursive: true}); + } + + // Move extracted directories from temp to final location + const extractedEntries = fs + .readdirSync(tmpExtractDir, {withFileTypes: true}) + .filter(dirent => dirent.isDirectory()); + for (const dirent of extractedEntries) { + const direntName = + typeof dirent.name === 'string' ? dirent.name : dirent.name.toString(); + const src = path.join(tmpExtractDir, direntName); + const dst = path.join(finalLocation, direntName); + const mvResult = spawnSync('mv', [src, dst], {stdio: 'inherit'}); + if (mvResult.status !== 0) { + // Fallback: copy recursively then remove source + console.log(`mv failed for ${direntName}, falling back to cp -R`); + const cpResult = spawnSync('cp', ['-R', src, dst], { + stdio: 'inherit', + }); + if (cpResult.status !== 0) { + throw new Error( + `cp fallback failed with exit code ${cpResult.status}`, + ); + } + } + } + } finally { + // Clean up temp directory + fs.rmSync(tmpDir, {force: true, recursive: true}); + } } function updateLastBuildConfiguration(configuration /*: string */) {