From 21d18c011d797320da55c8c326b29ce96d531053 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Apr 2026 11:38:59 -0700 Subject: [PATCH 1/4] 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 28bd7cbcf7d3e0f327c397360ec538b0a7818c44 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Apr 2026 14:19:06 -0700 Subject: [PATCH 2/4] 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 d79897985be3dd0b13fac7556c164c7740b6d7a6 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Apr 2026 14:32:20 -0700 Subject: [PATCH 3/4] 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 ee9d35832f031b2e3c026ffdcefa3e1beae8ec51 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 28 Apr 2026 14:58:56 -0700 Subject: [PATCH 4/4] 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 f6eefe500acd..ba1d02178ef6 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -945,6 +945,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(