diff --git a/packages/react-native/scripts/ios-prebuild.js b/packages/react-native/scripts/ios-prebuild.js index 82063be9565..0959f988cc5 100644 --- a/packages/react-native/scripts/ios-prebuild.js +++ b/packages/react-native/scripts/ios-prebuild.js @@ -17,6 +17,15 @@ const {execSync} = require('child_process'); const fs = require('fs'); const path = require('path'); +const REACT_NATIVE_PACKAGE_ROOT_FOLDER = path.join(__dirname, '..'); +const packageJsonPath = path.join( + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + 'package.json', +); + +// $FlowIgnore[unsupported-syntax] +const {version: currentVersion} = require(packageJsonPath); + async function main() { console.log('Prebuilding React Native iOS...'); console.log(''); @@ -93,6 +102,9 @@ async function main() { }); }; + // HERMES ARTIFACTS + await prepareHermesArtifactsAsync(currentVersion, 'release'); + // CODEGEN console.log('Running codegen...'); const codegenPath = path.join(root, '.build/codegen'); @@ -102,13 +114,6 @@ async function main() { console.log(command); execSync(command, {stdio: 'inherit'}); - // HERMES ARTIFACTS - console.log('Download hermes...'); - // Temporary hardcoded hermes version to make the script work - // We will make it right in a future diff. - // TODO: T223708709 - await prepareHermesArtifactsAsync('0.80.0-rc.0', 'debug'); - // LINKING link('Libraries/WebSocket/', 'React'); link('React/Base', 'React'); diff --git a/packages/react-native/scripts/ios-prebuild/hermes.js b/packages/react-native/scripts/ios-prebuild/hermes.js index bb1bd5f2954..50e414980de 100644 --- a/packages/react-native/scripts/ios-prebuild/hermes.js +++ b/packages/react-native/scripts/ios-prebuild/hermes.js @@ -12,75 +12,317 @@ const {execSync} = require('child_process'); const fs = require('fs'); const path = require('path'); +/** + * Downloads hermes artifacts from the specified version and build type. If you want to specify a specific + * version of hermes, use the HERMES_VERSION environment variable. The path to the artifacts will be inside + * the .build/artifacts/hermes folder, but this can be overridden by setting the HERMES_ENGINE_TARBALL_PATH + * environment variable. If this varuable is set, the script will use the local tarball instead of downloading it. + */ async function prepareHermesArtifactsAsync( version /*:string*/, - buildType /*:string*/, + buildType /*: 'debug' | 'release' */, ) /*: Promise */ { - // Check if the Hermes artifacts are already downloaded + hermesLog(`Preparing Hermes...`); + + // See if the user has set the HERMES_ENGINE_TARBALL_PATH environment variable + let localPath = process.env.HERMES_ENGINE_TARBALL_PATH ?? ''; + + // Create artifacts folder const artifactsPath /*: string*/ = path.resolve( process.cwd(), '.build', 'artifacts', 'hermes', ); - if (fs.existsSync(artifactsPath)) { - return artifactsPath; + + // Ensure that the artifacts folder exists + fs.mkdirSync(artifactsPath, {recursive: true}); + + // Path for keeping track of the current version in the artifacts folder + const versionFilePath = path.join(artifactsPath, 'version.txt'); + + // Only check if the artifacts folder exists if we are not using a local tarball + if (!localPath) { + // Resolve the version from the environment variable or use the default version + const resolvedVersion = process.env.HERMES_VERSION ?? version; + + // Check if the Hermes artifacts are already downloaded + if ( + checkExistingVersion( + versionFilePath, + resolvedVersion, + buildType, + artifactsPath, + ) + ) { + return artifactsPath; + } + + const sourceType = hermesSourceType(resolvedVersion, buildType); + localPath = resolveSourceFromSourceType( + sourceType, + resolvedVersion, + buildType, + artifactsPath, + ); + } else { + hermesLog('Using local tarball, skipping artifacts folder check'); + // Delete version.txt if it exists + if (fs.existsSync(versionFilePath)) { + fs.unlinkSync(versionFilePath); + } } - // Download the Hermes artifacts - const url = getHermesArtifactsUrl(version, buildType); - console.log(`Downloading Hermes artifacts from ${url}...`); + // Extract the tar.gz + execSync(`tar -xzf "${localPath}" -C "${artifactsPath}"`, { + stdio: 'inherit', + }); + + // Delete the tarball after extraction + if (!process.env.HERMES_ENGINE_TARBALL_PATH) { + fs.unlinkSync(localPath); + } - // download the file pointed to by the URL and store it in the ./.build/artifacts folder on disk - await downloadAndExtract(url, artifactsPath); return artifactsPath; } -async function downloadAndExtract(url /*:string*/, targetFolder /*:string*/) { - const buildDir = path.resolve('.build', 'artifacts'); - const tarballPath = path.join(buildDir, 'artifact.tar.gz'); +/*:: +type HermesEngineSourceType = + | 'local_prebuilt_tarball' + | 'download_prebuild_tarball' + | 'download_prebuilt_nightly_tarball' +*/ - // Ensure build directory exists - fs.mkdirSync(targetFolder, {recursive: true}); +const HermesEngineSourceTypes = { + LOCAL_PREBUILT_TARBALL: 'local_prebuilt_tarball', + DOWNLOAD_PREBUILD_TARBALL: 'download_prebuild_tarball', + DOWNLOAD_PREBUILT_NIGHTLY_TARBALL: 'download_prebuilt_nightly_tarball', +}; - console.log(`Downloading file from ${url} to ${tarballPath}...`); +/** + * Checks if the Hermes artifacts are already downloaded and up to date with the specified version. + * Returns true if the artifacts are up to date, false otherwise. + */ +function checkExistingVersion( + versionFilePath /*: string */, + version /*: string */, + buildType /*: 'debug' | 'release' */, + artifactsPath /*: string */, +) { + const resolvedVersion = `${version}-${buildType}`; + const hermesXCFramework = path.join( + artifactsPath, + 'destroot', + 'Libraries', + 'Frameworks', + 'universal', + 'hermes.xcframework', + ); - try { - // Download the file using curl via execSync - execSync(`curl -L "${url}" -o "${tarballPath}"`, {stdio: 'inherit'}); - - console.log('Download complete. Extracting...'); - - // Extract the tar.gz using execSync - execSync(`tar -xzf "${tarballPath}" -C "${targetFolder}"`, { - stdio: 'inherit', - }); - - // Delete the tarball after extraction - fs.unlinkSync(tarballPath); - - console.log('Download and extraction complete.'); - } catch (error) { - if (fs.existsSync(tarballPath)) { - fs.unlinkSync(tarballPath); + if (fs.existsSync(versionFilePath) && fs.existsSync(hermesXCFramework)) { + const versionFileContent = fs.readFileSync(versionFilePath, 'utf8'); + if (versionFileContent.trim() === resolvedVersion) { + hermesLog( + `Hermes artifacts already downloaded and up to date: ${artifactsPath}`, + ); + return true; } - throw new Error(`Failed to download or extract: ${error.message}`); + } + // If the version file does not exist or the version does not match, delete the artifacts folder + fs.rmSync(artifactsPath, {recursive: true, force: true}); + hermesLog( + `Hermes artifacts folder already exists, but version does not match. Deleting: ${artifactsPath}`, + ); + // Lets create the version.txt file + fs.mkdirSync(artifactsPath, {recursive: true}); + fs.writeFileSync(versionFilePath, resolvedVersion, 'utf8'); + hermesLog( + `Hermes artifacts folder created: ${artifactsPath} with version: ${resolvedVersion}`, + ); + return false; +} + +function hermesEngineTarballEnvvarDefined() /*: boolean */ { + return !!process.env.HERMES_ENGINE_TARBALL_PATH; +} + +function getTarballUrl( + version /*: string */, + buildType /*: 'debug' | 'release' */, +) /*: string */ { + const mavenRepoUrl = 'https://repo1.maven.org/maven2'; + const namespace = 'com/facebook/react'; + return `${mavenRepoUrl}/${namespace}/react-native-artifacts/${version}/react-native-artifacts-${version}-hermes-ios-${buildType}.tar.gz`; +} + +function getNightlyTarballUrl( + version /*: string */, + buildType /*: 'debug' | 'release' */, +) /*: string */ { + const params = `r=snapshots&g=com.facebook.react&a=react-native-artifacts&c=hermes-ios-${buildType}&e=tar.gz&v=${version}-SNAPSHOT`; + return resolveUrlRedirects( + `https://oss.sonatype.org/service/local/artifact/maven/redirect?${params}`, + ); +} + +function resolveUrlRedirects(url /*: string */) /*: string */ { + // Synchronously resolve the final URL after redirects using curl + try { + return execSync(`curl -Ls -o /dev/null -w '%{url_effective}' "${url}"`) + .toString() + .trim(); + } catch (e) { + hermesLog(`Failed to resolve URL redirects\n${e}`, 'error'); + return url; } } -function getHermesArtifactsUrl( - version /*:string*/, - buildType /*:string*/, -) /*:string*/ { - // Define the URL for the Hermes artifacts - // The URL format is: - // https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts//react-native-artifacts--hermes-ios-.tar.gz - // where is the version of React Native and is the build type (e.g., "debug" or "release") - // The Maven repository URL and namespace - // are hardcoded for simplicity, but they could be parameterized if needed - const maven_repo_url = 'https://repo1.maven.org/maven2'; - const namespace = 'com/facebook/react'; - return `${maven_repo_url}/${namespace}/react-native-artifacts/${version}/react-native-artifacts-${version}-hermes-ios-${buildType}.tar.gz`; +function hermesArtifactExists(tarballUrl /*: string */) /*: boolean */ { + try { + const code = execSync( + `curl -o /dev/null --silent -Iw '%{http_code}' -L "${tarballUrl}"`, + ) + .toString() + .trim(); + return code === '200'; + } catch (e) { + return false; + } +} + +function hermesSourceType( + version /*: string */, + buildType /*: 'debug' | 'release' */, +) /*: HermesEngineSourceType */ { + if (hermesEngineTarballEnvvarDefined()) { + hermesLog('Using local prebuild tarball'); + return HermesEngineSourceTypes.LOCAL_PREBUILT_TARBALL; + } + if (hermesArtifactExists(getTarballUrl(version, buildType))) { + hermesLog(`Using download prebuild ${buildType} tarball`); + return HermesEngineSourceTypes.DOWNLOAD_PREBUILD_TARBALL; + } + if ( + hermesArtifactExists( + getNightlyTarballUrl(version, buildType).replace(/\\/g, ''), + ) + ) { + hermesLog('Using download prebuild nightly tarball'); + return HermesEngineSourceTypes.DOWNLOAD_PREBUILT_NIGHTLY_TARBALL; + } + hermesLog( + 'Using download prebuild nightly tarball - this is a fallback and might not work.', + ); + return HermesEngineSourceTypes.DOWNLOAD_PREBUILT_NIGHTLY_TARBALL; +} + +function resolveSourceFromSourceType( + sourceType /*: HermesEngineSourceType */, + version /*: string */, + buildType /*: 'debug' | 'release' */, + artifactsPath /*: string*/, +) /*: string */ { + switch (sourceType) { + case HermesEngineSourceTypes.LOCAL_PREBUILT_TARBALL: + return localPrebuiltTarball(); + case HermesEngineSourceTypes.DOWNLOAD_PREBUILD_TARBALL: + return downloadPrebuildTarball(version, buildType, artifactsPath); + case HermesEngineSourceTypes.DOWNLOAD_PREBUILT_NIGHTLY_TARBALL: + return downloadPrebuiltNightlyTarball(version, buildType, artifactsPath); + default: + abort( + `[Hermes] Unsupported or invalid source type provided: ${sourceType}`, + ); + return ''; + } +} + +function localPrebuiltTarball() /*: string */ { + const tarballPath = process.env.HERMES_ENGINE_TARBALL_PATH; + if (tarballPath && fs.existsSync(tarballPath)) { + hermesLog( + `Using pre-built binary from local path defined by HERMES_ENGINE_TARBALL_PATH envvar: ${tarballPath}`, + ); + return `file://${tarballPath}`; + } + abort( + `[Hermes] HERMES_ENGINE_TARBALL_PATH is set, but points to a non-existing file: "${tarballPath ?? 'unknown'}"\nIf you don't want to use tarball, run 'unset HERMES_ENGINE_TARBALL_PATH'`, + ); + return ''; +} + +function downloadPrebuildTarball( + version /*: string */, + buildType /*: 'debug' | 'release' */, + artifactsPath /*: string*/, +) /*: string */ { + const url = getTarballUrl(version, buildType); + hermesLog(`Using release tarball from URL: ${url}`); + return downloadStableHermes(version, buildType, artifactsPath); +} + +function downloadPrebuiltNightlyTarball( + version /*: string */, + buildType /*: 'debug' | 'release' */, + artifactsPath /*: string*/, +) /*: string */ { + const url = getNightlyTarballUrl(version, buildType); + hermesLog(`Using nightly tarball from URL: ${url}`); + return downloadHermesTarball(url, version, buildType, artifactsPath); +} + +function downloadStableHermes( + version /*: string */, + buildType /*: 'debug' | 'release' */, + artifactsPath /*: string */, +) /*: string */ { + const tarballUrl = getTarballUrl(version, buildType); + return downloadHermesTarball(tarballUrl, version, buildType, artifactsPath); +} + +function downloadHermesTarball( + tarballUrl /*: string */, + version /*: string */, + buildType /*: 'debug' | 'release' */, + artifactsPath /*: string */, +) /*: string */ { + const destPath = buildType + ? `${artifactsPath}/hermes-ios-${version}-${buildType}.tar.gz` + : `${artifactsPath}/hermes-ios-${version}.tar.gz`; + if (!fs.existsSync(destPath)) { + const tmpFile = `${artifactsPath}/hermes-ios.download`; + try { + fs.mkdirSync(artifactsPath, {recursive: true}); + hermesLog(`Downloading Hermes tarball from ${tarballUrl}`); + execSync( + `curl "${tarballUrl}" -Lo "${tmpFile}" && mv "${tmpFile}" "${destPath}"`, + ); + } catch (e) { + abort(`Failed to download Hermes tarball from ${tarballUrl}`); + } + } + return destPath; +} + +function abort(message /*: string */) { + hermesLog(message, 'error'); + throw new Error(message); +} + +function hermesLog( + message /*: string */, + level /*: 'info' | 'warning' | 'error' */ = 'warning', +) { + // Simple log coloring for terminal output + const prefix = '[Hermes] '; + let colorFn = (x /*:string*/) => x; + if (process.stdout.isTTY) { + if (level === 'info') colorFn = x => `\x1b[32m${x}\x1b[0m`; + else if (level === 'error') colorFn = x => `\x1b[31m${x}\x1b[0m`; + else colorFn = x => `\x1b[33m${x}\x1b[0m`; + } + + console.log(colorFn(prefix + message)); } module.exports = { diff --git a/packages/react-native/scripts/ios-prebuild/utils.js b/packages/react-native/scripts/ios-prebuild/utils.js index 2455dcbaf87..efc5876c2ad 100644 --- a/packages/react-native/scripts/ios-prebuild/utils.js +++ b/packages/react-native/scripts/ios-prebuild/utils.js @@ -16,7 +16,7 @@ const fs = require('fs'); * @param {string} folderPath - The path to the folder * @returns {string} The path to the created or existing folder */ -function createFolderIfNotExists(folderPath /*:string*/) /*: string*/ { +function createFolderIfNotExists(folderPath /*:string*/) /*: string */ { if (!fs.existsSync(folderPath)) { fs.mkdirSync(folderPath, {recursive: true}); if (!fs.existsSync(folderPath)) { @@ -37,7 +37,4 @@ function throwIfOnEden() { throw new Error('Cannot prepare the iOS prebuilds on an Eden checkout'); } -module.exports = { - createFolderIfNotExists, - throwIfOnEden, -}; +module.exports = {createFolderIfNotExists, throwIfOnEden};