Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .github/scripts/resolve-hermes.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*
Expand All @@ -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}`;
Expand All @@ -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);
Expand All @@ -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`;
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/microsoft-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/microsoft-resolve-hermes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -118,15 +118,17 @@ 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:
HERMES_PATH: ${{ github.workspace }}/hermes
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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions packages/react-native/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -1357,7 +1357,7 @@ - (void)invalidateLayer
if (primitive.type == FilterType::DropShadow) {
if (_swiftUIWrapper != nullptr && std::holds_alternative<DropShadowParams>(primitive.parameters)) {
const auto &dropShadowParams = std::get<DropShadowParams>(primitive.parameters);
UIColor *shadowColor = RCTUIColorFromSharedColor(dropShadowParams.color);
RCTUIColor *shadowColor = RCTUIColorFromSharedColor(dropShadowParams.color); // [macOS]
[_swiftUIWrapper updateDropShadow:@(dropShadowParams.standardDeviation)
x:@(dropShadowParams.offsetX)
y:@(dropShadowParams.offsetY)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2512,6 +2518,7 @@ - (BOOL)resignFirstResponder

return YES;
}
#endif // [macOS]

@end

Expand Down
37 changes: 32 additions & 5 deletions packages/react-native/scripts/ios-prebuild/microsoft-hermes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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...');
Expand Down Expand Up @@ -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}'.`,
);
}

Expand Down Expand Up @@ -196,6 +222,7 @@ function abort(message /*: string */) {
module.exports = {
findMatchingHermesVersion,
hermesCommitAtMergeBase,
hermesV1Tag,
findVersionAtMergeBase,
getLatestStableVersionFromNPM,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading