add download/extract hermes prebuilts (#51216)

Summary:
When running the prebuild script:

`node scripts/ios-prebuild.js`

The script will now try to resolve and download a prebuilt version of hermes:

1. Hermes artifacts will be extracted to the `./build/artifacts/hermes` folder to ensure that Package.swift can find a version to link against.
2. The script checks the environment variable `HERMES_ENGINE_TARBALL_PATH` and tries do expand the tarball into the artifacts folder from 1
3. If not found, the script reads the hermes version from either the hidden environment-variable `HERMES_VERSION` and tries to download a release-tarball or a nightly tarball for this version. If the version does not exist, the script will fail.

Also added some extra logging features to the script.

bypass-github-export-checks

## Changelog:

[IOS] [ADDED] - Added downloading of hermes artifacts when pre-building for iOS.

Pull Request resolved: https://github.com/facebook/react-native/pull/51216

Test Plan:
1. Delete the `packages/react-native/.build` folder
2. Run the build script (provide a valid HERMES_ENGINE_TARBALL_PATH or a valid Hermes version (or nightly version)
3. Verify that the script successfully exits
4. Build the Package.swift in Xcode and verify that it finds and links the relevant Hermes files, verifying is done by the build succeeding.

Reviewed By: NickGerleman

Differential Revision: D74565936

Pulled By: cipolleschi

fbshipit-source-id: 5bd231999da24cfcce446150ac0fc1b5a4b6a4ae
This commit is contained in:
Christian Falch
2025-05-20 04:32:46 -07:00
committed by Facebook GitHub Bot
parent d6371bd6ac
commit 41d2b5de0a
3 changed files with 303 additions and 59 deletions
+12 -7
View File
@@ -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');
+289 -47
View File
@@ -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<string> */ {
// 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/<version>/react-native-artifacts-<version>-hermes-ios-<buildType>.tar.gz
// where <version> is the version of React Native and <buildType> 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 = {
+2 -5
View File
@@ -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};