From f8926cdeb06da4462f6cc27298d31c5ca0f32c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 13 Aug 2026 15:07:38 +0000 Subject: [PATCH 1/2] Configure the host Hermes compiler for one architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Passing CMAKE_OSX_ARCHITECTURES=arm64;x86_64 to build a universal hermesc makes llvh's feature try-compiles fail — standard headers report as missing and the configure dies on CheckAtomic. Configure it the way Hermes and React Native do, for the host architecture only. hermesc is then native to the Mac that built the archive, so the archive name carries the host architecture: a Mac of the other architecture finds no published archive and builds its own, instead of downloading a hermesc it cannot execute. Also assign each command substitution before echoing it into GITHUB_OUTPUT. Inside `echo "x=$(cmd)"` the step's exit status is echo's, so the failing build above passed its step with an empty path and only surfaced one step later, as `gh release upload ""`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH --- .changeset/prebuilt-hermes-host-compiler.md | 15 +++++++++++++++ .github/workflows/hermes-prebuilt.yml | 13 ++++++++++--- docs/CLI.md | 4 +++- packages/host/src/node/cli/hermes-prebuilt.ts | 16 ++++++++++------ 4 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 .changeset/prebuilt-hermes-host-compiler.md diff --git a/.changeset/prebuilt-hermes-host-compiler.md b/.changeset/prebuilt-hermes-host-compiler.md new file mode 100644 index 00000000..c8dd3a99 --- /dev/null +++ b/.changeset/prebuilt-hermes-host-compiler.md @@ -0,0 +1,15 @@ +--- +"react-native-node-api": patch +--- + +Fix `prebuilt-hermes` failing to configure the host Hermes compiler. It passed +`CMAKE_OSX_ARCHITECTURES=arm64;x86_64` to build a universal `hermesc`, but a +multi-arch host configure makes llvh's feature try-compiles fail — standard +headers report as missing and the configure dies with "Host compiler appears to +require libatomic, but cannot find it". The host compiler is now configured the +way Hermes and React Native configure it, for the host architecture only. + +`hermesc` is consequently native to the Mac that built the archive, so the +archive name now carries the host architecture. An Intel Mac finds no published +archive for its architecture and builds its own, rather than downloading one +whose `hermesc` it cannot execute. diff --git a/.github/workflows/hermes-prebuilt.yml b/.github/workflows/hermes-prebuilt.yml index cf619033..9c9a1a79 100644 --- a/.github/workflows/hermes-prebuilt.yml +++ b/.github/workflows/hermes-prebuilt.yml @@ -43,12 +43,17 @@ jobs: - run: pnpm run build # Resolved from the test app so the archive is built against the React # Native version this repository actually pins. + # Each command substitution is assigned before it is echoed: inside + # `echo "x=$(cmd)"` the step's exit status is echo's, so a failing cmd + # passes the step with an empty value. - name: Resolve prebuilt Hermes name id: hermes working-directory: apps/test-app run: | - echo "archive=$(pnpm exec react-native-node-api prebuilt-hermes --print name)" >> "$GITHUB_OUTPUT" - echo "tag=$(pnpm exec react-native-node-api prebuilt-hermes --print tag)" >> "$GITHUB_OUTPUT" + archive=$(pnpm exec react-native-node-api prebuilt-hermes --print name) + tag=$(pnpm exec react-native-node-api prebuilt-hermes --print tag) + echo "archive=$archive" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" - name: Cache prebuilt Hermes uses: actions/cache@v6 with: @@ -59,7 +64,9 @@ jobs: - name: Build prebuilt Hermes id: build working-directory: apps/test-app - run: echo "path=$(pnpm exec react-native-node-api prebuilt-hermes --no-download)" >> "$GITHUB_OUTPUT" + run: | + path=$(pnpm exec react-native-node-api prebuilt-hermes --no-download) + echo "path=$path" >> "$GITHUB_OUTPUT" # --latest=false keeps these out of the "latest release" slot, which # belongs to the package releases changesets publishes. - name: Publish as a release asset diff --git a/docs/CLI.md b/docs/CLI.md index 58c8ec35..a8c62549 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -21,7 +21,9 @@ The archive is looked for in this order, and cached under `~/Library/Caches/reac 2. The [release asset](https://github.com/callstackincubator/react-native-node-api/releases) published for the pinned commit by the `Hermes prebuilt` workflow, unless `--no-download` is passed. 3. A local build from the vendored source, unless `--no-build` is passed. This requires macOS and Xcode, and takes a while — but only once per pinned commit. -Its name covers everything that changes its contents: the pinned Hermes commit, the React Native version whose `ReactCommon/jsi` it is compiled against, the build type and the platforms. That makes it usable as a CI cache key. +Its name covers everything that changes its contents: the pinned Hermes commit, the React Native version whose `ReactCommon/jsi` it is compiled against, the build type, the platforms and the host architecture. That makes it usable as a CI cache key. + +The host architecture is part of it because `destroot/bin/hermesc` is a native binary for whichever Mac built the archive. Archives are published from Apple Silicon runners, so an Intel Mac finds none to download and builds its own instead of getting a `hermesc` it cannot execute. - `[from]` — Path to a file inside the app package. Defaults to the current working directory. - `--react-native-package ` — The React Native package to resolve Hermes for. Defaults to `react-native`. diff --git a/packages/host/src/node/cli/hermes-prebuilt.ts b/packages/host/src/node/cli/hermes-prebuilt.ts index 125deb16..3470cd09 100644 --- a/packages/host/src/node/cli/hermes-prebuilt.ts +++ b/packages/host/src/node/cli/hermes-prebuilt.ts @@ -74,7 +74,10 @@ export function getPrebuiltDirectory() { * Identifies an archive by everything that changes its contents. The React * Native version is part of it because Hermes is compiled against that * package's ReactCommon/jsi: a JSI mismatch between the framework and the app - * linking it is an ABI break. + * linking it is an ABI break. The host architecture is part of it because the + * hermesc in destroot/bin is a native binary for whichever Mac built it, so a + * host of the other architecture has to build its own rather than download one + * it cannot execute. */ export function getArchiveName({ reactNativeVersion, @@ -89,7 +92,7 @@ export function getArchiveName({ // GitHub rewrites every character outside [A-Za-z0-9._-] in a release asset // name, so the name has to stay within that set to survive a round-trip. const platformSuffix = [...platforms].sort().join("-"); - return `hermes-${shortSha}-rn${reactNativeVersion}-${buildType}-${platformSuffix}.tar.gz`; + return `hermes-${shortSha}-rn${reactNativeVersion}-${buildType}-${platformSuffix}-${process.arch}.tar.gz`; } export function getReleaseTag() { @@ -174,9 +177,11 @@ async function buildArchive({ }, }); - // Configured here instead of letting build-apple-framework.sh's - // build_host_hermesc do it: that one takes no architectures, and the hermesc - // we ship has to run on both Apple Silicon and Intel Macs. + // Configured here rather than by build-apple-framework.sh's + // build_host_hermesc only so the build type is explicit — the pinned Hermes + // hard-errors without one. Do not add CMAKE_OSX_ARCHITECTURES: a multi-arch + // host configure makes llvh's try-compiles fail, down to "Host compiler + // appears to require libatomic, but cannot find it". if (!fs.existsSync(importHostCompilersPath)) { await run("cmake", [ "-S", @@ -185,7 +190,6 @@ async function buildArchive({ hermescPath, `-DJSI_DIR=${jsiPath}`, "-DCMAKE_BUILD_TYPE=Release", - "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64", ]); await run("cmake", [ "--build", From 0ebf6fe31c05e3226fa215cc87f3479e43ae1e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 13 Aug 2026 15:08:27 +0000 Subject: [PATCH 2/2] Trigger CI with the labels attached The workflow's pull_request trigger doesn't fire on `labeled`, so the label-gated jobs need a synchronize event to be evaluated against. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH