Skip to content

Commit 41d2b5d

Browse files
chrfalchfacebook-github-bot
authored andcommitted
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: #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
1 parent d6371bd commit 41d2b5d

3 files changed

Lines changed: 298 additions & 54 deletions

File tree

packages/react-native/scripts/ios-prebuild.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ const {execSync} = require('child_process');
1717
const fs = require('fs');
1818
const path = require('path');
1919

20+
const REACT_NATIVE_PACKAGE_ROOT_FOLDER = path.join(__dirname, '..');
21+
const packageJsonPath = path.join(
22+
REACT_NATIVE_PACKAGE_ROOT_FOLDER,
23+
'package.json',
24+
);
25+
26+
// $FlowIgnore[unsupported-syntax]
27+
const {version: currentVersion} = require(packageJsonPath);
28+
2029
async function main() {
2130
console.log('Prebuilding React Native iOS...');
2231
console.log('');
@@ -93,6 +102,9 @@ async function main() {
93102
});
94103
};
95104

105+
// HERMES ARTIFACTS
106+
await prepareHermesArtifactsAsync(currentVersion, 'release');
107+
96108
// CODEGEN
97109
console.log('Running codegen...');
98110
const codegenPath = path.join(root, '.build/codegen');
@@ -102,13 +114,6 @@ async function main() {
102114
console.log(command);
103115
execSync(command, {stdio: 'inherit'});
104116

105-
// HERMES ARTIFACTS
106-
console.log('Download hermes...');
107-
// Temporary hardcoded hermes version to make the script work
108-
// We will make it right in a future diff.
109-
// TODO: T223708709
110-
await prepareHermesArtifactsAsync('0.80.0-rc.0', 'debug');
111-
112117
// LINKING
113118
link('Libraries/WebSocket/', 'React');
114119
link('React/Base', 'React');

packages/react-native/scripts/ios-prebuild/hermes.js

Lines changed: 284 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,75 +12,317 @@ const {execSync} = require('child_process');
1212
const fs = require('fs');
1313
const path = require('path');
1414

15+
/**
16+
* Downloads hermes artifacts from the specified version and build type. If you want to specify a specific
17+
* version of hermes, use the HERMES_VERSION environment variable. The path to the artifacts will be inside
18+
* the .build/artifacts/hermes folder, but this can be overridden by setting the HERMES_ENGINE_TARBALL_PATH
19+
* environment variable. If this varuable is set, the script will use the local tarball instead of downloading it.
20+
*/
1521
async function prepareHermesArtifactsAsync(
1622
version /*:string*/,
17-
buildType /*:string*/,
23+
buildType /*: 'debug' | 'release' */,
1824
) /*: Promise<string> */ {
19-
// Check if the Hermes artifacts are already downloaded
25+
hermesLog(`Preparing Hermes...`);
26+
27+
// See if the user has set the HERMES_ENGINE_TARBALL_PATH environment variable
28+
let localPath = process.env.HERMES_ENGINE_TARBALL_PATH ?? '';
29+
30+
// Create artifacts folder
2031
const artifactsPath /*: string*/ = path.resolve(
2132
process.cwd(),
2233
'.build',
2334
'artifacts',
2435
'hermes',
2536
);
26-
if (fs.existsSync(artifactsPath)) {
27-
return artifactsPath;
37+
38+
// Ensure that the artifacts folder exists
39+
fs.mkdirSync(artifactsPath, {recursive: true});
40+
41+
// Path for keeping track of the current version in the artifacts folder
42+
const versionFilePath = path.join(artifactsPath, 'version.txt');
43+
44+
// Only check if the artifacts folder exists if we are not using a local tarball
45+
if (!localPath) {
46+
// Resolve the version from the environment variable or use the default version
47+
const resolvedVersion = process.env.HERMES_VERSION ?? version;
48+
49+
// Check if the Hermes artifacts are already downloaded
50+
if (
51+
checkExistingVersion(
52+
versionFilePath,
53+
resolvedVersion,
54+
buildType,
55+
artifactsPath,
56+
)
57+
) {
58+
return artifactsPath;
59+
}
60+
61+
const sourceType = hermesSourceType(resolvedVersion, buildType);
62+
localPath = resolveSourceFromSourceType(
63+
sourceType,
64+
resolvedVersion,
65+
buildType,
66+
artifactsPath,
67+
);
68+
} else {
69+
hermesLog('Using local tarball, skipping artifacts folder check');
70+
// Delete version.txt if it exists
71+
if (fs.existsSync(versionFilePath)) {
72+
fs.unlinkSync(versionFilePath);
73+
}
2874
}
2975

30-
// Download the Hermes artifacts
31-
const url = getHermesArtifactsUrl(version, buildType);
32-
console.log(`Downloading Hermes artifacts from ${url}...`);
76+
// Extract the tar.gz
77+
execSync(`tar -xzf "${localPath}" -C "${artifactsPath}"`, {
78+
stdio: 'inherit',
79+
});
80+
81+
// Delete the tarball after extraction
82+
if (!process.env.HERMES_ENGINE_TARBALL_PATH) {
83+
fs.unlinkSync(localPath);
84+
}
3385

34-
// download the file pointed to by the URL and store it in the ./.build/artifacts folder on disk
35-
await downloadAndExtract(url, artifactsPath);
3686
return artifactsPath;
3787
}
3888

39-
async function downloadAndExtract(url /*:string*/, targetFolder /*:string*/) {
40-
const buildDir = path.resolve('.build', 'artifacts');
41-
const tarballPath = path.join(buildDir, 'artifact.tar.gz');
89+
/*::
90+
type HermesEngineSourceType =
91+
| 'local_prebuilt_tarball'
92+
| 'download_prebuild_tarball'
93+
| 'download_prebuilt_nightly_tarball'
94+
*/
95+
96+
const HermesEngineSourceTypes = {
97+
LOCAL_PREBUILT_TARBALL: 'local_prebuilt_tarball',
98+
DOWNLOAD_PREBUILD_TARBALL: 'download_prebuild_tarball',
99+
DOWNLOAD_PREBUILT_NIGHTLY_TARBALL: 'download_prebuilt_nightly_tarball',
100+
};
101+
102+
/**
103+
* Checks if the Hermes artifacts are already downloaded and up to date with the specified version.
104+
* Returns true if the artifacts are up to date, false otherwise.
105+
*/
106+
function checkExistingVersion(
107+
versionFilePath /*: string */,
108+
version /*: string */,
109+
buildType /*: 'debug' | 'release' */,
110+
artifactsPath /*: string */,
111+
) {
112+
const resolvedVersion = `${version}-${buildType}`;
113+
const hermesXCFramework = path.join(
114+
artifactsPath,
115+
'destroot',
116+
'Libraries',
117+
'Frameworks',
118+
'universal',
119+
'hermes.xcframework',
120+
);
42121

43-
// Ensure build directory exists
44-
fs.mkdirSync(targetFolder, {recursive: true});
122+
if (fs.existsSync(versionFilePath) && fs.existsSync(hermesXCFramework)) {
123+
const versionFileContent = fs.readFileSync(versionFilePath, 'utf8');
124+
if (versionFileContent.trim() === resolvedVersion) {
125+
hermesLog(
126+
`Hermes artifacts already downloaded and up to date: ${artifactsPath}`,
127+
);
128+
return true;
129+
}
130+
}
131+
// If the version file does not exist or the version does not match, delete the artifacts folder
132+
fs.rmSync(artifactsPath, {recursive: true, force: true});
133+
hermesLog(
134+
`Hermes artifacts folder already exists, but version does not match. Deleting: ${artifactsPath}`,
135+
);
136+
// Lets create the version.txt file
137+
fs.mkdirSync(artifactsPath, {recursive: true});
138+
fs.writeFileSync(versionFilePath, resolvedVersion, 'utf8');
139+
hermesLog(
140+
`Hermes artifacts folder created: ${artifactsPath} with version: ${resolvedVersion}`,
141+
);
142+
return false;
143+
}
45144

46-
console.log(`Downloading file from ${url} to ${tarballPath}...`);
145+
function hermesEngineTarballEnvvarDefined() /*: boolean */ {
146+
return !!process.env.HERMES_ENGINE_TARBALL_PATH;
147+
}
47148

149+
function getTarballUrl(
150+
version /*: string */,
151+
buildType /*: 'debug' | 'release' */,
152+
) /*: string */ {
153+
const mavenRepoUrl = 'https://repo1.maven.org/maven2';
154+
const namespace = 'com/facebook/react';
155+
return `${mavenRepoUrl}/${namespace}/react-native-artifacts/${version}/react-native-artifacts-${version}-hermes-ios-${buildType}.tar.gz`;
156+
}
157+
158+
function getNightlyTarballUrl(
159+
version /*: string */,
160+
buildType /*: 'debug' | 'release' */,
161+
) /*: string */ {
162+
const params = `r=snapshots&g=com.facebook.react&a=react-native-artifacts&c=hermes-ios-${buildType}&e=tar.gz&v=${version}-SNAPSHOT`;
163+
return resolveUrlRedirects(
164+
`https://oss.sonatype.org/service/local/artifact/maven/redirect?${params}`,
165+
);
166+
}
167+
168+
function resolveUrlRedirects(url /*: string */) /*: string */ {
169+
// Synchronously resolve the final URL after redirects using curl
48170
try {
49-
// Download the file using curl via execSync
50-
execSync(`curl -L "${url}" -o "${tarballPath}"`, {stdio: 'inherit'});
171+
return execSync(`curl -Ls -o /dev/null -w '%{url_effective}' "${url}"`)
172+
.toString()
173+
.trim();
174+
} catch (e) {
175+
hermesLog(`Failed to resolve URL redirects\n${e}`, 'error');
176+
return url;
177+
}
178+
}
51179

52-
console.log('Download complete. Extracting...');
180+
function hermesArtifactExists(tarballUrl /*: string */) /*: boolean */ {
181+
try {
182+
const code = execSync(
183+
`curl -o /dev/null --silent -Iw '%{http_code}' -L "${tarballUrl}"`,
184+
)
185+
.toString()
186+
.trim();
187+
return code === '200';
188+
} catch (e) {
189+
return false;
190+
}
191+
}
53192

54-
// Extract the tar.gz using execSync
55-
execSync(`tar -xzf "${tarballPath}" -C "${targetFolder}"`, {
56-
stdio: 'inherit',
57-
});
193+
function hermesSourceType(
194+
version /*: string */,
195+
buildType /*: 'debug' | 'release' */,
196+
) /*: HermesEngineSourceType */ {
197+
if (hermesEngineTarballEnvvarDefined()) {
198+
hermesLog('Using local prebuild tarball');
199+
return HermesEngineSourceTypes.LOCAL_PREBUILT_TARBALL;
200+
}
201+
if (hermesArtifactExists(getTarballUrl(version, buildType))) {
202+
hermesLog(`Using download prebuild ${buildType} tarball`);
203+
return HermesEngineSourceTypes.DOWNLOAD_PREBUILD_TARBALL;
204+
}
205+
if (
206+
hermesArtifactExists(
207+
getNightlyTarballUrl(version, buildType).replace(/\\/g, ''),
208+
)
209+
) {
210+
hermesLog('Using download prebuild nightly tarball');
211+
return HermesEngineSourceTypes.DOWNLOAD_PREBUILT_NIGHTLY_TARBALL;
212+
}
213+
hermesLog(
214+
'Using download prebuild nightly tarball - this is a fallback and might not work.',
215+
);
216+
return HermesEngineSourceTypes.DOWNLOAD_PREBUILT_NIGHTLY_TARBALL;
217+
}
58218

59-
// Delete the tarball after extraction
60-
fs.unlinkSync(tarballPath);
219+
function resolveSourceFromSourceType(
220+
sourceType /*: HermesEngineSourceType */,
221+
version /*: string */,
222+
buildType /*: 'debug' | 'release' */,
223+
artifactsPath /*: string*/,
224+
) /*: string */ {
225+
switch (sourceType) {
226+
case HermesEngineSourceTypes.LOCAL_PREBUILT_TARBALL:
227+
return localPrebuiltTarball();
228+
case HermesEngineSourceTypes.DOWNLOAD_PREBUILD_TARBALL:
229+
return downloadPrebuildTarball(version, buildType, artifactsPath);
230+
case HermesEngineSourceTypes.DOWNLOAD_PREBUILT_NIGHTLY_TARBALL:
231+
return downloadPrebuiltNightlyTarball(version, buildType, artifactsPath);
232+
default:
233+
abort(
234+
`[Hermes] Unsupported or invalid source type provided: ${sourceType}`,
235+
);
236+
return '';
237+
}
238+
}
61239

62-
console.log('Download and extraction complete.');
63-
} catch (error) {
64-
if (fs.existsSync(tarballPath)) {
65-
fs.unlinkSync(tarballPath);
240+
function localPrebuiltTarball() /*: string */ {
241+
const tarballPath = process.env.HERMES_ENGINE_TARBALL_PATH;
242+
if (tarballPath && fs.existsSync(tarballPath)) {
243+
hermesLog(
244+
`Using pre-built binary from local path defined by HERMES_ENGINE_TARBALL_PATH envvar: ${tarballPath}`,
245+
);
246+
return `file://${tarballPath}`;
247+
}
248+
abort(
249+
`[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'`,
250+
);
251+
return '';
252+
}
253+
254+
function downloadPrebuildTarball(
255+
version /*: string */,
256+
buildType /*: 'debug' | 'release' */,
257+
artifactsPath /*: string*/,
258+
) /*: string */ {
259+
const url = getTarballUrl(version, buildType);
260+
hermesLog(`Using release tarball from URL: ${url}`);
261+
return downloadStableHermes(version, buildType, artifactsPath);
262+
}
263+
264+
function downloadPrebuiltNightlyTarball(
265+
version /*: string */,
266+
buildType /*: 'debug' | 'release' */,
267+
artifactsPath /*: string*/,
268+
) /*: string */ {
269+
const url = getNightlyTarballUrl(version, buildType);
270+
hermesLog(`Using nightly tarball from URL: ${url}`);
271+
return downloadHermesTarball(url, version, buildType, artifactsPath);
272+
}
273+
274+
function downloadStableHermes(
275+
version /*: string */,
276+
buildType /*: 'debug' | 'release' */,
277+
artifactsPath /*: string */,
278+
) /*: string */ {
279+
const tarballUrl = getTarballUrl(version, buildType);
280+
return downloadHermesTarball(tarballUrl, version, buildType, artifactsPath);
281+
}
282+
283+
function downloadHermesTarball(
284+
tarballUrl /*: string */,
285+
version /*: string */,
286+
buildType /*: 'debug' | 'release' */,
287+
artifactsPath /*: string */,
288+
) /*: string */ {
289+
const destPath = buildType
290+
? `${artifactsPath}/hermes-ios-${version}-${buildType}.tar.gz`
291+
: `${artifactsPath}/hermes-ios-${version}.tar.gz`;
292+
if (!fs.existsSync(destPath)) {
293+
const tmpFile = `${artifactsPath}/hermes-ios.download`;
294+
try {
295+
fs.mkdirSync(artifactsPath, {recursive: true});
296+
hermesLog(`Downloading Hermes tarball from ${tarballUrl}`);
297+
execSync(
298+
`curl "${tarballUrl}" -Lo "${tmpFile}" && mv "${tmpFile}" "${destPath}"`,
299+
);
300+
} catch (e) {
301+
abort(`Failed to download Hermes tarball from ${tarballUrl}`);
66302
}
67-
throw new Error(`Failed to download or extract: ${error.message}`);
68303
}
304+
return destPath;
69305
}
70306

71-
function getHermesArtifactsUrl(
72-
version /*:string*/,
73-
buildType /*:string*/,
74-
) /*:string*/ {
75-
// Define the URL for the Hermes artifacts
76-
// The URL format is:
77-
// https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/<version>/react-native-artifacts-<version>-hermes-ios-<buildType>.tar.gz
78-
// where <version> is the version of React Native and <buildType> is the build type (e.g., "debug" or "release")
79-
// The Maven repository URL and namespace
80-
// are hardcoded for simplicity, but they could be parameterized if needed
81-
const maven_repo_url = 'https://repo1.maven.org/maven2';
82-
const namespace = 'com/facebook/react';
83-
return `${maven_repo_url}/${namespace}/react-native-artifacts/${version}/react-native-artifacts-${version}-hermes-ios-${buildType}.tar.gz`;
307+
function abort(message /*: string */) {
308+
hermesLog(message, 'error');
309+
throw new Error(message);
310+
}
311+
312+
function hermesLog(
313+
message /*: string */,
314+
level /*: 'info' | 'warning' | 'error' */ = 'warning',
315+
) {
316+
// Simple log coloring for terminal output
317+
const prefix = '[Hermes] ';
318+
let colorFn = (x /*:string*/) => x;
319+
if (process.stdout.isTTY) {
320+
if (level === 'info') colorFn = x => `\x1b[32m${x}\x1b[0m`;
321+
else if (level === 'error') colorFn = x => `\x1b[31m${x}\x1b[0m`;
322+
else colorFn = x => `\x1b[33m${x}\x1b[0m`;
323+
}
324+
325+
console.log(colorFn(prefix + message));
84326
}
85327

86328
module.exports = {

0 commit comments

Comments
 (0)