1111const { computeNightlyTarballURL, createLogger} = require ( './utils' ) ;
1212const { execSync} = require ( 'child_process' ) ;
1313const fs = require ( 'fs' ) ;
14+ const os = require ( 'os' ) ;
1415const path = require ( 'path' ) ;
1516const stream = require ( 'stream' ) ;
1617const { promisify} = require ( 'util' ) ;
@@ -22,6 +23,124 @@ const hermesLog = createLogger('Hermes');
2223import type {BuildFlavor, Destination, Platform} from './types';
2324*/
2425
26+ // [macOS
27+ /**
28+ * For react-native-macos stable branches, maps the macOS package version
29+ * to the upstream react-native version using peerDependencies.
30+ * Returns null for version 1000.0.0 (main branch dev version).
31+ *
32+ * This is the JavaScript equivalent of the Ruby `findMatchingHermesVersion`
33+ * in sdks/hermes-engine/hermes-utils.rb.
34+ */
35+ function findMatchingHermesVersion (
36+ packageJsonPath /*: string */ ,
37+ ) /*: ?string */ {
38+ const pkg = JSON . parse ( fs . readFileSync ( packageJsonPath , 'utf8' ) ) ;
39+
40+ if ( pkg . version === '1000.0.0' ) {
41+ hermesLog (
42+ 'Main branch detected (1000.0.0), no matching upstream Hermes version' ,
43+ ) ;
44+ return null ;
45+ }
46+
47+ if ( pkg . peerDependencies && pkg . peerDependencies [ 'react-native' ] ) {
48+ const upstreamVersion = pkg . peerDependencies [ 'react-native' ] ;
49+ hermesLog (
50+ `Mapped macOS version ${ pkg . version } to upstream RN version: ${ upstreamVersion } ` ,
51+ ) ;
52+ return upstreamVersion ;
53+ }
54+
55+ hermesLog (
56+ 'No matching Hermes version found in peerDependencies. Defaulting to package version.' ,
57+ ) ;
58+ return null ;
59+ }
60+
61+ /**
62+ * Finds the Hermes commit at the merge base with facebook/react-native.
63+ * Used on the main branch (1000.0.0) where no prebuilt artifacts exist.
64+ *
65+ * Since react-native-macos lags slightly behind facebook/react-native, we can't always use
66+ * the latest Hermes commit because Hermes and JSI don't always guarantee backwards compatibility.
67+ * Instead, we take the commit hash of Hermes at the time of the merge base with facebook/react-native.
68+ *
69+ * This is the JavaScript equivalent of the Ruby `hermes_commit_at_merge_base`
70+ * in sdks/hermes-engine/hermes-utils.rb.
71+ */
72+ function hermesCommitAtMergeBase ( ) /*: {| commit: string, timestamp: string |} */ {
73+ const HERMES_GITHUB_URL = 'https://github.com/facebook/hermes.git' ;
74+
75+ // Fetch upstream react-native
76+ hermesLog ( 'Fetching facebook/react-native to find merge base...' ) ;
77+ try {
78+ execSync ( 'git fetch -q https://github.com/facebook/react-native.git' , {
79+ stdio : 'pipe' ,
80+ } ) ;
81+ } catch ( e ) {
82+ abort (
83+ '[Hermes] Failed to fetch facebook/react-native into the local repository.' ,
84+ ) ;
85+ }
86+
87+ // Find merge base between our HEAD and upstream's HEAD
88+ const mergeBase = execSync ( 'git merge-base FETCH_HEAD HEAD' , {
89+ encoding : 'utf8' ,
90+ } ) . trim ( ) ;
91+ if ( ! mergeBase ) {
92+ abort (
93+ "[Hermes] Unable to find the merge base between our HEAD and upstream's HEAD." ,
94+ ) ;
95+ }
96+
97+ // Get timestamp of merge base
98+ const timestamp = execSync ( `git show -s --format=%ci ${ mergeBase } ` , {
99+ encoding : 'utf8' ,
100+ } ) . trim ( ) ;
101+ if ( ! timestamp ) {
102+ abort (
103+ `[Hermes] Unable to extract the timestamp for the merge base (${ mergeBase } ).` ,
104+ ) ;
105+ }
106+
107+ // Clone Hermes bare (minimal) into a temp directory and find the commit
108+ hermesLog (
109+ `Merge base timestamp: ${ timestamp } . Cloning Hermes to find matching commit...` ,
110+ ) ;
111+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'hermes-' ) ) ;
112+ const hermesGitDir = path . join ( tmpDir , 'hermes.git' ) ;
113+
114+ try {
115+ // Explicitly use Hermes 'main' branch since the default branch changed to 'static_h' (Hermes V1)
116+ execSync (
117+ `git clone -q --bare --filter=blob:none --single-branch --branch main ${ HERMES_GITHUB_URL } "${ hermesGitDir } "` ,
118+ { stdio : 'pipe' , timeout : 120000 } ,
119+ ) ;
120+
121+ // Find the Hermes commit at the time of the merge base on branch 'main'
122+ const commit = execSync (
123+ `git --git-dir="${ hermesGitDir } " rev-list -1 --before="${ timestamp } " refs/heads/main` ,
124+ { encoding : 'utf8' } ,
125+ ) . trim ( ) ;
126+
127+ if ( ! commit ) {
128+ abort (
129+ `[Hermes] Unable to find the Hermes commit hash at time ${ timestamp } on branch 'main'.` ,
130+ ) ;
131+ }
132+
133+ hermesLog (
134+ `Using Hermes commit from the merge base with facebook/react-native: ${ commit } (timestamp: ${ timestamp } )` ,
135+ ) ;
136+ return { commit, timestamp} ;
137+ } finally {
138+ // Clean up temp directory
139+ fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
140+ }
141+ }
142+ // macOS]
143+
25144/**
26145 * Downloads hermes artifacts from the specified version and build type. If you want to specify a specific
27146 * version of hermes, use the HERMES_VERSION environment variable. The path to the artifacts will be inside
@@ -56,6 +175,22 @@ async function prepareHermesArtifactsAsync(
56175 // Resolve the version from the environment variable or use the default version
57176 let resolvedVersion = process . env . HERMES_VERSION ?? version ;
58177
178+ // [macOS] Map macOS version to upstream RN version for artifact lookup.
179+ // If no mapped version is found (main branch / 1000.0.0), allowBuildFromSource
180+ // enables the fallback to hermesCommitAtMergeBase() when no prebuilt artifacts exist.
181+ let allowBuildFromSource = false ;
182+ if ( ! process . env . HERMES_VERSION ) {
183+ const packageJsonPath = path . resolve ( __dirname , '..' , '..' , 'package.json' ) ;
184+ const mappedVersion = findMatchingHermesVersion ( packageJsonPath ) ;
185+ if ( mappedVersion != null ) {
186+ hermesLog ( `Using mapped upstream version for Hermes lookup: ${ mappedVersion } ` ) ;
187+ resolvedVersion = mappedVersion ;
188+ } else {
189+ allowBuildFromSource = true ;
190+ }
191+ }
192+ // macOS]
193+
59194 if ( resolvedVersion === 'nightly' ) {
60195 hermesLog ( 'Using latest nightly tarball' ) ;
61196 const hermesVersion = await getNightlyVersionFromNPM ( ) ;
@@ -74,7 +209,7 @@ async function prepareHermesArtifactsAsync(
74209 return artifactsPath ;
75210 }
76211
77- const sourceType = await hermesSourceType ( resolvedVersion , buildType ) ;
212+ const sourceType = await hermesSourceType ( resolvedVersion , buildType , allowBuildFromSource ) ;
78213 localPath = await resolveSourceFromSourceType (
79214 sourceType ,
80215 resolvedVersion ,
@@ -124,12 +259,14 @@ type HermesEngineSourceType =
124259 | 'local_prebuilt_tarball'
125260 | 'download_prebuild_tarball'
126261 | 'download_prebuilt_nightly_tarball'
262+ | 'build_from_hermes_commit'
127263*/
128264
129265const HermesEngineSourceTypes = {
130266 LOCAL_PREBUILT_TARBALL : 'local_prebuilt_tarball' ,
131267 DOWNLOAD_PREBUILD_TARBALL : 'download_prebuild_tarball' ,
132268 DOWNLOAD_PREBUILT_NIGHTLY_TARBALL : 'download_prebuilt_nightly_tarball' ,
269+ BUILD_FROM_HERMES_COMMIT : 'build_from_hermes_commit' , // [macOS]
133270 } /*:: as const */ ;
134271
135272/**
@@ -221,10 +358,16 @@ async function hermesArtifactExists(
221358
222359/**
223360 * Determines the source type for Hermes based on availability
361+ *
362+ * @param version - The resolved version string
363+ * @param buildType - Debug or Release
364+ * @param allowBuildFromSource - If true (macOS main branch), fall back to BUILD_FROM_HERMES_COMMIT
365+ * when no prebuilt artifacts exist. If false, fall back to nightly download (original behavior).
224366 */
225367async function hermesSourceType (
226368 version /*: string */ ,
227369 buildType /*: BuildFlavor */ ,
370+ allowBuildFromSource /*: boolean */ = false ,
228371) /*: Promise<HermesEngineSourceType> */ {
229372 if ( hermesEngineTarballEnvvarDefined ( ) ) {
230373 hermesLog ( 'Using local prebuild tarball' ) ;
@@ -244,6 +387,16 @@ async function hermesSourceType(
244387 return HermesEngineSourceTypes . DOWNLOAD_PREBUILT_NIGHTLY_TARBALL ;
245388 }
246389
390+ // [macOS] When on the macOS main branch (no mapped version, no explicit HERMES_VERSION),
391+ // fall back to resolving the Hermes commit at the merge base with facebook/react-native.
392+ if ( allowBuildFromSource ) {
393+ hermesLog (
394+ 'No prebuilt Hermes artifact found. Will attempt to resolve from merge base with facebook/react-native.' ,
395+ ) ;
396+ return HermesEngineSourceTypes . BUILD_FROM_HERMES_COMMIT ;
397+ }
398+ // macOS]
399+
247400 hermesLog (
248401 'Using download prebuild nightly tarball - this is a fallback and might not work.' ,
249402 ) ;
@@ -263,6 +416,8 @@ async function resolveSourceFromSourceType(
263416 return downloadPrebuildTarball ( version , buildType , artifactsPath ) ;
264417 case HermesEngineSourceTypes . DOWNLOAD_PREBUILT_NIGHTLY_TARBALL :
265418 return downloadPrebuiltNightlyTarball ( version , buildType , artifactsPath ) ;
419+ case HermesEngineSourceTypes . BUILD_FROM_HERMES_COMMIT : // [macOS]
420+ return buildFromHermesCommit ( version , buildType , artifactsPath ) ;
266421 default :
267422 abort (
268423 `[Hermes] Unsupported or invalid source type provided: ${ sourceType } ` ,
@@ -369,11 +524,37 @@ async function downloadHermesTarball(
369524 return destPath ;
370525}
371526
527+ // [macOS
528+ /**
529+ * Handles the case where no prebuilt Hermes artifacts are available.
530+ * Determines the Hermes commit at the merge base with facebook/react-native
531+ * and provides actionable guidance for building Hermes.
532+ */
533+ async function buildFromHermesCommit (
534+ version /*: string */ ,
535+ buildType /*: BuildFlavor */ ,
536+ artifactsPath /*: string */ ,
537+ ) /*: Promise<string> */ {
538+ const { commit, timestamp} = hermesCommitAtMergeBase ( ) ;
539+ abort (
540+ `[Hermes] No prebuilt Hermes artifacts available for version "${ version } ".\n` +
541+ `Hermes commit at merge base with facebook/react-native: ${ commit } (timestamp: ${ timestamp } )\n` +
542+ `To resolve, either:\n` +
543+ ` 1. Set HERMES_ENGINE_TARBALL_PATH to a local Hermes tarball path\n` +
544+ ` 2. Set HERMES_VERSION to an upstream RN version with published artifacts (e.g., HERMES_VERSION=nightly)\n` +
545+ ` 3. Build Hermes from commit ${ commit } and provide the tarball path via HERMES_ENGINE_TARBALL_PATH` ,
546+ ) ;
547+ return '' ; // unreachable
548+ }
549+ // macOS]
550+
372551function abort ( message /*: string */ ) {
373552 hermesLog ( message , 'error' ) ;
374553 throw new Error ( message ) ;
375554}
376555
377556module . exports = {
378557 prepareHermesArtifactsAsync,
558+ findMatchingHermesVersion, // [macOS]
559+ hermesCommitAtMergeBase, // [macOS]
379560} ;
0 commit comments