From efe10282c9fec21fb73ff76ff3bdb23a4e59e805 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Apr 2026 11:38:59 -0700 Subject: [PATCH 01/10] fix(ci): align Resolve Hermes with HermesV1 / hermesvm.xcframework Package.swift and ios-prebuild/hermes.js already reference hermesvm.xcframework (the HermesV1 product introduced in RN 0.83), but the Resolve Hermes job still recomposed hermes.xcframework and resolved Hermes commits on the legacy `main` branch. With the upstream build scripts now unconditionally producing hermesvm, this left the SPM build path internally inconsistent. - microsoft-hermes.js: resolve commits on Hermes `static_h` branch; add `hermesV1Tag()` helper that reads sdks/.hermesv1version - resolve-hermes.mts: recompose hermesvm.xcframework / hermesvm.framework - microsoft-resolve-hermes.yml: bump cache key (hermes-v1- -> hermesv1-engine-) so stale V0-shaped caches aren't reused as V1 artifacts Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/scripts/resolve-hermes.mts | 15 ++++---- .../workflows/microsoft-resolve-hermes.yml | 4 +- .../scripts/ios-prebuild/microsoft-hermes.js | 37 ++++++++++++++++--- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/.github/scripts/resolve-hermes.mts b/.github/scripts/resolve-hermes.mts index 8d6289573593..bfd3446d1b30 100644 --- a/.github/scripts/resolve-hermes.mts +++ b/.github/scripts/resolve-hermes.mts @@ -125,8 +125,9 @@ async function downloadUpstreamHermesTarball( * Extracts an upstream Hermes tarball and recomposes the xcframework to include * the macOS slice, if needed. * - * Upstream tarballs ship a universal xcframework (iOS, simulator, catalyst, - * tvOS, visionOS) plus a standalone macosx/hermes.framework. This function + * As of RN 0.83, the Hermes binary product is `hermesvm` (HermesV1). Upstream + * tarballs ship a universal `hermesvm.xcframework` (iOS, simulator, catalyst, + * tvOS, visionOS) plus a standalone `macosx/hermesvm.framework`. This function * merges the standalone macOS framework into the universal xcframework using * `xcodebuild -create-xcframework`. * @@ -147,7 +148,7 @@ async function recomposeHermesXcframework( await $`tar -xzf ${tarballPath} -C ${destroot} --strip-components=2`; const frameworksDir = path.join(destroot, 'Library', 'Frameworks'); - const xcfwPath = path.join(frameworksDir, 'universal', 'hermes.xcframework'); + const xcfwPath = path.join(frameworksDir, 'universal', 'hermesvm.xcframework'); echo('Upstream tarball contents:'); await $`ls -la ${frameworksDir}`; @@ -167,16 +168,16 @@ async function recomposeHermesXcframework( } // Check for standalone macOS framework - const standaloneMacFw = path.join(frameworksDir, 'macosx', 'hermes.framework'); + const standaloneMacFw = path.join(frameworksDir, 'macosx', 'hermesvm.framework'); if (!fs.existsSync(standaloneMacFw)) { - echo('ERROR: Upstream tarball missing macosx/hermes.framework'); + echo('ERROR: Upstream tarball missing macosx/hermesvm.framework'); return false; } // Collect existing frameworks from inside the universal xcframework const frameworkArgs: string[] = []; for (const entry of xcfwContents) { - const fwPath = path.join(xcfwPath, entry, 'hermes.framework'); + const fwPath = path.join(xcfwPath, entry, 'hermesvm.framework'); if (fs.existsSync(fwPath) && fs.statSync(fwPath).isDirectory()) { echo(`Found slice: ${fwPath}`); frameworkArgs.push('-framework', fwPath); @@ -188,7 +189,7 @@ async function recomposeHermesXcframework( frameworkArgs.push('-framework', standaloneMacFw); // Build new xcframework at a temp path (frameworks reference paths inside the old xcfw) - const xcfwNew = path.join(frameworksDir, 'universal', 'hermes-new.xcframework'); + const xcfwNew = path.join(frameworksDir, 'universal', 'hermesvm-new.xcframework'); const sliceCount = frameworkArgs.filter(f => f !== '-framework').length; echo(`Creating new universal xcframework with ${sliceCount} slices...`); await $`xcodebuild -create-xcframework ${frameworkArgs} -output ${xcfwNew} -allow-internal-distribution`; diff --git a/.github/workflows/microsoft-resolve-hermes.yml b/.github/workflows/microsoft-resolve-hermes.yml index 78f590715ddc..2c6cb2a05fcb 100644 --- a/.github/workflows/microsoft-resolve-hermes.yml +++ b/.github/workflows/microsoft-resolve-hermes.yml @@ -79,7 +79,7 @@ jobs: id: cache uses: actions/cache/restore@v4 with: - key: hermes-v1-${{ steps.resolve.outputs.hermes-commit }}-Debug + key: hermesv1-engine-${{ steps.resolve.outputs.hermes-commit }}-Debug path: hermes-destroot - name: Upload cached Hermes artifacts @@ -249,7 +249,7 @@ jobs: - name: Save Hermes cache uses: actions/cache/save@v4 with: - key: hermes-v1-${{ needs.resolve-hermes.outputs.hermes-commit }}-Debug + key: hermesv1-engine-${{ needs.resolve-hermes.outputs.hermes-commit }}-Debug path: hermes/destroot - name: Upload Hermes artifacts diff --git a/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js b/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js index 9bdeec72d8fc..f882d749cc05 100644 --- a/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js +++ b/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js @@ -56,6 +56,29 @@ function findMatchingHermesVersion( return null; } +/** + * Reads the pinned HermesV1 tag from sdks/.hermesv1version. + * + * Returns a value like 'hermes-v250829098.0.2', which can be used as a git ref + * when checking out facebook/hermes for from-source builds. Returns null if the + * file is missing or empty. + */ +function hermesV1Tag() /*: ?string */ { + const tagFile = path.resolve( + __dirname, + '..', + '..', + 'sdks', + '.hermesv1version', + ); + try { + const tag = fs.readFileSync(tagFile, 'utf8').trim(); + return tag.length > 0 ? tag : null; + } catch (_) { + return null; + } +} + /** * Finds the Hermes commit at the merge base with facebook/react-native. * Used on the main branch (1000.0.0) where no prebuilt artifacts exist. @@ -64,11 +87,15 @@ function findMatchingHermesVersion( * the latest Hermes commit because Hermes and JSI don't always guarantee backwards compatibility. * Instead, we take the commit hash of Hermes at the time of the merge base with facebook/react-native. * + * Hermes ships HermesV1 on the `static_h` branch as of RN 0.83, and the macOS fork's + * SPM build path consumes hermesvm — so we resolve against `static_h`, not `main`. + * * This is the JavaScript equivalent of the Ruby `hermes_commit_at_merge_base` * in sdks/hermes-engine/hermes-utils.rb. */ function hermesCommitAtMergeBase() /*: {| commit: string, timestamp: string |} */ { const HERMES_GITHUB_URL = 'https://github.com/facebook/hermes.git'; + const HERMES_BRANCH = 'static_h'; // Fetch upstream react-native macosLog('Fetching facebook/react-native to find merge base...'); @@ -110,21 +137,20 @@ function hermesCommitAtMergeBase() /*: {| commit: string, timestamp: string |} * const hermesGitDir = path.join(tmpDir, 'hermes.git'); try { - // Explicitly use Hermes 'main' branch since the default branch changed to 'static_h' (Hermes V1) execSync( - `git clone -q --bare --filter=blob:none --single-branch --branch main ${HERMES_GITHUB_URL} "${hermesGitDir}"`, + `git clone -q --bare --filter=blob:none --single-branch --branch ${HERMES_BRANCH} ${HERMES_GITHUB_URL} "${hermesGitDir}"`, {stdio: 'pipe', timeout: 120000}, ); - // Find the Hermes commit at the time of the merge base on branch 'main' + // Find the Hermes commit at the time of the merge base on the HermesV1 branch const commit = execSync( - `git --git-dir="${hermesGitDir}" rev-list -1 --before="${timestamp}" refs/heads/main`, + `git --git-dir="${hermesGitDir}" rev-list -1 --before="${timestamp}" refs/heads/${HERMES_BRANCH}`, {encoding: 'utf8'}, ).trim(); if (!commit) { abort( - `[Hermes] Unable to find the Hermes commit hash at time ${timestamp} on branch 'main'.`, + `[Hermes] Unable to find the Hermes commit hash at time ${timestamp} on branch '${HERMES_BRANCH}'.`, ); } @@ -196,6 +222,7 @@ function abort(message /*: string */) { module.exports = { findMatchingHermesVersion, hermesCommitAtMergeBase, + hermesV1Tag, findVersionAtMergeBase, getLatestStableVersionFromNPM, }; From 82bacbe2db89469a4d9258ec1c735f65d49e4974 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Apr 2026 14:19:06 -0700 Subject: [PATCH 02/10] fix(ci): pass CMAKE_BUILD_TYPE when building host hermesc for V1 HermesV1's static_h CMakeLists requires CMAKE_BUILD_TYPE to be set; the upstream `build_host_hermesc` helper in build-apple-framework.sh doesn't pass it, so the from-source fallback in Resolve Hermes failed at the hermesc step with "Please set CMAKE_BUILD_TYPE". Invoke cmake directly from the workflow with -DCMAKE_BUILD_TYPE=Release (the host compiler is always built Release for perf, matching configure_apple_framework's behavior in Debug builds of the apple frameworks). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/microsoft-resolve-hermes.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/microsoft-resolve-hermes.yml b/.github/workflows/microsoft-resolve-hermes.yml index 2c6cb2a05fcb..780f52cadae9 100644 --- a/.github/workflows/microsoft-resolve-hermes.yml +++ b/.github/workflows/microsoft-resolve-hermes.yml @@ -118,6 +118,8 @@ jobs: ref: ${{ needs.resolve-hermes.outputs.hermes-commit }} path: hermes + # HermesV1's CMakeLists requires CMAKE_BUILD_TYPE explicitly. The upstream + # `build_host_hermesc` helper doesn't set it, so we invoke cmake directly here. - name: Build hermesc working-directory: hermes env: @@ -125,8 +127,8 @@ jobs: JSI_PATH: ${{ github.workspace }}/hermes/API/jsi MAC_DEPLOYMENT_TARGET: '14.0' run: | - source $GITHUB_WORKSPACE/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh - build_host_hermesc + cmake -S . -B build_host_hermesc -DJSI_DIR="$JSI_PATH" -DCMAKE_BUILD_TYPE=Release + cmake --build ./build_host_hermesc --target hermesc -j "$(sysctl -n hw.ncpu)" - name: Upload hermesc artifact uses: actions/upload-artifact@v4 From ae4bd9e961691d3a4381c8658c556ca8d0d520f6 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Apr 2026 14:32:20 -0700 Subject: [PATCH 03/10] fix(hermes): tolerate missing legacy inspector headers under HermesV1 HermesV1 (static_h) replaces the legacy API/hermes/inspector/ headers with API/hermes/cdp/, so the unconditional cp commands in build_apple_framework and prepare_dest_root_for_ci fail with 'No such file or directory' when building from V1 sources. Guard each copy with compgen so the copy is skipped cleanly when the source directory is empty. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../utils/build-apple-framework.sh | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh b/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh index dbb363bd6870..bc62858e6dd9 100755 --- a/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh +++ b/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh @@ -163,11 +163,17 @@ function build_apple_framework { mkdir -p destroot/include/hermes/cdp cp API/hermes/cdp/*.h destroot/include/hermes/cdp - mkdir -p destroot/include/hermes/inspector - cp API/hermes/inspector/*.h destroot/include/hermes/inspector + # [macOS] HermesV1 (static_h) drops the legacy inspector headers in favour of + # API/hermes/cdp/. Skip the copy when the source directory has no headers. + if compgen -G "API/hermes/inspector/*.h" > /dev/null; then + mkdir -p destroot/include/hermes/inspector + cp API/hermes/inspector/*.h destroot/include/hermes/inspector + fi - mkdir -p destroot/include/hermes/inspector/chrome - cp API/hermes/inspector/chrome/*.h destroot/include/hermes/inspector/chrome + if compgen -G "API/hermes/inspector/chrome/*.h" > /dev/null; then + mkdir -p destroot/include/hermes/inspector/chrome + cp API/hermes/inspector/chrome/*.h destroot/include/hermes/inspector/chrome + fi mkdir -p destroot/include/jsi cp "$JSI_PATH"/jsi/*.h destroot/include/jsi @@ -193,11 +199,17 @@ function prepare_dest_root_for_ci { mkdir -p destroot/include/hermes/cdp cp API/hermes/cdp/*.h destroot/include/hermes/cdp - mkdir -p destroot/include/hermes/inspector - cp API/hermes/inspector/*.h destroot/include/hermes/inspector + # [macOS] HermesV1 (static_h) drops the legacy inspector headers in favour of + # API/hermes/cdp/. Skip the copy when the source directory has no headers. + if compgen -G "API/hermes/inspector/*.h" > /dev/null; then + mkdir -p destroot/include/hermes/inspector + cp API/hermes/inspector/*.h destroot/include/hermes/inspector + fi - mkdir -p destroot/include/hermes/inspector/chrome - cp API/hermes/inspector/chrome/*.h destroot/include/hermes/inspector/chrome + if compgen -G "API/hermes/inspector/chrome/*.h" > /dev/null; then + mkdir -p destroot/include/hermes/inspector/chrome + cp API/hermes/inspector/chrome/*.h destroot/include/hermes/inspector/chrome + fi mkdir -p destroot/include/jsi cp "$JSI_PATH"/jsi/*.h destroot/include/jsi From 0e107d1184dbde91e50b760ed54807a678d862a1 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Apr 2026 14:58:56 -0700 Subject: [PATCH 04/10] fix(spm): define HERMES_V1_ENABLED so V1-aware guards take effect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Package.swift links against hermesvm.xcframework (HermesV1) but never defines HERMES_V1_ENABLED, so headers like ReactCommon/hermes/inspector-modern/chrome/Registration.h still try to #include — a legacy header V1 dropped. Set HERMES_V1_ENABLED=1 in the shared cxxSettings (alongside USE_HERMES); this matches what cocoapods/utils.rb does when RCT_HERMES_V1_ENABLED=1. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/react-native/Package.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index d42ab0f8fa63..d6273aa7ed51 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -946,6 +946,11 @@ extension Target { .define("DEBUG", .when(configuration: .debug)), .define("NDEBUG", .when(configuration: .release)), .define("USE_HERMES", to: "1"), + // [macOS] The SPM build links against hermesvm.xcframework (HermesV1). + // Several headers (e.g. ReactCommon/hermes/inspector-modern/chrome/Registration.h) + // gate legacy inspector code on `!defined(HERMES_V1_ENABLED)`, matching what the + // CocoaPods path sets via cocoapods/utils.rb when RCT_HERMES_V1_ENABLED=1. + .define("HERMES_V1_ENABLED", to: "1"), ] + defines + cxxCommonHeaderPaths return .target( From 19f6014d2f72361201c2ad322d4182160a3d65f7 Mon Sep 17 00:00:00 2001 From: Thiago Vinhas Date: Wed, 13 May 2026 14:34:06 -0500 Subject: [PATCH 05/10] fix(hermes): pick static_h vs main branch by RCT_HERMES_V1_ENABLED in hermes-utils.rb --- .../sdks/hermes-engine/hermes-utils.rb | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/react-native/sdks/hermes-engine/hermes-utils.rb b/packages/react-native/sdks/hermes-engine/hermes-utils.rb index 2118dddea813..8ede597ea56d 100644 --- a/packages/react-native/sdks/hermes-engine/hermes-utils.rb +++ b/packages/react-native/sdks/hermes-engine/hermes-utils.rb @@ -251,14 +251,22 @@ def hermes_commit_at_merge_base() commit = nil Dir.mktmpdir do |tmpdir| hermes_git_dir = File.join(tmpdir, "hermes.git") - # Explicitly use Hermes 'main' branch since the default branch changed to 'static_h' (Hermes V1) - `git clone -q --bare --filter=blob:none --single-branch --branch main #{HERMES_GITHUB_URL} "#{hermes_git_dir}"` - - # If all goes well, this will be the commit hash of Hermes at the time of the merge base on branch 'main' - commit = `git --git-dir="#{hermes_git_dir}" rev-list -1 --before="#{timestamp}" refs/heads/main`.strip + # Pick the Hermes branch that matches the engine variant we resolve at: + # V1 (Hermes 0.83+ split package) lives on `static_h` + # V0 (legacy) lives on `main` + # Without this gate, RCT_HERMES_V1_ENABLED=1 + from-source fallback + # (no Maven artifact, RCT_BUILD_HERMES_FROM_SOURCE=true) would clone + # V0 source while the rest of the podspec expects hermesvm.framework + # (V1) artifacts. Mirror of the same fix on the JS side in PR #2952. + hermes_branch = hermes_v1_enabled() ? "static_h" : "main" + `git clone -q --bare --filter=blob:none --single-branch --branch #{hermes_branch} #{HERMES_GITHUB_URL} "#{hermes_git_dir}"` + + # Resolve the Hermes commit at the time of the merge base on the + # chosen branch. + commit = `git --git-dir="#{hermes_git_dir}" rev-list -1 --before="#{timestamp}" refs/heads/#{hermes_branch}`.strip if commit.empty? abort <<-EOS - [Hermes] Unable to find the Hermes commit hash at time #{timestamp} on branch 'main'. + [Hermes] Unable to find the Hermes commit hash at time #{timestamp} on branch '#{hermes_branch}'. EOS end end From 1a3034187d20d0cfeed300a6423b8c9749919eb0 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Jul 2026 14:14:42 -0700 Subject: [PATCH 06/10] audit(hermes): scope 0.83 to hermesvm rename only, defer V1 engine to 0.84 Upstream 0.83-stable defaults to Hermes V0 (sdks/.hermesversion = hermes-v0.14.1); V1 is opt-in via RCT_HERMES_V1_ENABLED and only becomes the default in 0.84 (facebook/react-native#54989, commit bbe02de, not backported to 0.83). The `hermes` -> `hermesvm` product rename, however, already landed in 0.83 (facebook/react-native#53094), so the fork's resolve-hermes.mts genuinely needed to catch up to the new artifact name. Reduce the PR to just that V0-compatible rename catch-up and revert the V1-engine bits to match upstream 0.83: - resolve-hermes.mts: keep hermes.* -> hermesvm.* (needed on 0.83). - Package.swift: drop HERMES_V1_ENABLED define (inherited at 0.84 merge). - microsoft-hermes.js: revert static_h -> main; drop unused hermesV1Tag(). - hermes-utils.rb: revert static_h/main gate back to main (V0). - build-apple-framework.sh: revert inspector-header guard (V0 ships them). - microsoft-resolve-hermes.yml: revert cache-key rename + cmake hermesc change (build_host_hermesc works for V0). Net: 6 files -> 1; the five reverted files now match upstream/0.83-merge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/resolve-hermes.mts | 10 ++--- .../workflows/microsoft-resolve-hermes.yml | 10 ++--- packages/react-native/Package.swift | 5 --- .../scripts/ios-prebuild/microsoft-hermes.js | 37 +++---------------- .../sdks/hermes-engine/hermes-utils.rb | 20 +++------- .../utils/build-apple-framework.sh | 28 ++++---------- 6 files changed, 28 insertions(+), 82 deletions(-) diff --git a/.github/scripts/resolve-hermes.mts b/.github/scripts/resolve-hermes.mts index bfd3446d1b30..6a59d0d57397 100644 --- a/.github/scripts/resolve-hermes.mts +++ b/.github/scripts/resolve-hermes.mts @@ -125,11 +125,11 @@ async function downloadUpstreamHermesTarball( * Extracts an upstream Hermes tarball and recomposes the xcframework to include * the macOS slice, if needed. * - * As of RN 0.83, the Hermes binary product is `hermesvm` (HermesV1). Upstream - * tarballs ship a universal `hermesvm.xcframework` (iOS, simulator, catalyst, - * tvOS, visionOS) plus a standalone `macosx/hermesvm.framework`. This function - * merges the standalone macOS framework into the universal xcframework using - * `xcodebuild -create-xcframework`. + * As of RN 0.83, upstream renamed the Hermes binary product from `hermes` to + * `hermesvm`. Upstream tarballs ship a universal `hermesvm.xcframework` (iOS, + * simulator, catalyst, tvOS, visionOS) plus a standalone `macosx/hermesvm.framework`. + * This function merges the standalone macOS framework into the universal + * xcframework using `xcodebuild -create-xcframework`. * * NOTE: Once upstream Hermes includes macOS in the universal xcframework * natively, this function will detect the existing macOS slice and skip diff --git a/.github/workflows/microsoft-resolve-hermes.yml b/.github/workflows/microsoft-resolve-hermes.yml index 780f52cadae9..78f590715ddc 100644 --- a/.github/workflows/microsoft-resolve-hermes.yml +++ b/.github/workflows/microsoft-resolve-hermes.yml @@ -79,7 +79,7 @@ jobs: id: cache uses: actions/cache/restore@v4 with: - key: hermesv1-engine-${{ steps.resolve.outputs.hermes-commit }}-Debug + key: hermes-v1-${{ steps.resolve.outputs.hermes-commit }}-Debug path: hermes-destroot - name: Upload cached Hermes artifacts @@ -118,8 +118,6 @@ jobs: ref: ${{ needs.resolve-hermes.outputs.hermes-commit }} path: hermes - # HermesV1's CMakeLists requires CMAKE_BUILD_TYPE explicitly. The upstream - # `build_host_hermesc` helper doesn't set it, so we invoke cmake directly here. - name: Build hermesc working-directory: hermes env: @@ -127,8 +125,8 @@ jobs: JSI_PATH: ${{ github.workspace }}/hermes/API/jsi MAC_DEPLOYMENT_TARGET: '14.0' run: | - cmake -S . -B build_host_hermesc -DJSI_DIR="$JSI_PATH" -DCMAKE_BUILD_TYPE=Release - cmake --build ./build_host_hermesc --target hermesc -j "$(sysctl -n hw.ncpu)" + source $GITHUB_WORKSPACE/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh + build_host_hermesc - name: Upload hermesc artifact uses: actions/upload-artifact@v4 @@ -251,7 +249,7 @@ jobs: - name: Save Hermes cache uses: actions/cache/save@v4 with: - key: hermesv1-engine-${{ needs.resolve-hermes.outputs.hermes-commit }}-Debug + key: hermes-v1-${{ needs.resolve-hermes.outputs.hermes-commit }}-Debug path: hermes/destroot - name: Upload Hermes artifacts diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index d6273aa7ed51..d42ab0f8fa63 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -946,11 +946,6 @@ extension Target { .define("DEBUG", .when(configuration: .debug)), .define("NDEBUG", .when(configuration: .release)), .define("USE_HERMES", to: "1"), - // [macOS] The SPM build links against hermesvm.xcframework (HermesV1). - // Several headers (e.g. ReactCommon/hermes/inspector-modern/chrome/Registration.h) - // gate legacy inspector code on `!defined(HERMES_V1_ENABLED)`, matching what the - // CocoaPods path sets via cocoapods/utils.rb when RCT_HERMES_V1_ENABLED=1. - .define("HERMES_V1_ENABLED", to: "1"), ] + defines + cxxCommonHeaderPaths return .target( diff --git a/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js b/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js index f882d749cc05..9bdeec72d8fc 100644 --- a/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js +++ b/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js @@ -56,29 +56,6 @@ function findMatchingHermesVersion( return null; } -/** - * Reads the pinned HermesV1 tag from sdks/.hermesv1version. - * - * Returns a value like 'hermes-v250829098.0.2', which can be used as a git ref - * when checking out facebook/hermes for from-source builds. Returns null if the - * file is missing or empty. - */ -function hermesV1Tag() /*: ?string */ { - const tagFile = path.resolve( - __dirname, - '..', - '..', - 'sdks', - '.hermesv1version', - ); - try { - const tag = fs.readFileSync(tagFile, 'utf8').trim(); - return tag.length > 0 ? tag : null; - } catch (_) { - return null; - } -} - /** * Finds the Hermes commit at the merge base with facebook/react-native. * Used on the main branch (1000.0.0) where no prebuilt artifacts exist. @@ -87,15 +64,11 @@ function hermesV1Tag() /*: ?string */ { * the latest Hermes commit because Hermes and JSI don't always guarantee backwards compatibility. * Instead, we take the commit hash of Hermes at the time of the merge base with facebook/react-native. * - * Hermes ships HermesV1 on the `static_h` branch as of RN 0.83, and the macOS fork's - * SPM build path consumes hermesvm — so we resolve against `static_h`, not `main`. - * * This is the JavaScript equivalent of the Ruby `hermes_commit_at_merge_base` * in sdks/hermes-engine/hermes-utils.rb. */ function hermesCommitAtMergeBase() /*: {| commit: string, timestamp: string |} */ { const HERMES_GITHUB_URL = 'https://github.com/facebook/hermes.git'; - const HERMES_BRANCH = 'static_h'; // Fetch upstream react-native macosLog('Fetching facebook/react-native to find merge base...'); @@ -137,20 +110,21 @@ function hermesCommitAtMergeBase() /*: {| commit: string, timestamp: string |} * const hermesGitDir = path.join(tmpDir, 'hermes.git'); try { + // Explicitly use Hermes 'main' branch since the default branch changed to 'static_h' (Hermes V1) execSync( - `git clone -q --bare --filter=blob:none --single-branch --branch ${HERMES_BRANCH} ${HERMES_GITHUB_URL} "${hermesGitDir}"`, + `git clone -q --bare --filter=blob:none --single-branch --branch main ${HERMES_GITHUB_URL} "${hermesGitDir}"`, {stdio: 'pipe', timeout: 120000}, ); - // Find the Hermes commit at the time of the merge base on the HermesV1 branch + // Find the Hermes commit at the time of the merge base on branch 'main' const commit = execSync( - `git --git-dir="${hermesGitDir}" rev-list -1 --before="${timestamp}" refs/heads/${HERMES_BRANCH}`, + `git --git-dir="${hermesGitDir}" rev-list -1 --before="${timestamp}" refs/heads/main`, {encoding: 'utf8'}, ).trim(); if (!commit) { abort( - `[Hermes] Unable to find the Hermes commit hash at time ${timestamp} on branch '${HERMES_BRANCH}'.`, + `[Hermes] Unable to find the Hermes commit hash at time ${timestamp} on branch 'main'.`, ); } @@ -222,7 +196,6 @@ function abort(message /*: string */) { module.exports = { findMatchingHermesVersion, hermesCommitAtMergeBase, - hermesV1Tag, findVersionAtMergeBase, getLatestStableVersionFromNPM, }; diff --git a/packages/react-native/sdks/hermes-engine/hermes-utils.rb b/packages/react-native/sdks/hermes-engine/hermes-utils.rb index 8ede597ea56d..2118dddea813 100644 --- a/packages/react-native/sdks/hermes-engine/hermes-utils.rb +++ b/packages/react-native/sdks/hermes-engine/hermes-utils.rb @@ -251,22 +251,14 @@ def hermes_commit_at_merge_base() commit = nil Dir.mktmpdir do |tmpdir| hermes_git_dir = File.join(tmpdir, "hermes.git") - # Pick the Hermes branch that matches the engine variant we resolve at: - # V1 (Hermes 0.83+ split package) lives on `static_h` - # V0 (legacy) lives on `main` - # Without this gate, RCT_HERMES_V1_ENABLED=1 + from-source fallback - # (no Maven artifact, RCT_BUILD_HERMES_FROM_SOURCE=true) would clone - # V0 source while the rest of the podspec expects hermesvm.framework - # (V1) artifacts. Mirror of the same fix on the JS side in PR #2952. - hermes_branch = hermes_v1_enabled() ? "static_h" : "main" - `git clone -q --bare --filter=blob:none --single-branch --branch #{hermes_branch} #{HERMES_GITHUB_URL} "#{hermes_git_dir}"` - - # Resolve the Hermes commit at the time of the merge base on the - # chosen branch. - commit = `git --git-dir="#{hermes_git_dir}" rev-list -1 --before="#{timestamp}" refs/heads/#{hermes_branch}`.strip + # Explicitly use Hermes 'main' branch since the default branch changed to 'static_h' (Hermes V1) + `git clone -q --bare --filter=blob:none --single-branch --branch main #{HERMES_GITHUB_URL} "#{hermes_git_dir}"` + + # If all goes well, this will be the commit hash of Hermes at the time of the merge base on branch 'main' + commit = `git --git-dir="#{hermes_git_dir}" rev-list -1 --before="#{timestamp}" refs/heads/main`.strip if commit.empty? abort <<-EOS - [Hermes] Unable to find the Hermes commit hash at time #{timestamp} on branch '#{hermes_branch}'. + [Hermes] Unable to find the Hermes commit hash at time #{timestamp} on branch 'main'. EOS end end diff --git a/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh b/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh index bc62858e6dd9..dbb363bd6870 100755 --- a/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh +++ b/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh @@ -163,17 +163,11 @@ function build_apple_framework { mkdir -p destroot/include/hermes/cdp cp API/hermes/cdp/*.h destroot/include/hermes/cdp - # [macOS] HermesV1 (static_h) drops the legacy inspector headers in favour of - # API/hermes/cdp/. Skip the copy when the source directory has no headers. - if compgen -G "API/hermes/inspector/*.h" > /dev/null; then - mkdir -p destroot/include/hermes/inspector - cp API/hermes/inspector/*.h destroot/include/hermes/inspector - fi + mkdir -p destroot/include/hermes/inspector + cp API/hermes/inspector/*.h destroot/include/hermes/inspector - if compgen -G "API/hermes/inspector/chrome/*.h" > /dev/null; then - mkdir -p destroot/include/hermes/inspector/chrome - cp API/hermes/inspector/chrome/*.h destroot/include/hermes/inspector/chrome - fi + mkdir -p destroot/include/hermes/inspector/chrome + cp API/hermes/inspector/chrome/*.h destroot/include/hermes/inspector/chrome mkdir -p destroot/include/jsi cp "$JSI_PATH"/jsi/*.h destroot/include/jsi @@ -199,17 +193,11 @@ function prepare_dest_root_for_ci { mkdir -p destroot/include/hermes/cdp cp API/hermes/cdp/*.h destroot/include/hermes/cdp - # [macOS] HermesV1 (static_h) drops the legacy inspector headers in favour of - # API/hermes/cdp/. Skip the copy when the source directory has no headers. - if compgen -G "API/hermes/inspector/*.h" > /dev/null; then - mkdir -p destroot/include/hermes/inspector - cp API/hermes/inspector/*.h destroot/include/hermes/inspector - fi + mkdir -p destroot/include/hermes/inspector + cp API/hermes/inspector/*.h destroot/include/hermes/inspector - if compgen -G "API/hermes/inspector/chrome/*.h" > /dev/null; then - mkdir -p destroot/include/hermes/inspector/chrome - cp API/hermes/inspector/chrome/*.h destroot/include/hermes/inspector/chrome - fi + mkdir -p destroot/include/hermes/inspector/chrome + cp API/hermes/inspector/chrome/*.h destroot/include/hermes/inspector/chrome mkdir -p destroot/include/jsi cp "$JSI_PATH"/jsi/*.h destroot/include/jsi From 5e8d29c57d777b5f1dcdc26304f32d8f4891e5cc Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Jul 2026 14:46:58 -0700 Subject: [PATCH 07/10] fix(hermes): download upstream prebuilt Hermes from the correct Maven coordinate resolve-hermes.mts queried com/facebook/react/react-native-artifacts/ for the Hermes tarball, but upstream publishes prebuilt Hermes under com/facebook/hermes/hermes-ios/ (keyed by the Hermes version, not the react-native version). That coordinate has no Hermes classifier, so the download always 404'd and every 0.83 Resolve Hermes run fell back to an expensive from-source build. Point the download at upstream's real coordinate and read the Hermes version from sdks/hermes-engine/version.properties, mirroring the CocoaPods release_tarball_url: HERMES_VERSION_NAME by default (RN 0.83 ships V0 0.14.0), or HERMES_V1_VERSION_NAME when RCT_HERMES_V1_ENABLED=1. The existing recompose step then merges the standalone macOS slice into the universal xcframework. Verified locally end-to-end: - download-hermes Debug -> hermes-ios/0.14.0 (200), recompose -> hermesvm.xcframework with 8 slices incl. macos-arm64_x86_64 (recomposed=true) - RCT_HERMES_V1_ENABLED=1 -> hermes-ios/250829098.0.2 (200) - V0/V1 x debug/release URLs all resolve 200 on repo1.maven.org Result: 0.83 downloads upstream's prebuilt Hermes (on par with RN 0.83's default V0 engine) and no longer builds Hermes from source, so the from-source V1 machinery is unnecessary on 0.83. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/resolve-hermes.mts | 147 +++++++++++++++-------------- 1 file changed, 78 insertions(+), 69 deletions(-) diff --git a/.github/scripts/resolve-hermes.mts b/.github/scripts/resolve-hermes.mts index 6a59d0d57397..d1fede955c23 100644 --- a/.github/scripts/resolve-hermes.mts +++ b/.github/scripts/resolve-hermes.mts @@ -17,9 +17,6 @@ import { $, echo, fs, path } from 'zx'; // Use createRequire to import CommonJS modules from ESM context const require = createRequire(import.meta.url); const { - findMatchingHermesVersion, - findVersionAtMergeBase, - getLatestStableVersionFromNPM, hermesCommitAtMergeBase, } = require('../../packages/react-native/scripts/ios-prebuild/microsoft-hermes.js'); const { @@ -34,90 +31,102 @@ function setActionOutput(key: string, value: string) { } /** - * Downloads the upstream Hermes tarball from Maven or Sonatype. + * Reads the prebuilt Hermes artifact version from + * packages/react-native/sdks/hermes-engine/version.properties. * - * Tries multiple version resolution strategies in order: - * 1. Mapped version from peerDependencies (stable branches) - * 2. Version at merge base with facebook/react-native (main branch) - * 3. Latest stable version from npm (last resort) - * - * Returns {tarballPath, version} on success, or null if no tarball is available. + * Mirrors the upstream CocoaPods podspec: HERMES_V1_VERSION_NAME when + * RCT_HERMES_V1_ENABLED=1, otherwise HERMES_VERSION_NAME (the RN 0.83 default, + * V0). This is the Hermes version (e.g. '0.14.0') — which is how upstream keys + * the prebuilt tarballs on Maven — not the react-native version. */ -async function downloadUpstreamHermesTarball( - buildType: string = 'Debug', -): Promise<{ tarballPath: string; version: string } | null> { - const packageJsonPath = path.resolve( - import.meta.dirname!, '..', '..', 'packages', 'react-native', 'package.json', +function resolveHermesArtifactVersion(): string | null { + const propsPath = path.resolve( + import.meta.dirname!, '..', '..', + 'packages', 'react-native', 'sdks', 'hermes-engine', 'version.properties', ); - - // Build a list of candidate versions to try (in priority order) - const candidates: string[] = []; - - const mapped = findMatchingHermesVersion(packageJsonPath); - if (mapped != null) { - candidates.push(mapped); - } - - const mergeBaseVersion = findVersionAtMergeBase(); - if (mergeBaseVersion != null && !candidates.includes(mergeBaseVersion)) { - candidates.push(mergeBaseVersion); - } - try { - const latestStable = await getLatestStableVersionFromNPM(); - if (!candidates.includes(latestStable)) { - candidates.push(latestStable); + const props: Record = {}; + for (const line of fs.readFileSync(propsPath, 'utf8').split('\n')) { + const eq = line.indexOf('='); + if (eq > 0) { + props[line.slice(0, eq).trim()] = line.slice(eq + 1).trim(); + } } + const key = + process.env.RCT_HERMES_V1_ENABLED === '1' + ? 'HERMES_V1_VERSION_NAME' + : 'HERMES_VERSION_NAME'; + const version = props[key]; + return version != null && version.length > 0 ? version : null; } catch { - // npm lookup failed, continue with what we have + return null; } +} - if (candidates.length === 0) { - echo('Could not determine any upstream version to download Hermes tarball'); +/** + * Downloads the upstream prebuilt Hermes tarball from Maven (release) or + * Sonatype snapshots (nightly). + * + * Upstream publishes prebuilt Hermes under `com/facebook/hermes/hermes-ios/`, + * keyed by the Hermes version from version.properties — matching the CocoaPods + * `release_tarball_url` in sdks/hermes-engine/hermes-utils.rb. (The fork + * previously queried `com/facebook/react/react-native-artifacts/`, + * which has no Hermes classifier, so every resolve fell back to building Hermes + * from source.) + * + * Returns {tarballPath, version} on success, or null if no tarball is available + * (callers then fall back to building Hermes from source). + */ +async function downloadUpstreamHermesTarball( + buildType: string = 'Debug', +): Promise<{ tarballPath: string; version: string } | null> { + const version = resolveHermesArtifactVersion(); + if (version == null) { + echo('Could not read Hermes version from sdks/hermes-engine/version.properties'); return null; } const mavenRepoUrl = 'https://repo1.maven.org/maven2'; - const namespace = 'com/facebook/react'; - - for (const version of candidates) { - const releaseUrl = `${mavenRepoUrl}/${namespace}/react-native-artifacts/${version}/react-native-artifacts-${version}-hermes-ios-${buildType.toLowerCase()}.tar.gz`; - const nightlyUrl = await computeNightlyTarballURL( - version, - buildType, - 'react-native-artifacts', - `hermes-ios-${buildType.toLowerCase()}.tar.gz`, - ); - const urlsToTry = [releaseUrl]; - if (nightlyUrl) { - urlsToTry.push(nightlyUrl); - } + const namespace = 'com/facebook/hermes'; + const flavor = buildType.toLowerCase(); - for (const tarballUrl of urlsToTry) { - echo(`Trying upstream Hermes tarball (version: ${version}, ${buildType}) at ${tarballUrl}...`); - - try { - const response = await fetch(tarballUrl); - if (!response.ok) { - echo(`Tarball not available: ${response.status} ${response.statusText}`); - continue; - } - - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-')); - const tarballPath = path.join(tmpDir, 'hermes-ios.tar.gz'); - const buffer = await response.arrayBuffer(); - fs.writeFileSync(tarballPath, Buffer.from(buffer)); - - echo(`Downloaded upstream Hermes tarball (${version}) to ${tarballPath}`); - return { tarballPath, version }; - } catch (e: any) { - echo(`Error downloading tarball for ${version}: ${e.message}`); + const releaseUrl = `${mavenRepoUrl}/${namespace}/hermes-ios/${version}/hermes-ios-${version}-hermes-ios-${flavor}.tar.gz`; + const nightlyUrl = await computeNightlyTarballURL( + version, + buildType, + 'hermes-ios', + `hermes-ios-${flavor}.tar.gz`, + ); + + const urlsToTry = [releaseUrl]; + if (nightlyUrl) { + urlsToTry.push(nightlyUrl); + } + + for (const tarballUrl of urlsToTry) { + echo(`Trying upstream Hermes tarball (version: ${version}, ${buildType}) at ${tarballUrl}...`); + + try { + const response = await fetch(tarballUrl); + if (!response.ok) { + echo(`Tarball not available: ${response.status} ${response.statusText}`); continue; } + + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-')); + const tarballPath = path.join(tmpDir, 'hermes-ios.tar.gz'); + const buffer = await response.arrayBuffer(); + fs.writeFileSync(tarballPath, Buffer.from(buffer)); + + echo(`Downloaded upstream Hermes tarball (${version}) to ${tarballPath}`); + return { tarballPath, version }; + } catch (e: any) { + echo(`Error downloading tarball for ${version}: ${e.message}`); + continue; } } - echo('No upstream Hermes tarball found for any candidate version — will build from source.'); + echo('No upstream Hermes tarball found — will build from source.'); return null; } From 3b5da98ff47439c2bef202efefdfdf2769f5d938 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Jul 2026 16:18:21 -0700 Subject: [PATCH 08/10] fix(hermes): resolve pinned Hermes tag instead of merge-base commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macOS 'resolve Hermes' from-source fallback resolved a Hermes commit from the merge base with facebook/react-native. Now that upstream pins the Hermes version in sdks/.hermesversion (V0) and sdks/.hermesv1version (V1) — files that travel with the RN source — resolve-commit reads the pinned tag (a valid actions/checkout ref) instead. Retires the merge-base heuristic from this path; the from-source fallback jobs are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/resolve-hermes.mts | 37 ++++++++++++++++--- .../workflows/microsoft-resolve-hermes.yml | 9 +++-- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.github/scripts/resolve-hermes.mts b/.github/scripts/resolve-hermes.mts index d1fede955c23..efff51948587 100644 --- a/.github/scripts/resolve-hermes.mts +++ b/.github/scripts/resolve-hermes.mts @@ -16,9 +16,6 @@ import { $, echo, fs, path } from 'zx'; // Use createRequire to import CommonJS modules from ESM context const require = createRequire(import.meta.url); -const { - hermesCommitAtMergeBase, -} = require('../../packages/react-native/scripts/ios-prebuild/microsoft-hermes.js'); const { computeNightlyTarballURL, } = require('../../packages/react-native/scripts/ios-prebuild/utils.js'); @@ -63,6 +60,30 @@ function resolveHermesArtifactVersion(): string | null { } } +/** + * Reads the pinned Hermes tag from the RN source checkout. + * + * RCT_HERMES_V1_ENABLED=1 opts into the V1 tag; otherwise use the default V0 + * tag. These tags are valid refs in facebook/hermes and can be passed directly + * to actions/checkout. + */ +function resolveHermesTag(): string | null { + const tagFile = + process.env.RCT_HERMES_V1_ENABLED === '1' + ? '.hermesv1version' + : '.hermesversion'; + const tagPath = path.resolve( + import.meta.dirname!, '..', '..', + 'packages', 'react-native', 'sdks', tagFile, + ); + try { + const tag = fs.readFileSync(tagPath, 'utf8').trim(); + return tag.length > 0 ? tag : null; + } catch { + return null; + } +} + /** * Downloads the upstream prebuilt Hermes tarball from Maven (release) or * Sonatype snapshots (nightly). @@ -249,9 +270,13 @@ switch (command) { break; } case 'resolve-commit': { - const { commit } = hermesCommitAtMergeBase(); - setActionOutput('hermes-commit', commit); - echo(`Resolved Hermes commit: ${commit}`); + const tag = resolveHermesTag(); + if (tag == null) { + echo('Could not read pinned Hermes tag from sdks/.hermesversion or sdks/.hermesv1version'); + process.exit(1); + } + setActionOutput('hermes-commit', tag); + echo(`Resolved Hermes tag: ${tag}`); break; } default: diff --git a/.github/workflows/microsoft-resolve-hermes.yml b/.github/workflows/microsoft-resolve-hermes.yml index 78f590715ddc..ab153d6e8290 100644 --- a/.github/workflows/microsoft-resolve-hermes.yml +++ b/.github/workflows/microsoft-resolve-hermes.yml @@ -3,7 +3,8 @@ # Strategy (fast path first): # 1. Download upstream Hermes tarball from Maven/Sonatype # 2. If found → recompose xcframework (add macOS slice) → upload artifact → done -# 3. If not found → resolve Hermes commit at merge base → check cache → upload if cached +# 3. If not found → resolve pinned Hermes tag from sdks/.hermesversion or +# sdks/.hermesv1version → check cache → upload if cached # # Build-from-source fallback (only when recomposed != true AND cache-hit != true): # build-hermesc → build 5 platform slices in parallel → assemble universal xcframework @@ -67,9 +68,9 @@ jobs: path: hermes-destroot retention-days: 30 - # Step 3 (fallback): No upstream tarball — resolve the Hermes commit hash - # at the merge base with facebook/react-native and check the build cache. - - name: Resolve Hermes commit at merge base + # Step 3 (fallback): No upstream tarball — resolve the pinned Hermes tag + # from sdks/.hermesversion or sdks/.hermesv1version and check the build cache. + - name: Resolve pinned Hermes tag if: steps.recompose.outputs.recomposed != 'true' id: resolve run: node .github/scripts/resolve-hermes.mts resolve-commit From 850c1a23531d636e4a761aa5f1411a8e1b48ef3d Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Jul 2026 16:21:58 -0700 Subject: [PATCH 09/10] refactor(hermes): retire merge-base heuristic; defer to pinned version files Revert scripts/ios-prebuild/hermes.js fully to upstream (facebook/0.83-stable), removing the fork's [macOS] Hermes machinery: the RN-version mapping, the build-from-hermes-commit source type, and buildFromHermesCommit. This also removes a latent bug where the react-native version was fed into the Hermes Maven coordinate (com/facebook/hermes/hermes-ios/), which 404s on 0.83. In macOS CI this path is short-circuited anyway (HERMES_VERSION=prebuilt). In sdks/hermes-engine/hermes-utils.rb, restore podspec_source_build_from_github_main to upstream and delete the [macOS] hermes_commit_at_merge_base helper (plus its now-unused open3/json/tmpdir requires). Hermes resolution on 0.83 stays covered: the release tarball (hermes-ios/0.14.0) serves the prebuilt path, and BUILD_FROM_GITHUB_TAG (sdks/.hermesversion / .hermesv1version) serves from-source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../scripts/ios-prebuild/hermes.js | 156 +----------------- .../sdks/hermes-engine/hermes-utils.rb | 64 +------ 2 files changed, 5 insertions(+), 215 deletions(-) diff --git a/packages/react-native/scripts/ios-prebuild/hermes.js b/packages/react-native/scripts/ios-prebuild/hermes.js index dcc3e47c3791..72c0c4c2073a 100644 --- a/packages/react-native/scripts/ios-prebuild/hermes.js +++ b/packages/react-native/scripts/ios-prebuild/hermes.js @@ -8,14 +8,9 @@ * @format */ -const { - findMatchingHermesVersion, - hermesCommitAtMergeBase, -} = require('./microsoft-hermes'); // [macOS] const {computeNightlyTarballURL, createLogger} = require('./utils'); const {execSync} = require('child_process'); const fs = require('fs'); -const os = require('os'); // [macOS] const path = require('path'); const stream = require('stream'); const {promisify} = require('util'); @@ -61,27 +56,6 @@ async function prepareHermesArtifactsAsync( // Resolve the version from the environment variable or use the default version let resolvedVersion = process.env.HERMES_VERSION ?? 'nightly'; - // [macOS] Map macOS version to upstream RN version for artifact lookup. - let allowBuildFromSource = false; - if (!process.env.HERMES_VERSION) { - const packageJsonPath = path.resolve( - __dirname, - '..', - '..', - 'package.json', - ); - const mappedVersion = findMatchingHermesVersion(packageJsonPath); - if (mappedVersion != null) { - hermesLog( - `Using mapped upstream version for Hermes lookup: ${mappedVersion}`, - ); - resolvedVersion = mappedVersion; - } else { - allowBuildFromSource = true; - } - } - // macOS] - if (resolvedVersion === 'nightly') { hermesLog('Using latest nightly tarball'); const hermesVersion = await getNightlyVersionFromNPM(); @@ -100,11 +74,7 @@ async function prepareHermesArtifactsAsync( return artifactsPath; } - const sourceType = await hermesSourceType( - resolvedVersion, - buildType, - allowBuildFromSource, // [macOS] - ); + const sourceType = await hermesSourceType(resolvedVersion, buildType); localPath = await resolveSourceFromSourceType( sourceType, resolvedVersion, @@ -154,19 +124,16 @@ type HermesEngineSourceType = | 'local_prebuilt_tarball' | 'download_prebuild_tarball' | 'download_prebuilt_nightly_tarball' - | 'build_from_hermes_commit' // [macOS] */ const HermesEngineSourceTypes /*:{ +DOWNLOAD_PREBUILD_TARBALL: "download_prebuild_tarball", +DOWNLOAD_PREBUILT_NIGHTLY_TARBALL: "download_prebuilt_nightly_tarball", - +LOCAL_PREBUILT_TARBALL: "local_prebuilt_tarball", - +BUILD_FROM_HERMES_COMMIT: "build_from_hermes_commit" // [macOS] + +LOCAL_PREBUILT_TARBALL: "local_prebuilt_tarball" } */ = { LOCAL_PREBUILT_TARBALL: 'local_prebuilt_tarball', DOWNLOAD_PREBUILD_TARBALL: 'download_prebuild_tarball', DOWNLOAD_PREBUILT_NIGHTLY_TARBALL: 'download_prebuilt_nightly_tarball', - BUILD_FROM_HERMES_COMMIT: 'build_from_hermes_commit', // [macOS] }; /** @@ -265,7 +232,6 @@ async function hermesArtifactExists( async function hermesSourceType( version /*: string */, buildType /*: BuildFlavor */, - allowBuildFromSource /*: boolean */ = false, // [macOS] ) /*: Promise */ { if (hermesEngineTarballEnvvarDefined()) { hermesLog('Using local prebuild tarball'); @@ -285,15 +251,6 @@ async function hermesSourceType( return HermesEngineSourceTypes.DOWNLOAD_PREBUILT_NIGHTLY_TARBALL; } - // [macOS] Fall back to building Hermes from the merge-base commit. - if (allowBuildFromSource) { - hermesLog( - 'No prebuilt Hermes artifact found. Will attempt to resolve from merge base with facebook/react-native.', - ); - return HermesEngineSourceTypes.BUILD_FROM_HERMES_COMMIT; - } - // macOS] - hermesLog( 'Using download prebuild nightly tarball - this is a fallback and might not work.', ); @@ -313,8 +270,6 @@ async function resolveSourceFromSourceType( return downloadPrebuildTarball(version, buildType, artifactsPath); case HermesEngineSourceTypes.DOWNLOAD_PREBUILT_NIGHTLY_TARBALL: return downloadPrebuiltNightlyTarball(version, buildType, artifactsPath); - case HermesEngineSourceTypes.BUILD_FROM_HERMES_COMMIT: // [macOS] - return buildFromHermesCommit(version, buildType, artifactsPath); default: abort( `[Hermes] Unsupported or invalid source type provided: ${sourceType}`, @@ -421,113 +376,6 @@ async function downloadHermesTarball( return destPath; } -// [macOS -/** - * Handles the case where no prebuilt Hermes artifacts are available. - * Determines the Hermes commit at the merge base with facebook/react-native - * and provides actionable guidance for building Hermes. - */ -async function buildFromHermesCommit( - version /*: string */, - buildType /*: BuildFlavor */, - artifactsPath /*: string */, -) /*: Promise */ { - const {commit, timestamp} = hermesCommitAtMergeBase(); - hermesLog( - `Building Hermes from source at commit ${commit} (merge base timestamp: ${timestamp})`, - ); - - const HERMES_GITHUB_URL = 'https://github.com/facebook/hermes.git'; - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-build-')); - const hermesDir = path.join(tmpDir, 'hermes'); - - try { - // Clone Hermes at the identified commit using the most efficient - // single-fetch pattern (see https://github.com/actions/checkout) - hermesLog(`Cloning Hermes at commit ${commit}...`); - execSync(`git init "${hermesDir}"`, {stdio: 'inherit'}); - execSync(`git -C "${hermesDir}" remote add origin ${HERMES_GITHUB_URL}`, { - stdio: 'inherit', - }); - execSync( - `git -C "${hermesDir}" fetch --no-tags --depth 1 origin +${commit}:refs/remotes/origin/main`, - {stdio: 'inherit', timeout: 300000}, - ); - execSync(`git -C "${hermesDir}" checkout main`, {stdio: 'inherit'}); - - const reactNativeRoot = path.resolve(__dirname, '..', '..'); - const buildScript = path.join( - reactNativeRoot, - 'sdks', - 'hermes-engine', - 'utils', - 'build-ios-framework.sh', - ); - - const buildEnv = { - ...process.env, - BUILD_TYPE: buildType, - HERMES_PATH: hermesDir, - JSI_PATH: path.join(hermesDir, 'API', 'jsi'), - REACT_NATIVE_PATH: reactNativeRoot, - // Deployment targets matching react-native-macos minimums - IOS_DEPLOYMENT_TARGET: '15.1', - MAC_DEPLOYMENT_TARGET: '14.0', - XROS_DEPLOYMENT_TARGET: '1.0', - RELEASE_VERSION: version, - }; - - hermesLog(`Building Hermes frameworks (${buildType})...`); - execSync(`bash "${buildScript}"`, { - stdio: 'inherit', - cwd: hermesDir, - timeout: 3600000, // 60 minutes - env: buildEnv, - }); - - // Create tarball from the destroot (same structure as Maven artifacts) - const tarballName = `hermes-ios-${buildType.toLowerCase()}.tar.gz`; - const tarballPath = path.join(artifactsPath, tarballName); - hermesLog('Creating Hermes tarball from build output...'); - execSync(`tar -czf "${tarballPath}" -C "${hermesDir}" destroot`, { - stdio: 'inherit', - }); - - hermesLog(`Hermes built from source and packaged at ${tarballPath}`); - return tarballPath; - } catch (e) { - // Dump CMake error logs before cleanup for debugging - try { - const cmakeErrorLog = path.join( - hermesDir, - 'build_host_hermesc', - 'CMakeFiles', - 'CMakeError.log', - ); - if (fs.existsSync(cmakeErrorLog)) { - hermesLog('=== CMakeError.log ==='); - hermesLog(fs.readFileSync(cmakeErrorLog, 'utf8')); - } - } catch (_) { - // ignore - } - - abort( - `[Hermes] Failed to build Hermes from source at commit ${commit}.\n` + - `Error: ${e.message}\n` + - `To resolve, either:\n` + - ` 1. Set HERMES_ENGINE_TARBALL_PATH to a local Hermes tarball path\n` + - ` 2. Set HERMES_VERSION to an upstream RN version with published artifacts\n` + - ` 3. Build Hermes manually from commit ${commit} and provide the tarball path via HERMES_ENGINE_TARBALL_PATH`, - ); - return ''; // unreachable - } finally { - // Clean up - fs.rmSync(tmpDir, {recursive: true, force: true}); - } -} -// macOS] - function abort(message /*: string */) { hermesLog(message, 'error'); throw new Error(message); diff --git a/packages/react-native/sdks/hermes-engine/hermes-utils.rb b/packages/react-native/sdks/hermes-engine/hermes-utils.rb index 2118dddea813..0686a7532c5d 100644 --- a/packages/react-native/sdks/hermes-engine/hermes-utils.rb +++ b/packages/react-native/sdks/hermes-engine/hermes-utils.rb @@ -5,9 +5,6 @@ require 'net/http' require 'rexml/document' -require 'open3' # [macOS] -require 'json' # [macOS] -require 'tmpdir' # [macOS] HERMES_GITHUB_URL = "https://github.com/facebook/hermes.git" ENV_BUILD_FROM_SOURCE = "RCT_BUILD_HERMES_FROM_SOURCE" @@ -187,21 +184,9 @@ def podspec_source_build_from_github_tag(react_native_path) end def podspec_source_build_from_github_main() - # branch = hermes_v1_enabled() ? "250829098.0.0-stable" : "main" - # hermes_log("Using the latest commit from #{branch}.") - # return {:git => HERMES_GITHUB_URL, :commit => `git ls-remote #{HERMES_GITHUB_URL} #{branch} | cut -f 1`.strip} - - # [macOS - # The logic for this is a bit different on macOS. - # Since react-native-macos lags slightly behind facebook/react-native, we can't always use - # the latest Hermes commit because Hermes and JSI don't always guarantee backwards compatibility. - # Instead, we take the commit hash of Hermes at the time of the merge base with facebook/react-native. - tuple = hermes_commit_at_merge_base() - commit = tuple[:commit] - timestamp = tuple[:timestamp] - hermes_log("Using Hermes commit from the merge base with facebook/react-native: #{commit} and timestamp: #{timestamp}") - return {:git => HERMES_GITHUB_URL, :commit => commit} - # macOS] + branch = hermes_v1_enabled() ? "250829098.0.0-stable" : "main" + hermes_log("Using the latest commit from #{branch}.") + return {:git => HERMES_GITHUB_URL, :commit => `git ls-remote #{HERMES_GITHUB_URL} #{branch} | cut -f 1`.strip} end def podspec_source_download_prebuild_release_tarball(react_native_path, version) @@ -224,49 +209,6 @@ def artifacts_dir() return File.join(Pod::Config.instance.project_pods_root, "hermes-engine-artifacts") end -# [macOS -def hermes_commit_at_merge_base() - # We don't need ls-remote because react-native-macos is a fork of facebook/react-native - fetch_result = `git fetch -q https://github.com/facebook/react-native.git` - if $?.exitstatus != 0 - abort <<-EOS - [Hermes] Failed to fetch facebook/react-native into the local repository. - EOS - end - - merge_base = `git merge-base FETCH_HEAD HEAD`.strip - if merge_base.empty? - abort <<-EOS - [Hermes] Unable to find the merge base between our HEAD and upstream's HEAD. - EOS - end - - timestamp = `git show -s --format=%ci #{merge_base}`.strip - if timestamp.empty? - abort <<-EOS - [Hermes] Unable to extract the timestamp for the merge base (#{merge_base}). - EOS - end - - commit = nil - Dir.mktmpdir do |tmpdir| - hermes_git_dir = File.join(tmpdir, "hermes.git") - # Explicitly use Hermes 'main' branch since the default branch changed to 'static_h' (Hermes V1) - `git clone -q --bare --filter=blob:none --single-branch --branch main #{HERMES_GITHUB_URL} "#{hermes_git_dir}"` - - # If all goes well, this will be the commit hash of Hermes at the time of the merge base on branch 'main' - commit = `git --git-dir="#{hermes_git_dir}" rev-list -1 --before="#{timestamp}" refs/heads/main`.strip - if commit.empty? - abort <<-EOS - [Hermes] Unable to find the Hermes commit hash at time #{timestamp} on branch 'main'. - EOS - end - end - - return { :commit => commit, :timestamp => timestamp} -end -# macOS] - def hermestag_file(react_native_path) if hermes_v1_enabled() return File.join(react_native_path, "sdks", ".hermesv1version") From 9059877e43e362b34350eaecf71d1f9e2d251038 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Jul 2026 16:24:11 -0700 Subject: [PATCH 10/10] refactor(hermes): remove unreferenced merge-base helper from microsoft-hermes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes hermesCommitAtMergeBase() (and its now-unused abort helper and os/path imports) now that Phases 1-2 removed its callers in resolve-hermes.mts and hermes.js. The RN-version resolvers (findMatchingHermesVersion, findVersionAtMergeBase, getLatestStableVersionFromNPM) are retained — they still key the reactnative-dependencies artifact, which legitimately stays react-native-version-keyed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../scripts/ios-prebuild/microsoft-hermes.js | 101 +----------------- 1 file changed, 4 insertions(+), 97 deletions(-) diff --git a/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js b/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js index 9bdeec72d8fc..968b8b2aafe2 100644 --- a/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js +++ b/packages/react-native/scripts/ios-prebuild/microsoft-hermes.js @@ -4,11 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * [macOS] Resolves Hermes artifacts for macOS fork branches. - * - * Library functions for version resolution and resolving Hermes commits. - * The CI entry point that orchestrates downloading, recomposing, and - * caching is at .github/scripts/resolve-hermes.mts. + * [macOS] Library functions for resolving React Native versions used by + * Hermes-related dependency artifacts. * * @flow * @format @@ -17,8 +14,6 @@ const {createLogger} = require('./utils'); const {execSync} = require('child_process'); const fs = require('fs'); -const os = require('os'); -const path = require('path'); const macosLog = createLogger('macOS'); @@ -56,88 +51,6 @@ function findMatchingHermesVersion( return null; } -/** - * Finds the Hermes commit at the merge base with facebook/react-native. - * Used on the main branch (1000.0.0) where no prebuilt artifacts exist. - * - * Since react-native-macos lags slightly behind facebook/react-native, we can't always use - * the latest Hermes commit because Hermes and JSI don't always guarantee backwards compatibility. - * Instead, we take the commit hash of Hermes at the time of the merge base with facebook/react-native. - * - * This is the JavaScript equivalent of the Ruby `hermes_commit_at_merge_base` - * in sdks/hermes-engine/hermes-utils.rb. - */ -function hermesCommitAtMergeBase() /*: {| commit: string, timestamp: string |} */ { - const HERMES_GITHUB_URL = 'https://github.com/facebook/hermes.git'; - - // Fetch upstream react-native - macosLog('Fetching facebook/react-native to find merge base...'); - try { - execSync('git fetch -q https://github.com/facebook/react-native.git', { - stdio: 'pipe', - }); - } catch (e) { - abort( - '[Hermes] Failed to fetch facebook/react-native into the local repository.', - ); - } - - // Find merge base between our HEAD and upstream's HEAD - const mergeBase = execSync('git merge-base FETCH_HEAD HEAD', { - encoding: 'utf8', - }).trim(); - if (!mergeBase) { - abort( - "[Hermes] Unable to find the merge base between our HEAD and upstream's HEAD.", - ); - } - - // Get timestamp of merge base - const timestamp = execSync(`git show -s --format=%ci ${mergeBase}`, { - encoding: 'utf8', - }).trim(); - if (!timestamp) { - abort( - `[Hermes] Unable to extract the timestamp for the merge base (${mergeBase}).`, - ); - } - - // Clone Hermes bare (minimal) into a temp directory and find the commit - macosLog( - `Merge base timestamp: ${timestamp}. Cloning Hermes to find matching commit...`, - ); - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-')); - const hermesGitDir = path.join(tmpDir, 'hermes.git'); - - try { - // Explicitly use Hermes 'main' branch since the default branch changed to 'static_h' (Hermes V1) - execSync( - `git clone -q --bare --filter=blob:none --single-branch --branch main ${HERMES_GITHUB_URL} "${hermesGitDir}"`, - {stdio: 'pipe', timeout: 120000}, - ); - - // Find the Hermes commit at the time of the merge base on branch 'main' - const commit = execSync( - `git --git-dir="${hermesGitDir}" rev-list -1 --before="${timestamp}" refs/heads/main`, - {encoding: 'utf8'}, - ).trim(); - - if (!commit) { - abort( - `[Hermes] Unable to find the Hermes commit hash at time ${timestamp} on branch 'main'.`, - ); - } - - macosLog( - `Using Hermes commit from the merge base with facebook/react-native: ${commit} (timestamp: ${timestamp})`, - ); - return {commit, timestamp}; - } finally { - // Clean up temp directory - fs.rmSync(tmpDir, {recursive: true, force: true}); - } -} - /** * Finds the upstream react-native version at the merge base with facebook/react-native. * Falls back to null if the version at merge base is also 1000.0.0 (i.e. merge base is @@ -145,8 +58,8 @@ function hermesCommitAtMergeBase() /*: {| commit: string, timestamp: string |} * */ function findVersionAtMergeBase() /*: ?string */ { try { - // hermesCommitAtMergeBase() already fetches facebook/react-native, but we - // might not have FETCH_HEAD if this runs standalone. Fetch it. + // Ensure facebook/react-native is fetched so FETCH_HEAD is available when + // this runs standalone. execSync('git fetch -q https://github.com/facebook/react-native.git', { stdio: 'pipe', timeout: 60000, @@ -188,14 +101,8 @@ async function getLatestStableVersionFromNPM() /*: Promise */ { return json.version; } -function abort(message /*: string */) { - macosLog(message, 'error'); - throw new Error(message); -} - module.exports = { findMatchingHermesVersion, - hermesCommitAtMergeBase, findVersionAtMergeBase, getLatestStableVersionFromNPM, };