Skip to content

Commit 233fe34

Browse files
kraenhansenclaude
andcommitted
Use the prebuilt Hermes archive for iOS builds
Hermes is the iOS build: 17m15s of an 18m58s "Build test app" step. Point pod install at the archive built by `prebuilt-hermes` through HERMES_ENGINE_TARBALL_PATH, so hermes-engine.podspec vendors the prebuilt frameworks and its two Hermes script phases don't run at all. Building from source stays available behind REACT_NATIVE_NODE_API_HERMES_FROM_SOURCE=1, which is the faster loop while iterating on Hermes itself. react-native-macos stays on that path for now, see #392. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH
1 parent 8f91084 commit 233fe34

5 files changed

Lines changed: 81 additions & 14 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"react-native-node-api": minor
3+
---
4+
5+
Stop compiling Hermes as part of every iOS app build. The Cocoapods integration
6+
now resolves the pinned commit with `prebuilt-hermes` and hands the archive's
7+
path to React Native through `HERMES_ENGINE_TARBALL_PATH` — so
8+
`hermes-engine.podspec` vendors the prebuilt frameworks instead of running its
9+
"Build Hermesc" and "Build Hermes" script phases.
10+
11+
Building Hermes from source remains available and is the faster loop while
12+
iterating on Hermes itself, since Xcode then rebuilds it incrementally: set
13+
`REACT_NATIVE_NODE_API_HERMES_FROM_SOURCE=1` before `pod install`. Setting
14+
`REACT_NATIVE_OVERRIDE_HERMES_DIR` or `HERMES_ENGINE_TARBALL_PATH` yourself
15+
still takes precedence, and Android is unchanged.

.github/workflows/check.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,27 @@ jobs:
244244
ccache --set-config file_clone=true
245245
ccache --set-config depend_mode=true
246246
ccache --set-config inode_cache=true
247+
# Hermes dominates the iOS build (17m15s of an 18m58s "Build test app"
248+
# step, measured in #439) and only changes when the pinned commit does, so
249+
# it is built once into an archive that `pod install` injects through
250+
# HERMES_ENGINE_TARBALL_PATH. The archive name covers every input that
251+
# changes its contents — the pinned commit, the React Native version whose
252+
# JSI it is compiled against, the build type and the platforms — which
253+
# makes it the cache key too.
254+
- name: Resolve prebuilt Hermes name
255+
id: hermes
256+
run: echo "archive=$(pnpm exec react-native-node-api prebuilt-hermes --print name)" >> "$GITHUB_OUTPUT"
257+
working-directory: apps/test-app
258+
- name: Cache prebuilt Hermes
259+
uses: actions/cache@v6
260+
with:
261+
path: ~/Library/Caches/react-native-node-api/hermes-prebuilt
262+
key: ${{ steps.hermes.outputs.archive }}
263+
# Explicit rather than left to `pod install`, so a cold cache shows up as
264+
# its own step in the job log instead of as a mysteriously slow install.
265+
- name: Build prebuilt Hermes
266+
run: pnpm exec react-native-node-api prebuilt-hermes
267+
working-directory: apps/test-app
247268
# Must precede `pod install`: react-native-test-app embeds the resources
248269
# declared in app.json when generating the workspace, skipping missing
249270
# ones, and the app would then expect a Metro dev server at runtime.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ patch or workaround:
6060

6161
## Critical Build Dependencies
6262

63-
- **Vendored Hermes**: Builds Hermes from a pinned commit on the `static_h` branch, which carries Hermes' first-party Node-API implementation (`API/napi`, target `hermesNapi`). The pin lives in `packages/host/src/node/cli/hermes.ts` and is fetched by the `vendor-hermes` command.
63+
- **Vendored Hermes**: Builds Hermes from a pinned commit on the `static_h` branch, which carries Hermes' first-party Node-API implementation (`API/napi`, target `hermesNapi`). The pin lives in `packages/host/src/node/cli/hermes.ts` and is fetched by the `vendor-hermes` command. On Apple platforms it is built once into an archive by the `prebuilt-hermes` command and injected into `pod install` through `HERMES_ENGINE_TARBALL_PATH`; Android and the opt-in `REACT_NATIVE_NODE_API_HERMES_FROM_SOURCE=1` path build it from that checkout instead. See [docs/CLI.md](docs/CLI.md).
6464
- **Prebuilt Binary Spec**: All tools must output to the exact naming scheme:
6565
- Android: `*.android.node/` with jniLibs structure + `react-native-node-api-module` marker file
6666
- iOS: `*.apple.node` (XCFramework renamed) + marker file

docs/CLI.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Run `npx react-native-node-api help` or `npx react-native-node-api help <command
1313
1414
## `prebuilt-hermes [from]`
1515

16-
Resolves an archive of the pinned Hermes, prebuilt for Apple platforms, and prints its path. The archive holds the `destroot` layout React Native's `hermes-engine.podspec` expects from a tarball pointed at by `HERMES_ENGINE_TARBALL_PATH`, so an app that sets that variable vendors the prebuilt frameworks instead of compiling Hermes as part of its own build.
16+
Resolves an archive of the pinned Hermes, prebuilt for Apple platforms, and prints its path. This is what the Cocoapods integration uses by default: the path is handed to React Native through the `HERMES_ENGINE_TARBALL_PATH` environment variable, and `hermes-engine.podspec` then vendors the frameworks out of the archive instead of compiling Hermes as part of every app build.
1717

1818
The archive is looked for in this order, and cached under `~/Library/Caches/react-native-node-api/hermes-prebuilt` (overridable with `REACT_NATIVE_NODE_API_CACHE_PATH`):
1919

@@ -35,10 +35,14 @@ The host architecture is part of it because `destroot/bin/hermesc` is a native b
3535
- `--no-build` — Don't build the archive locally when none is published.
3636
- `--print <property>` — Print `name`, `tag` or `url` of the archive instead of resolving it.
3737

38+
To build Hermes from source as part of the app build instead — which is the faster loop while iterating on Hermes itself, since Xcode then rebuilds it incrementally — set `REACT_NATIVE_NODE_API_HERMES_FROM_SOURCE=1` before running `pod install`. Setting `REACT_NATIVE_OVERRIDE_HERMES_DIR` or `HERMES_ENGINE_TARBALL_PATH` yourself also takes precedence.
39+
3840
## `vendor-hermes [from]`
3941

4042
Clones the pinned commit of Hermes' `static_h` branch (which carries Hermes' first-party Node-API implementation) into the `sdks/node-api-hermes` directory of the app's `react-native` package, so the native build can compile against it. Prints the path to the vendored checkout on success.
4143

44+
This is how Hermes is built on Android, and on Apple when the from-source path described above is selected.
45+
4246
- `[from]` — Path to a file inside the app package. Defaults to the current working directory.
4347
- `--react-native-package <package-name>` — The React Native package to vendor Hermes into. Defaults to `react-native`.
4448
- `--silent` — Don't print anything except the final path. Defaults to `false`.
Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,51 @@
11
Pod::UI.warn "!!! CONFIGURING HERMES WITH NODE-API SUPPORT !!!"
22

3-
if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].nil?
4-
def get_react_native_package
5-
if caller.any? { |frame| frame.include?("node_modules/react-native-macos/") }
6-
return "react-native-macos"
7-
elsif caller.any? { |frame| frame.include?("node_modules/react-native/") }
8-
return "react-native"
9-
else
10-
raise "Unable to determine React Native package from call stack."
11-
end
3+
def node_api_react_native_package
4+
if caller.any? { |frame| frame.include?("node_modules/react-native-macos/") }
5+
return "react-native-macos"
6+
elsif caller.any? { |frame| frame.include?("node_modules/react-native/") }
7+
return "react-native"
8+
else
9+
raise "Unable to determine React Native package from call stack."
1210
end
11+
end
12+
13+
def node_api_run_cli(command, react_native_package)
14+
args = [
15+
command,
16+
"--react-native-package", react_native_package,
17+
"--silent", Pod::Config.instance.installation_root.to_s
18+
].map { |arg| "'#{arg}'" }.join(" ")
19+
result = `npx react-native-node-api #{args}`.strip
20+
raise "Hermes setup failed: 'react-native-node-api #{command}' exited with #{$?.exitstatus}" unless $?.success?
21+
result
22+
end
1323

14-
VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --react-native-package '#{get_react_native_package()}' --silent '#{Pod::Config.instance.installation_root}'`.strip
15-
ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] = VENDORED_HERMES_DIR
24+
if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].nil? && ENV['HERMES_ENGINE_TARBALL_PATH'].nil?
25+
react_native_package = node_api_react_native_package()
26+
# Building from source keeps Hermes inside the Xcode build, where it rebuilds
27+
# incrementally — the faster loop while iterating on Hermes itself. Otherwise
28+
# the pinned commit is resolved to an archive built once and reused.
29+
#
30+
# react-native-macos stays on the source path: the archive is only produced
31+
# and exercised for the iOS platforms today.
32+
if ENV['REACT_NATIVE_NODE_API_HERMES_FROM_SOURCE'].to_s == '1' || react_native_package == "react-native-macos"
33+
ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] = node_api_run_cli("vendor-hermes", react_native_package)
34+
else
35+
ENV['HERMES_ENGINE_TARBALL_PATH'] = node_api_run_cli("prebuilt-hermes", react_native_package)
36+
end
1637
end
1738

1839
if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] && !ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].empty?
1940
if Dir.exist?(ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'])
20-
Pod::UI.info "[Node-API] Using overridden Hermes in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}"
41+
Pod::UI.info "[Node-API] Building Hermes from source in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}"
2142
else
2243
raise "Hermes setup failed: Expected override to exist in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}"
2344
end
45+
elsif ENV['HERMES_ENGINE_TARBALL_PATH'] && !ENV['HERMES_ENGINE_TARBALL_PATH'].empty?
46+
if File.exist?(ENV['HERMES_ENGINE_TARBALL_PATH'])
47+
Pod::UI.info "[Node-API] Using prebuilt Hermes from #{ENV['HERMES_ENGINE_TARBALL_PATH'].inspect}"
48+
else
49+
raise "Hermes setup failed: Expected prebuilt archive to exist at #{ENV['HERMES_ENGINE_TARBALL_PATH'].inspect}"
50+
end
2451
end

0 commit comments

Comments
 (0)