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-pr.yml b/.github/workflows/microsoft-pr.yml index ee3a01e5dd01..f408a1b0c547 100644 --- a/.github/workflows/microsoft-pr.yml +++ b/.github/workflows/microsoft-pr.yml @@ -3,7 +3,7 @@ name: PR on: pull_request: types: [opened, synchronize, edited] - branches: [ "main", "*-stable", "release/*" ] + branches: [ "main", "*-stable", "release/*", "0.83-merge" ] concurrency: # Ensure single build of a pull request. `main` should not be affected. diff --git a/.github/workflows/microsoft-resolve-hermes.yml b/.github/workflows/microsoft-resolve-hermes.yml index 78f590715ddc..780f52cadae9 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 @@ -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 @@ -249,7 +251,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/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( diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 17945ce64e40..73068815def1 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -807,7 +807,7 @@ - (RCTPlatformView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event // BOOL isPointInside = [self pointInside:point withEvent:event]; - UIView *currentContainerView = self.currentContainerView; + RCTPlatformView *currentContainerView = self.currentContainerView; // [macOS] BOOL clipsToBounds = false; @@ -1045,19 +1045,19 @@ - (BOOL)styleWouldClipOverflowInk // `blur` applied, we need to wrap it in a SwiftUI view to render the effect. // In this case, `effectiveContentView` will be the content view inside the // SwiftUI wrapper. -- (UIView *)effectiveContentView +- (RCTPlatformView *)effectiveContentView // [macOS] { if (!ReactNativeFeatureFlags::enableSwiftUIBasedFilters()) { return self; } - UIView *effectiveContentView = self; + RCTPlatformView *effectiveContentView = self; // [macOS] if (self.styleNeedsSwiftUIContainer) { if (_swiftUIWrapper == nullptr) { _swiftUIWrapper = [RCTSwiftUIContainerViewWrapper new]; - UIView *swiftUIContentView = [[UIView alloc] init]; - for (UIView *subview = nullptr in self.subviews) { + RCTPlatformView *swiftUIContentView = [[RCTPlatformView alloc] init]; // [macOS] + for (RCTPlatformView *subview = nullptr in self.subviews) { // [macOS] [swiftUIContentView addSubview:subview]; } swiftUIContentView.clipsToBounds = self.clipsToBounds; @@ -1074,8 +1074,8 @@ - (UIView *)effectiveContentView effectiveContentView = _swiftUIWrapper.contentView; } else { if (_swiftUIWrapper != nullptr) { - UIView *swiftUIContentView = _swiftUIWrapper.contentView; - for (UIView *subview = nullptr in swiftUIContentView.subviews) { + RCTPlatformView *swiftUIContentView = _swiftUIWrapper.contentView; // [macOS] + for (RCTPlatformView *subview = nullptr in swiftUIContentView.subviews) { // [macOS] [self addSubview:subview]; } self.clipsToBounds = swiftUIContentView.clipsToBounds; @@ -1096,7 +1096,7 @@ - (UIView *)effectiveContentView // the view and is not affected by clipping. - (RCTUIView *)currentContainerView // [macOS] { - UIView *effectiveContentView = self.effectiveContentView; + RCTPlatformView *effectiveContentView = self.effectiveContentView; // [macOS] if (_useCustomContainerView) { if (!_containerView) { @@ -1357,7 +1357,7 @@ - (void)invalidateLayer if (primitive.type == FilterType::DropShadow) { if (_swiftUIWrapper != nullptr && std::holds_alternative(primitive.parameters)) { const auto &dropShadowParams = std::get(primitive.parameters); - UIColor *shadowColor = RCTUIColorFromSharedColor(dropShadowParams.color); + RCTUIColor *shadowColor = RCTUIColorFromSharedColor(dropShadowParams.color); // [macOS] [_swiftUIWrapper updateDropShadow:@(dropShadowParams.standardDeviation) x:@(dropShadowParams.offsetX) y:@(dropShadowParams.offsetY) @@ -2409,7 +2409,7 @@ - (BOOL)styleNeedsSwiftUIContainer return NO; } -- (void)transferVisualPropertiesFromView:(UIView *)sourceView toView:(UIView *)destinationView +- (void)transferVisualPropertiesFromView:(RCTPlatformView *)sourceView toView:(RCTPlatformView *)destinationView // [macOS] { // shadow destinationView.layer.shadowColor = sourceView.layer.shadowColor; @@ -2457,6 +2457,12 @@ - (void)transferVisualPropertiesFromView:(UIView *)sourceView toView:(UIView *)d } } +#if !TARGET_OS_OSX // [macOS] +// macOS provides its own implementations of these methods inside the +// `#if TARGET_OS_OSX` block above (see ~line 1846 onwards), so the iOS +// definitions must be guarded to avoid duplicate-declaration errors and +// to keep AppKit's responder model intact (NSView uses +// `acceptsFirstResponder` rather than `canBecomeFirstResponder`). - (BOOL)canBecomeFirstResponder { return YES; @@ -2512,6 +2518,7 @@ - (BOOL)resignFirstResponder return YES; } +#endif // [macOS] @end 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, }; 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