Skip to content

Add cflags support to gyp-to-cmake - #454

Open
huytdps13400 wants to merge 25 commits into
callstackincubator:nextfrom
huytdps13400:fix/99-handle-gyp-cflags
Open

huytdps13400 wants to merge 25 commits into
callstackincubator:nextfrom
huytdps13400:fix/99-handle-gyp-cflags

Conversation

@huytdps13400

Copy link
Copy Markdown

Fixes #99

Summary

  • accept and validate target-level cflags in parsed binding.gyp input
  • generate target_compile_options(<target> PRIVATE ...) for those flags
  • preserve the transformer's existing command-expansion and space-escaping behavior
  • target the resolved CMake name when namespaced targets are enabled
  • add a minor changeset for gyp-to-cmake

This intentionally leaves cflags_cc, ldflags, variables, conditions, and target_defaults to their separate tracked issues.

TDD evidence

Parser tests first failed because valid cflags were rejected as an extra property and malformed values were accepted. After adding the parser boundary, transformer tests failed because no target_compile_options command was emitted. Both stages are now green.

Coverage includes:

  • valid and malformed cflags input
  • non-string array entries
  • multiple flags and escaped spaces
  • command expansion into a flag list
  • namespaced target output

Validation

Node 24.16.0 / pnpm 10.33.0:

  • pnpm --filter gyp-to-cmake test — 38/38 passed
  • pnpm run build — passed
  • pnpm test — 115/115 passed across gyp-to-cmake, host, and cmake-rn
  • pnpm lint — passed
  • pnpm prettier:check — passed
  • pnpm depcheck — passed
  • git diff --check — passed

kraenhansen and others added 25 commits August 13, 2026 10:14
Enters changeset pre mode with the "rc" tag and lets the release workflow
run on this branch, so 2.0.0-rc.N can be published under the "rc" dist tag
while main keeps releasing stable versions. Pre mode is repo-wide state
(.changeset/pre.json lists every package), which is why it lives here
rather than on main.

Exit pre mode before merging this branch back into main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Now that next is the default branch, pushes to it should get the same
coverage main gets, including the jobs gated on being on the trunk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Phase 1: vendor static_h Hermes, bump to RN 0.87 nightly

Begin migrating off the kraenhansen/hermes fork + JSI-patching path toward
Hermes' first-party Node-API (the static_h branch).

- vendor-hermes: shallow-fetch facebook/hermes at pinned static_h SHA
  0ae42446d1ae669508368b0a18e60c789f76735d; drop the JSI-header copy step
- patch-hermes.rb: rely on REACT_NATIVE_OVERRIDE_HERMES_DIR alone to trigger
  build-from-source; drop the no-op BUILD_FROM_SOURCE var and the obsolete
  RCT_USE_PREBUILT_RNCORE / JSI-patch guard
- CxxNodeApiHostModule: stub env=nullptr (real env arrives in Phase 2 via
  hermes_napi_create_env)
- bump react-native to 0.87.0-nightly-20260529-88857d22f (+ test-app deps,
  react-native-test-app 5.x); regenerate lockfile
- RN 0.87 fallout: add @types/babel__core, fix test-app tsconfig extends for
  the tightened @react-native/typescript-config exports map, delete the
  podspec test asserting the removed guard

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Resolve Xcode app project resiliently in workspaces

react-native-test-app 5.x generates the app's ReactTestApp.xcodeproj under
the nearest node_modules, which in a workspace is the app-local
node_modules (apps/test-app/node_modules/.generated), not the hoisted root.
The workspace can also accumulate stale references to a project under a
different node_modules.

findXcodeProject took the first fileRef unconditionally, which could be the
stale (non-existent) reference or the Pods project. Resolve every app
project reference and pick the first whose project.pbxproj exists on disk,
ignoring Pods.xcodeproj.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix test-app tooling for RN 0.87 / Metro 0.84

- Bump @rnx-kit/metro-config to ^2.2.4: 2.1.1 called metro-config's
  exclusionList as a bare function, but Metro 0.84 changed that module to a
  { default } export, breaking `react-native start`.
- Gradle wrapper bumped to 9.3.1 by react-native-test-app 5.x's
  configureGradleWrapper during pod install (RN 0.87 alignment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Phase 2: create a real Node-API env via hermes_napi_create_env

Replace the `env = nullptr` stub in CxxNodeApiHostModule with a real
Node-API environment: cast the JSI runtime to `IHermes`, read the
underlying `vm::Runtime*` via `getVMRuntimeUnsafe()`, and create the env
with `hermes_napi_create_env(vm, nullptr)`. The env is owned by the
runtime and cached on the module (shared across all addons).

This flips the Phase 1 baseline abort (`assert(status == napi_ok)` right
after `napi_create_object(env=nullptr, …)`) green: with
`MOCHA_REMOTE_CONTEXT=allTests` the iOS-sim suite now reports 14 passing
(node-addon-examples getting-started incl. the Rust ferric addon,
buffers, async, and a js-native-api node-test).

Linking note: the RN `hermesvm` framework force-loads `hermesNapi`, and
the public `hermes_napi_*` entry points are exported from it as long as
Hermes is built from a checkout that includes facebook/hermes #2044
("Export public hermes_napi entry points with NAPI macros") — which the
pinned SHA (0ae42446) already contains. No pod-side linker surgery or
source patching is required; just ensure the vendored checkout is
actually at the pinned SHA (a stale pre-#2044 checkout is what stripped
the symbol during bring-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Phase 2: bump Node-API to v10, drop engine/runtime split

All Node-API symbols are now sourced from Hermes' hermesNapi, so the old
engine (js_native_api → libhermes.so) / runtime (node_api →
libnode-api-host.so) distinction and the hand-maintained
IMPLEMENTED_RUNTIME_FUNCTIONS allow-list are obsolete.

- weak-node-api: getNodeApiFunctions defaults to v10 and no longer computes
  the dead `kind`/`libraryPath` fields; CMake compiles the generated
  weak_node_api.cpp at NAPI_VERSION=10 (145 → 155 symbols, adding the v9/v10
  node_api_* surface).
- generate-injector.mts: bind every symbol (no filter) and emit
  `#include <Versions.hpp>` first so the injector TU also compiles at v10.
- Versions.hpp: guarded bump to NAPI_VERSION 10.

Regenerated (gitignored) WeakNodeApiInjector.cpp + weak-node-api/generated
now expose all 155 symbols incl. TSFN and napi_make_callback. Verified:
build, prettier, lint, workspace unit tests, and the weak-node-api native
build + ctest all pass. iOS e2e pending (rides the cold re-vendor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* vendor-hermes: export public hermes_napi_* entry points

The clean Hermes build at the pinned SHA does NOT export
hermes_napi_create_env (and the other hermes_napi_* entry points). They are
declared in API/napi/hermes_napi.h with NAPI_EXTERN (visibility "default")
but — unlike the sibling js_native_api.h / node_api.h headers — without any
extern "C" wrapping, so they get C++ linkage. The mangled C++ symbols stay
out of the framework's dynamic export table under Hermes' global
-fvisibility=hidden, and a from-scratch build fails at the app link with
"Undefined symbol: hermes_napi_create_env".

vendor-hermes now wraps the hermes_napi.h declarations in
EXTERN_C_START / EXTERN_C_END (both available via the node_api.h include),
giving the entry points C linkage so they export under their unmangled C
names. This mirrors the upstream fix in facebook/hermes#2106. The patch is
idempotent (guarded on EXTERN_C_START) and asserts its anchors exist so a
future Hermes bump fails loudly rather than silently no-op'ing.

Also ignore **/build-tests/** in ESLint (CMake writes compiler_depend.ts
dependency files there that aren't real TypeScript).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* vendor-hermes: apply prettier formatting

Collapse the single-argument `.replace()` call in patchHermesNapiVisibility
onto one line to satisfy prettier:check (fixup for the hermes_napi patch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Regenerate pnpm-lock.yaml for RN 0.87 dependency bumps

Rebased onto main after the npm->pnpm migration (callstackincubator#381). The original PR's
two package-lock.json maintenance commits (restore public registry URLs,
restore pruned optional platform binaries) are dropped: both addressed
npm-specific lockfile problems that no longer exist under pnpm.

Regenerate pnpm-lock.yaml against the RN 0.87 nightly / react-native-test-app
5.x / @rnx-kit/metro-config bumps so the lockfile matches the workspace
manifests. Verified with pnpm install --frozen-lockfile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TVfanKvtyfSsoMgZv3DJtY

* vendor-hermes: advance pin to include upstream napi C-linkage fix

Move the pinned Hermes commit forward from 0ae42446 to efcf68e2 on the
static_h branch (a descendant, 18 commits ahead). The only relevant change
in that range is facebook/hermes#2106 "give hermes_napi.h public API C
linkage", which wraps the public hermes_napi_* entry points in extern "C".

That is exactly the fix we were applying locally after cloning: without C
linkage the mangled hermes_napi_create_env symbol stayed out of the
framework export table under Hermes' global -fvisibility=hidden. Now that
the fix is upstream at the pinned commit, drop patchHermesNapiVisibility and
its header-anchor constants entirely — the vendored checkout exports the
entry points as-is.

No commit in the bumped range touches getVMRuntimeUnsafe or the IHermes JSI
interface we depend on, so the unstable-accessor rationale for pinning still
holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TVfanKvtyfSsoMgZv3DJtY

* host: match hermes_napi_create_env C linkage after upstream #2106

The pinned Hermes commit now includes facebook/hermes#2106, which wraps the
public hermes_napi_* entry points in extern "C". Hermes therefore exports the
unmangled C symbol for hermes_napi_create_env.

CxxNodeApiHostModule forward-declares that entry point (to avoid including
Hermes' node_api.h) but did so with C++ linkage, so it referenced the mangled
name. After the pin bump the two no longer matched and the iOS app failed to
link with "Undefined symbol: hermes_napi_create_env".

Wrap the forward declaration in extern "C" so the reference resolves to the
exported unmangled symbol.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TVfanKvtyfSsoMgZv3DJtY

* android: inject ExecOperations for Gradle 9 compatibility (callstackincubator#386)

RN 0.87 bumps the Gradle wrapper to 9.x, which removed Project.exec(). The
linkNodeApiModules task used the bare `exec {}` closure in its doLast action,
failing every Android build (and gradle.test.ts on all platforms) with
"Could not find method exec()". Inject the ExecOperations service via an
@Inject-annotated interface and call injectedExecOps.execOps.exec {} instead.

Greens the ubuntu and macOS unit-test lanes. Windows surfaces a separate,
pre-existing RN 0.87 / Gradle 9 issue (missing react-native/tmp projectDir)
tracked separately.

* android: patch RN settings.gradle.kts /tmp projectDir for Windows (callstackincubator#387)

* android: patch RN settings.gradle.kts /tmp projectDir for Windows

The Windows unit-test lane failed configuring the React Native build-from-
source composite build:

    Configuring project ':packages:react-native' without an existing directory
    is not allowed. The configured projectDirectory '...\react-native\tmp'
    does not exist

React Native's own settings.gradle.kts declares the intermediate container
projects :packages and :packages:react-native with projectDir = file("/tmp"),
purely to satisfy Gradle 9's rule that every project in a path have an existing
folder. "/tmp" exists on the posix CI hosts but on Windows it is not an
absolute path, so Gradle resolves it to a non-existent <react-native>\tmp and
the build fails before any task runs. This is why only windows-latest was red
while ubuntu and macOS passed.

Add a pnpm patch replacing file("/tmp") with
file(System.getProperty("java.io.tmpdir", "/tmp")): the JVM temp dir is "/tmp"
on posix and %TEMP% on Windows, both of which always exist. Remove the patch
once React Native stops hardcoding "/tmp" upstream.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TVfanKvtyfSsoMgZv3DJtY

* android: point RN /tmp patch at the merged upstream fix

The upstream fix landed on react-native main as 908872a6 (2026-07-28,
react/react-native#57706), after the 0.87 branch cut — so 0.87-stable
does not carry it. Record that in the patch comment so the removal gate is
a concrete react-native version rather than "once upstream fixes it".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

* host: apply the Kotlin plugin only when built-in Kotlin is unavailable

AGP 9 ships built-in Kotlin support and enables it by default, which
registers the `kotlin` extension itself. Applying `kotlin-android` on top
of that fails the consumer's build with "Cannot add extension with name
'kotlin'", so any consumer who has migrated off the `builtInKotlin=false`
opt-out currently cannot build against this package.

Gate the plugin on the AGP major version and the consumer's opt-out, so
the library works both for consumers still on AGP 8 (or opted out while
they migrate) and for those already on built-in Kotlin. React Native's
own ReactAndroid no longer applies the Kotlin plugin either, as of 0.87.

Reuses the `com.android.Version` idiom already used by supportsNamespace().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* deps: bump react-native to 0.87.0-rc.4

Moves off the 0.87.0-nightly-20260529 pin onto the 0.87 release
candidate. The motivating change is AGP: the nightly still resolved AGP
8.12, while AGP 9.2.1 landed on the 0.87 line in mid-June. AGP 9 is what
react-native-test-app assumes for React Native >= 0.87 (it forces Gradle
9.4.1 and then uses the built-in Kotlin `kotlin {}` extension), so the
test app could not configure against the old pin.

The Windows `/tmp` projectDir patch is unchanged — settings.gradle.kts is
byte-identical between the two versions (same blob 2036e0f), so only the
file name and the patchedDependencies key move. The fix for it is still
main-only, so the patch stays until we are on 0.88+.

Also switches the two React Native facing tsconfigs to nodenext module
resolution. 0.87.0-rc.4 drops react-native's top-level `types` field and
flips the default `types` export condition to the generated strict API,
neither of which the node10 resolution inherited from
@tsconfig/react-native can see — the package stopped resolving entirely
(TS2688). @tsconfig/react-native is stale at every published version
through 3.0.9, so there is nothing to bump there. Emit is unaffected:
both projects still produce CommonJS. The strict API exports TurboModule
and TurboModuleRegistry, and still references react-native's globals, so
console/require stay typed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test-app: adopt built-in Kotlin on Android, opt out of the AGP 9 DSL

With React Native 0.87 the test app builds against AGP 9.2.1, where
built-in Kotlin is enabled by default. Nothing in the build needs the
Kotlin plugin any more: ReactAndroid dropped it upstream,
react-native-test-app's modules are gated on it, and react-native-node-api
now only applies it when built-in Kotlin is unavailable. So unlike the
React Native app template, we do not set `android.builtInKotlin=false`.

The new DSL is a different matter and stays opted out: both of
react-native-test-app's Gradle modules still use the old one, and that is
third-party code. AGP 10 removes this opt out, so it is tracked in callstackincubator#389
along with the upstream code that has to migrate first.

Also pins the Gradle wrapper at 9.4.1, which react-native-test-app rewrites
it to at run time for React Native >= 0.87 — pinning it ourselves keeps CI
from building with a dirty working tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* deps: bump react-native to a 0.88 nightly and drop the Windows patch

React Native 57706 ("Fix build-from-source on Windows: use JVM temp dir
instead of hardcoded /tmp", 908872a6, 2026-07-28) landed on main after
the 0.87 branch cut, so it ships on the 0.88 line and not in 0.87.0-rc.4.
Verified in the published artifact, not just the tree: the tarball for
0.88.0-nightly-20260809-db662caea carries the fix in settings.gradle.kts,
the exact file (and path) we were patching. Our patch is now redundant.

Dropping it is what makes Android build. Patching a dependency makes pnpm
encode the patch hash into the virtual store directory as
`..._patch_hash=<hash>`, and prefab — which the Android Gradle plugin runs
over react-native's package directory — parses a positional path
containing `=` as an option name and dies with "Error: no such option".
That is google/prefab#187, open since March and
hitting every pnpm user with a patched dependency. With no patched
dependencies there is no `=` in the store, so the bug goes untriggered.

Requires react-native-test-app >= 5.4.8, which widened its peer range to
`0.76 - 0.87 || >=0.88.0-0 <0.88.0` — a prerelease window covering exactly
these nightlies. 5.4.5 did not accept 0.88 at all, so the floor moves up.

Everything the AGP 9 work depends on is unchanged on this line: AGP 9.2.1,
Kotlin 2.2.0, and react-native-test-app still resolves Gradle 9.4.1 for
0.88, matching the pinned wrapper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* host: link the renamed hermesvm prefab module on Android

React Native renamed the prefab module published by `hermes-engine` from
`libhermes` to `hermesvm` between 0.81 and 0.83 — the Android counterpart
of the `hermesvm` framework this branch already links against on Apple
platforms. This CMakeLists has been on `libhermes` since callstackincubator#308, which was
correct while the repo targeted 0.81, and stayed behind when this branch
jumped to 0.87/0.88.

Without it CMake fails to configure:

    Target "node-api-host" links to target "hermes-engine::libhermes" but
    the target was not found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test-app: opt out of built-in Kotlin after all

fa4424b deliberately left `android.builtInKotlin` unset, on the reasoning
that nothing in the build still needs the Kotlin plugin. That reasoning
was wrong, and only a real Android build showed it:

    ComponentActivity.kt:33:9 Unresolved reference 'ComponentActivityDelegate'

react-native-test-app's app module pulls in version-specific sources with
`main.java.srcDirs += [...]` — src/reactactivitydelegate-0.75/java,
src/reactapplication-0.76/java, src/camera/java and others. The Kotlin
plugin compiles the Kotlin in those directories; AGP's built-in Kotlin
only picks up the standard source directories, so every symbol defined in
an added one goes unresolved (`testApp`, `reactHost`, `canUseCamera`,
`ComponentBottomSheetDialogFragment`, …). Their `useBuiltInKotlin` gate
avoids the plugin-conflict failure but does not make the module itself
built-in-Kotlin ready, which is why their template ships this opt out.

react-native-node-api itself stays built-in-Kotlin ready via the
conditional in ee41927 — with this flag set it applies the Kotlin plugin,
and for a consumer on built-in Kotlin it steps aside. This is only about
the test harness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test-app: fail the Android run as soon as the app crashes

`mocha-remote` waits indefinitely for a client to connect and has no
notion of the app dying. When the test app crashed on startup, nothing
ever connected: the run sat idle until the 75 minute step timeout, with
the actual cause — a `FATAL EXCEPTION` one second after `am start` —
only visible by downloading the logcat artifact afterwards.

Add a watchdog that follows `adb logcat -b crash` alongside the app and
exits non-zero when the crash buffer names the test app, printing the
stack trace inline. `concurrently --kill-others-on-fail` then tears down
Metro and the app run, and `mocha-remote` inherits the failing exit code,
so a startup crash fails the job in seconds rather than in an hour.

It deliberately only reacts to crashes — an app that hangs or never
launches still falls back to the job timeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test-app: don't let the crash watchdog hold the step's stderr open

The watchdog correctly failed the run on the first crash it saw, but the
job kept hanging afterwards: `@actions/exec` — how the emulator-runner
action runs each line of the step's script — resolves a command only once
the stdio streams it handed out are closed, and the `adb logcat` child
inherited our stderr. Exiting orphaned it, so that pipe stayed open and
the step waited on a dangling file descriptor long after everything else
had been torn down.

Give the child no stderr of its own and kill it on the way out. Verified
by spawning the watchdog the way `@actions/exec` does: before, the
process exited after 1.6s but its stdio never closed; now both happen
together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* vendor-hermes: advance the pin past Hermes' JSI_UNSTABLE default flip

The Android test app crashed on startup, in `NodeApiHostPackage.<init>`:

    java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol
    "_ZTIN8facebook3jsi10SerializedE" referenced by ".../libhermesvm.so"
    com.facebook.soloader.SoLoaderDSONotFoundError: couldn't find DSO to
    load: libhermesvm.so

That symbol is `typeinfo for facebook::jsi::Serialized`. JSI's
`Serialized` / `ISerialization` APIs sit behind `#ifdef JSI_UNSTABLE`,
and React Native never defines it when building the `libjsi.so` it ships
in the ReactAndroid AAR. Our pinned Hermes still defaulted `JSI_UNSTABLE`
to ON, so `hermesvm` compiled those APIs in and referenced symbols that
nothing in the APK defines.

Apple builds are unaffected because JSI is compiled into the `hermesvm`
framework itself; on Android the two are separate shared libraries, and
RN's hermes-engine build imports `libjsi.so` rather than packaging the
copy Hermes builds for itself.

facebook/hermes 5a795c9f8 ("Fix: JSI_UNSTABLE CMake flag should be OFF by
default") is the immediate child of the previous pin, so this picks up
the one-line fix and nothing else.

Verified by rebuilding the release APK for x86_64: `libhermesvm.so` no
longer references `jsi::Serialized`, and every undefined JSI symbol it
does have is defined by a library shipped in the APK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* host: create one Node-API env per addon

Node creates a fresh napi_env for every addon it loads (see the "Create a
new napi_env for this specific module" branch of
napi_module_register_by_symbol in src/node_api.cc), because the env holds
addon-scoped state: instance data, last error info and the addon's
Node-API version. Sharing one env across all addons breaks that isolation
most visibly for instance data, where the single slot on napi_env__ means
two addons built on Napi::Addon<T> clobber each other — the second
registration finalizes the first addon's object, and Addon::Unwrap then
casts the wrong type.

Move the env onto the addon record and create it during initialization.
hermes_napi_create_env() allocates a fresh env per call and registers its
teardown with the vm::Runtime, so ownership is unchanged: each env is
torn down with the runtime.

The call invoker registry is already keyed by env, so it needs no change
beyond dropping entries when an env goes away — with an env per addon
those would otherwise accumulate across reloads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Add changeset for the static_h Node-API adoption

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: describe the vendored Hermes instead of a patched one

Node-API is implemented in Hermes itself now, so nothing is patched or
forked: we build from a pinned commit on the static_h branch. Also
corrects HOW-IT-WORKS, which described the removed
jsi::Runtime::createNodeApiEnv.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: describe the Node-API host struct in HOW-IT-WORKS

Hermes implements both js_native_api.h and node_api.h; what it can't
supply without libuv are the scheduling primitives, which the host passes
in as a hermes_napi_host struct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…allstackincubator#398)

* Implement hermes_napi_host and pass it to hermes_napi_create_env

Provide the Phase 3 host integration for Hermes' first-party Node-API:

- New HermesNapiHost.{hpp,cpp}: a mirror of the hermes_napi_host struct
  (pinned to HERMES_GIT_SHA) and a HostContext per React Native runtime,
  backed by a process-global 4-thread worker pool (post_work /
  cancel_work) and the runtime's CallInvoker behind a type-erased JS
  dispatcher (post_task and work completions). fatal_exception
  stringifies the error, logs and aborts; uv_loop and
  ref_loop/unref_loop stay null by design. Contexts are retained for the
  process lifetime because the env reads the struct during Runtime
  teardown after env cleanup hooks have run.
- CxxNodeApiHostModule passes the host at env creation - before the
  addon's init runs, fixing init-time async work - and drops
  setCallInvoker.
- Delete the RuntimeNodeApiAsync overrides: async work falls through to
  Hermes' implementation, so execute now runs on a worker thread instead
  of the JS thread, and thread-safe functions work for the first time.
- tests/async: execute/complete thread-identity assertions, a gated
  blocking execute (deadlock-proof that execute is off the JS thread)
  and a deterministic cancel-of-running-work case.
- tests/threadsafe-function: port of Node's test_threadsafe_function
  (pthread shim for uv threads, upstream assertions restored) plus
  JS-thread and never-inline supplements; re-enable the
  async_work_thread_safe_function example (its SIGABRT was the null
  host).
- packages/host/tests: Catch2 suite exercising the worker pool,
  cancellation atomicity, post_task ordering/reentrancy and the teardown
  drop path on plain Linux, with a host-cpp-tests CI job mirroring
  weak-node-api-tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6xHHeMFF853z5R6th5CkF

* Trigger CI for the label-gated device lanes

The Check workflow only reacts to opened/synchronize/reopened, so the
Apple and Android labels added to the PR need a synchronize event to be
seen by the job conditions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6xHHeMFF853z5R6th5CkF

* Trigger CI with the weak-node-api and host labels applied

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6xHHeMFF853z5R6th5CkF

* Address review: truthful teardown outcomes, duplicate-queue drop

- JsDispatcher now reports acceptance and WorkItem holds its HostContext
  strongly: the weak_ptr could never expire (contexts are retained for
  the process lifetime), so the pool's drop branches were dead code and
  napi_cancel_async_work could claim success for a completion the
  dispatcher was about to drop. cancel_work now returns the dispatcher's
  verdict, and workerMain/postTask log drops where they actually happen.
- WorkerPool::enqueue drops a double-queued (loopData, workData) instead
  of enqueueing it: a second entry meant two completions for one
  napi_async_work and a use-after-free once the addon deletes the work
  inside the first. Covered by a new Catch2 test; the saturation helper
  now uses distinct jobs per worker so it does not trip the detection.
- Delete HostContext copy/move: host_.data points at this.
- Justify the CallInvoker-liveness assumption at the dispatcher site
  (RuntimeSchedulerCallInvoker holds a weak RuntimeScheduler owned
  together with the runtime, so accepted work cannot outlive it) and
  correct the WorkItem comment: the (loopData, workData) pair separates
  runtimes/reloads, not envs.
- Rework the Catch2 teardown test to model an expired CallInvoker (the
  state production reaches) instead of dropping the last context ref
  (which it never does), and cover the rejected post_task path.
- Scope the 30s mocha timeout to the threadsafe-function suite so a
  genuine deadlock elsewhere still fails fast; add a TODO on
  fatal_exception about routing through RN error handling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6xHHeMFF853z5R6th5CkF

---------

Co-authored-by: Claude <noreply@anthropic.com>
…tackincubator#408)

* ci: bring host-cpp-tests in line with the rest of the workflow

host-cpp-tests was added on next (callstackincubator#398), so the Node.js 20 deprecation
sweep on main (callstackincubator#405, callstackincubator#407) never reached it: it is the one job in the
workflow still on actions/checkout@v4, pnpm/action-setup@v4 and
actions/setup-node@v6, and so the only remaining source of the runner's
"targets Node.js 20" warning.

Merges main to pick up callstackincubator#407 and gives the job the same arrangement as
its siblings: Node.js set up before pnpm, pnpm/action-setup v6 owning
the store cache, and ccache-action pinned to an exact patch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W14eEfXdK5DYzv43MazryE

* ci: re-run checks for the newly applied labels

check.yml only runs on opened/synchronize/reopened, so the host-gated
job this change is about does not start from labelling alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W14eEfXdK5DYzv43MazryE

---------

Co-authored-by: Claude <noreply@anthropic.com>
)

The Android setup has two separate requirements — building React Native from
source and pointing it at the vendored Hermes — and the doc ran them together
without saying why either is needed.

- Split them into their own sections and say up front that this is the manual
  equivalent of what `pod install` does on Apple platforms.
- Note that apps based on react-native-test-app get the dependency
  substitutions from `react.buildFromSource=true` instead of editing
  settings.gradle themselves, as apps/test-app does.
- Spell out that REACT_NATIVE_OVERRIDE_HERMES_DIR is read from the environment
  (Gradle cannot set it for its own build), so it has to be exported for every
  shell — or for the environment Android Studio is launched from — and what
  goes wrong without it.

Also drop the last two references to a "patched" Hermes from the host README,
left over from before we adopted Hermes' first-party Node-API.


Claude-Session: https://claude.ai/code/session_01HX4imsygeawVtsmoP1sj3F

Co-authored-by: Claude <noreply@anthropic.com>
…bator#410)

The spinners were passed `isEnabled: !silent`. A disabled ora spinner still
writes `- <text>` on start and the success/fail symbol on completion (to
stderr) — it only skips the animation. `isSilent` is the option that
suppresses output entirely.

Callers capture stdout only (`$(... --silent)` in CI and the Gradle error
message, backticks in patch-hermes.rb), so the stray output was noise rather
than a broken path, but `--silent` now does what it says.


Claude-Session: https://claude.ai/code/session_01HX4imsygeawVtsmoP1sj3F

Co-authored-by: Claude <noreply@anthropic.com>
…ckincubator#409)

* ci: verify the ferric Apple binaries depend on weak-node-api

Extends the "Test ferric Apple triplets" job so it doesn't only assert
which architectures were produced, but also that each produced binary
actually links the weak-node-api framework, catching regressions where a
triplet builds but drops the dependency.

The expected number of `@rpath/weak-node-api.framework/weak-node-api`
lines is derived from the otool output itself — `otool -L` prints one
header per file, or one per architecture for fat files — rather than
hard-coded, so it doesn't rot when a triplet is added or dropped.

Also renames lipo-info.txt to lipo-output.txt for symmetry with the new
otool-output.txt, and uploads both as artifacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SrPdjhQ6aG949mVDDaiT2U

* ci: accept the versioned weak-node-api install name on macOS

The first CI run of this check reported 8 dependencies across 10 binary
slices. The two misses were the macOS slices: macOS frameworks use the
versioned bundle layout, so weak-node-api's install name there is

  @rpath/weak-node-api.framework/Versions/0.1.1/weak-node-api

where iOS, tvOS and visionOS get the flat

  @rpath/weak-node-api.framework/weak-node-api

Both are a genuine dependency on the framework, so match the optional
"Versions/<version>/" component rather than only the flat form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SrPdjhQ6aG949mVDDaiT2U

---------

Co-authored-by: Claude <noreply@anthropic.com>
…ator#429)

- HOW-IT-WORKS.md: replace the three TODO comments near the top with
  a real, runnable example of calculator-lib's native C addon (mirrors
  docs/USAGE.md) plus the JS that requires and calls it, and clone
  instructions for readers who want to follow along with the source
  referenced later in the document.
- CLI.md: hand-write documentation for all five react-native-node-api
  CLI commands (vendor-hermes, link, list, info, patch-xcode-project),
  their options and the shared library-naming strategies, sourced from
  packages/host/src/node/cli/program.ts, hermes.ts and options.ts, with
  a note to keep it in sync with those definitions.

Fixes callstackincubator#425


Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm

Co-authored-by: Claude <noreply@anthropic.com>
* ci: run linting without building native code (callstackincubator#414)

The lint job set up a full native toolchain (JDK 17, Android SDK + NDK,
x86_64-linux-android Rust target) and ran two native bootstraps purely to
get generated TypeScript types for type-checking. Resolve both TODOs:

- Add `ferric build --dts-only`, which generates a crate's `.d.ts` and JS
  entrypoint via a plain host `cargo build` (napi-rs typedef codegen),
  without cross-compiling any Android/Apple binaries. The library basename
  is derived from `cargo metadata`'s cdylib target instead of from built
  artifact paths, so no platform build is needed to compute it. Wire this
  up as `ferric-example`'s new `build:types` script.
- Use `weak-node-api`'s existing `prebuild:prepare` script (header copy +
  C++/TS declaration codegen) instead of `bootstrap` (which also runs the
  native CMake build). It already required nothing beyond clang-format.

With both native builds no longer needed for typing, the lint job drops
the JDK 17, Android SDK, and `rustup target add` steps entirely.

Verified locally (Node 24, cargo present, no Android/Apple SDK): fresh
`pnpm install && pnpm run build`, then `pnpm --filter weak-node-api run
prebuild:prepare`, `pnpm --filter @react-native-node-api/ferric-example
run build:types`, `pnpm run lint`, `pnpm run prettier:check`, `pnpm run
depcheck` and `pnpm run publint` all pass end-to-end with no native
toolchain present, reproducing the new lint job's steps.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm

* ci: commit ferric-example's declarations as a fixture instead of building them

kraenhansen suspected generateTypeScriptDeclarations doesn't actually skip a
native build. Confirmed: napi-rs's `napi build` has no typegen-only mode — it
always runs a real `cargo build`, and --dts-only leaves a fully populated
~123MB target/ directory (including a compiled libferric_example.so) behind.
"Skipping the native build entirely" was wrong; only Android/Apple
cross-compilation was actually skipped, and the lint job stayed coupled to
the host Rust toolchain's health exactly as callstackincubator#414 wanted to avoid.

Switch to the issue's other suggested option: commit ferric_example.d.ts and
ferric_example.js as a checked-in fixture (no longer gitignored), and drop
the ferric-example build:types step from the lint job entirely — it no
longer needs to regenerate anything. --dts-only stays, now documented
accurately, as the way to regenerate the fixture by hand after changing
packages/ferric-example/src/lib.rs.

To catch drift, the two CI jobs that already do a real `ferric build`
(Android and Apple triplets) now `git diff --exit-code` the two committed
files right after building. Both are label-gated rather than running on
every PR, so this doesn't fully close the gap — flagged in the PR thread.

Also excludes the two fixture files from Prettier: they're left in napi-rs's
own output formatting so regenerating them reproduces the committed bytes
exactly, and the new drift check doesn't false-positive on formatting alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm

* ci: drop the explanatory comment from ferric-example/.gitignore

Per review feedback on callstackincubator#435.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm

---------

Co-authored-by: Claude <noreply@anthropic.com>
…ncubator#433)

`ANDROID_STL` was hardcoded to `c++_shared` when configuring Android
builds, with no escape hatch for an addon that needs `c++_static` or must
match a prebuilt third-party dependency's STL (callstackincubator#418).

The generic `-D`/`--define` cache-variable pass-through (added for callstackincubator#332,
which callstackincubator#227 also asks for) already lets a consumer set arbitrary CMake
cache variables, including `ANDROID_STL` - but it didn't actually work:
our hardcoded Android defaults were appended to the CMake command line
*after* the user-provided `-D` arguments, and CMake resolves a variable
set multiple times via `-D` to its last occurrence, so the hardcoded
value always won.

Fix the ordering so the user's `--define` is applied last. `ANDROID_STL`
still defaults to `c++_shared`, matching what React Native itself uses.

Extract the CMake definitions building into an exported
`buildCommonDefinitions` and add unit tests covering the default and the
override precedence.


Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm

Co-authored-by: Claude <noreply@anthropic.com>
…tackincubator#432)

HostContext::fatalException previously stringified the error and called
abort() unconditionally, matching Hermes' null-host default but giving
node-addon-api's tsfn error path (which calls napi_fatal_exception whenever
an exception escapes a thread-safe-function callback) no chance of being
observed or handled — a single throwing tsfn callback hard-killed the app
with no LogBox and no JS-side handler getting a say.

napi_fatal_exception (unlike the noreturn napi_fatal_error) is a plain
napi_status-returning function, and the pinned Hermes commit's
hermes_napi_error.cpp explicitly supports the host hook returning normally,
so routing can be done synchronously against the passed env with plain
Node-API calls, keeping HermesNapiHost.cpp free of React Native/JSI
includes:

- Attempt global.ErrorUtils.reportFatalError(err) via napi_get_global +
  napi_get_named_property (x2, type-checked at each step) +
  napi_call_function.
- On success, return normally (napi_ok reaches the addon), matching Node's
  process.emit('uncaughtException') returning to the caller when a handler
  is installed.
- On any failure (ErrorUtils/reportFatalError absent or not the right
  type, or the call itself throwing) fall back to the previous stringify +
  log_error + abort() path, clearing any pending exception first so the
  fallback's own Node-API calls aren't defeated by a stale exception.
- Guard reentrancy with a HostContext member flag: if the ErrorUtils
  handler itself triggers another napi_fatal_exception, the nested call
  skips straight to the fallback instead of recursing.

Adds a "minor" changeset for react-native-node-api: this is an observable
behavior change for addons/apps that relied on the previous immediate
abort.

Not compiled or exercised on-device in this environment (no Android/iOS
toolchain here) — see the PR description for what remains to be verified.

Closes callstackincubator#402


Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm

Co-authored-by: Claude <noreply@anthropic.com>
…ns (callstackincubator#434)

* Drop the host's shadowing implementations of runtime Node-API functions

Hermes' first-party Node-API (adopted in callstackincubator#372, integrated via
hermes_napi_host in callstackincubator#398) already implements the buffer functions,
napi_get_version and napi_get_node_version. RuntimeNodeApi.{cpp,hpp} still
defined all of these, and since the generated injector
(scripts/generate-injector.mts) resolves each NodeApiHost field by
unqualified name inside `namespace callstack::react_native_node_api`, the
host's shims won and Hermes' implementations were never reached.

Remove napi_create_buffer, napi_create_buffer_copy,
napi_create_external_buffer, napi_get_buffer_info, napi_is_buffer,
napi_get_version and napi_get_node_version from RuntimeNodeApi.{cpp,hpp},
letting unqualified lookup fall through to Hermes' own symbols. This also
fixes two bugs the shims carried:

- napi_get_buffer_info wrote its typed-array-kind output into a mutable
  global (`ArrayType`) that every subsequent napi_create_buffer /
  napi_create_external_buffer call read back, so calling it on e.g. a
  Float64Array corrupted every later buffer creation (and raced across
  runtimes).
- napi_create_buffer_copy accepted `result_data` but never wrote it.

napi_is_buffer / napi_get_buffer_info also become stricter, matching Node:
true/napi_ok only for Uint8Array, napi_invalid_arg otherwise, instead of
accepting any ArrayBuffer/TypedArray.

Keep napi_fatal_error's host-side implementation: Hermes routes it to
stderr, which is not logcat on Android, while the host's version reaches
logcat via the "NodeApiHost" logger tag. Documented why this one
intentionally keeps shadowing Hermes so a future sweep doesn't remove it
as dead weight.

RuntimeNodeApi.{cpp,hpp} keep their own translation unit rather than
folding into Logger-adjacent code: the file now holds exactly the one
shim the host deliberately keeps, and renaming would touch the injector,
CMakeLists and podspec globbing for no functional benefit.

Adds a changeset (patch) for the observable behavior change: addons now
see Hermes' real napi_get_node_version instead of napi_generic_failure,
and the stricter buffer type-checking. Closes callstackincubator#67.

Verified: pnpm install && pnpm run build, pnpm --filter react-native-node-api
run test (pre-existing failures only, confirmed present on unmodified
origin/next too - they stem from running as root, not this change),
eslint and prettier on touched files. Native C++ compilation was not
verified - no Android/iOS toolchain is available in this environment.

Closes callstackincubator#428

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm

* ci: fix host-cpp-tests label check to match the actual "Host 🏡" label

The job checked for a label literally named "host", but the repository's
real label (used on issues, e.g. callstackincubator#428/callstackincubator#420/callstackincubator#412) is "Host 🏡" — a label
named plain "host" existed too, seemingly a leftover/duplicate, and has
since been deleted. The condition never actually matched the label anyone
would apply in practice, so this job only ever ran on pushes to main/next,
never on a labeled PR.

Found while attaching labels to this PR: the CI still showed green with
host-cpp-tests silently not running, exactly the kind of gap
.claude/CLAUDE.md (callstackincubator#436) exists to prevent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm

---------

Co-authored-by: Claude <noreply@anthropic.com>
…llstackincubator#430)

Add a --code-signing-allowed flag to the Apple platform of cmake-rn.
CODE_SIGNING_ALLOWED=NO remains the default (needed for the
free-standing dynamic libraries we produce), but a consumer who needs
signed binaries in the XCFramework - enterprise distribution, or a
target whose downstream tooling verifies signatures - can now opt in.

Addresses the Apple half of callstackincubator#418.


Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm

Co-authored-by: Claude <noreply@anthropic.com>
…kincubator#431)

* fix(host): compile out log_debug in release (NDEBUG) builds

`log_debug`'s per-addon diagnostic chatter (library found/loaded, symbol
resolution, ...) was firing unconditionally on every addon load, including in
shipped release builds - unwanted logcat/os_log output plus string-formatting
cost on a startup path.

log_debug is now an inline no-op declared in Logger.hpp when NDEBUG is
defined (set by CMake's Release/MinSizeRel/RelWithDebInfo configurations, and
by Xcode's Release configuration by default), mirroring React Native's own
dev/release logging split. Logger.cpp's real definition is compiled only
outside of NDEBUG. log_warning/log_error are untouched and keep firing in
every build type, including RelWithDebInfo.

This is compile-time only, not the "compile-time default with runtime
override" the issue floats as the ideal: there's no existing runtime config
plumbing (env var, JS-settable flag, ...) in this codebase to hook an
override into, so adding one would mean inventing new plumbing rather than
reusing something established. Shipping the safer compile-time-only guard now
per the issue's own fallback.

Closes callstackincubator#420

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaK9eAAF5G8wj6UT8VekAm

* docs: keep inline comments brief, trim the Logger ones

Adds a `.claude/CLAUDE.md` section: default to no inline comment, and keep
the ones that survive to a line or two. Rationale, rejected alternatives and
change narration go in the PR description, which is where a `git blame` leads
anyway and which does not go stale as the surrounding code moves.

Applies it to the log_debug/NDEBUG comments this PR added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmbKZsRagnasxGVXtnCLoF

---------

Co-authored-by: Claude <noreply@anthropic.com>
…lstackincubator#438)

* chore: upgrade bufout to v1.0.0 and drop defaultMaxListeners bumps

bufout v1.0.0 keeps the number of listeners on the process and on the
output streams constant regardless of how many children are spawned
concurrently: a single shared exit/SIGINT listener is attached only while
children are running, and every child pipes into one shared pass-through
per destination stream.

That removes the reason the CLIs raised EventEmitter.defaultMaxListeners
to 100, so those assignments (and the now-unused node:events /
node:stream imports) are gone and Node's default limit applies again,
restoring the leak warning it exists to give.

Verified with 80 concurrent children in both "inherit" and "buffered"
mode, plus the SpawnFailure flush path, at the default limit of 10: no
MaxListenersExceededWarning, and process listener counts return to zero.

The public API is unchanged from 0.3.x — the major bump reflects the
1.0.0 milestone, not a breaking change to spawn/SpawnFailure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAjP89a9VA9EtQsVBxGcto

* ci: trigger label-gated jobs

The Check workflow only runs on opened/synchronize/reopened, so the
Apple/Android/Ferric jobs gated on labels never evaluated the labels
added after the pull request was opened. This empty commit fires a
synchronize event so they run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAjP89a9VA9EtQsVBxGcto

---------

Co-authored-by: Claude <noreply@anthropic.com>
* Add --namespaced-targets to gyp-to-cmake

* Use namespaced targets in node-addon-examples

* Support building multiple addons

* Limit spawn concurrency

* Emit prebuilds per target, next to their sources

A project declaring multiple addons wrote every prebuild into a single
output directory, named after the CMake target. Both the location and the
name are now derived per target from the CMake File API:

- The output directory defaults to {targetSourceDir}/build/{configuration},
  where the new {targetSourceDir} placeholder expands to the target's own
  source directory. A single-addon project reports "." and so resolves to
  the same path as before.
- The prebuild is named after the artifact on disk (the target's
  OUTPUT_NAME) rather than the target name, so a target renamed to avoid a
  clash within the project still produces the name the JS require expects.

Together this keeps a prebuild where the Babel plugin and auto-linking
resolve it from, and reduces --namespaced-targets to an internal concern.

Also fixes, in the same area:

- gyp-to-cmake emitted OUTPUT_NAME regardless of --namespaced-targets, due
  to an always-truthy condition, and never emitted it for Apple framework
  targets, which CMake names after it.
- The Apple build ran a full "cmake --build" once per shared library,
  concurrently against one build tree, and called "xcodebuild -list" (a
  synchronous spawn) once per library per triplet.
- xcodebuild invocations now run in sequence per build directory, as
  concurrent invocations against a single Xcode project are not reliable.
- postBuild looked for "<target name>.framework" while createAppleFramework
  names it after the artifact, so the two diverged under namespacing.
- --concurrency accepted any value, and did not implement the documented
  fallback to 1 under --verbose. Max listeners is now derived from it.
- verify-prebuilds globbed a directory the prebuilds had moved out of, so
  it passed by finding nothing. It now covers tests/ too and requires a
  non-zero count.
- The root example project globbed recursively, which both missed examples
  copied in after configure and would add a nested project twice. It is
  now generated from the same script pipeline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfKQDvEkxNtkSE4aaF9yG8

---------

Co-authored-by: Claude <noreply@anthropic.com>
…ve (callstackincubator#440)

* Add a prebuilt-hermes command and a workflow that publishes its archive

Building Hermes for Apple platforms is the expensive part of an iOS build,
and it only changes when the pinned commit does. Build it once into an
archive in the destroot layout hermes-engine.podspec expects from
HERMES_ENGINE_TARBALL_PATH, and publish it as a release asset keyed by the
pinned commit.

Nothing consumes the archive yet — pod install still builds Hermes from
source. This lands the command and the publishing workflow first, so the
workflow is dispatchable and an asset exists before anything depends on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH

* 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ubator#442)

* Configure the host Hermes compiler for one architecture

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH

* 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…kincubator#443)

* Dump the CMake configure log when the Hermes build fails

CMake reports a failed feature check as a bare "not found", with the
compiler's actual complaint only in its configure log. The host hermesc
configure is failing on the macOS runner with checks that succeed
everywhere else, so surface that log rather than guess at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH

* Stop the host Hermes compiler build from targeting visionOS

Every command got all three deployment targets build-apple-framework.sh
can ask for, including the host compiler build. XROS_DEPLOYMENT_TARGET is
also a clang driver variable, so clang targeted visionOS against the macOS
sysroot:

  clang: warning: using sysroot for 'MacOSX' but targeting 'XR'
  error: 'pthread_mutexattr_init' is unavailable: not available on visionOS

Every API marked unavailable on visionOS then failed to compile, which is
why unistd.h (which reaches _fd_def.h through sys/select.h) came back "not
found" while sys/stat.h did not, and why the configure died on CheckAtomic.

Each platform build now gets only the deployment target it needs, and the
host compiler build gets none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…kincubator#444)

The paths filter fires on any edit to hermes.ts, hermes-prebuilt.ts or
this workflow, not just a bumped pin — and `--no-download` meant the run
then rebuilt for half an hour and re-uploaded 118 MB identical to what was
already on the release. Merging callstackincubator#443 did exactly that.

The Actions cache does not cover this: it is scoped to the branch that
wrote it, so a build on a feature branch leaves nothing behind for `next`,
and it evicts after 7 days idle or under the repository's 10 GB cap, which
several multi-gigabyte ccache entries already compete for.

The archive name covers every input that changes its contents, so an asset
already published under that name is what the run would rebuild. Look it
up and skip the build and the upload, with a `force` dispatch input for
deliberate rebuilds.


Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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 callstackincubator#392.


Claude-Session: https://claude.ai/code/session_01UkNbgdyuKgHaFwT27RahGH

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ator#445)

* Load addons through Hermes' hermes_napi_load_module

The vendored Hermes ships a first-party addon loader that does what
CxxNodeApiHostModule did by hand — dlopen, resolve the init function,
create the exports object and call it — plus the deprecated
napi_module_register fallback the host never implemented.

Hand the platform specific path to it instead, and drop AddonLoaders.hpp
along with the host's own loading and initialization code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ugFE6vmMUVMTuoupvhMhX

* Trigger the label-gated CI jobs

The check workflow only re-evaluates its label conditions on opened,
synchronize and reopened events, so the labels added after opening this
PR need a push to take effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ugFE6vmMUVMTuoupvhMhX

---------

Co-authored-by: Claude <noreply@anthropic.com>
…allstackincubator#446)

* Add a fixture registering via the deprecated napi_module_register

The host gained support for addons that register themselves by calling
napi_module_register while their library loads (callstackincubator#445), but nothing in the
repo exercises that path — every other addon here exports
napi_register_module_v1, which the loader finds first.

This addon exports no such symbol, so it only loads if the fallback works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ugFE6vmMUVMTuoupvhMhX

* Trigger the label-gated CI jobs

The check workflow only re-evaluates its label conditions on opened,
synchronize and reopened events, so the labels added after opening this
PR need a push to take effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ugFE6vmMUVMTuoupvhMhX

* Drop the gyp file from the module-register fixture

Nothing builds this from binding.gyp — cmake-rn drives the CMake project
directly. The sibling fixtures keep theirs to stay close to upstream
sources they were derived from, which does not apply to an addon written
here.

CMakeLists.txt is now hand-maintained rather than regenerated by
gyp-to-cmake, which skips the directory now that there is no binding.gyp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ugFE6vmMUVMTuoupvhMhX

* Require the addon directly instead of through bindings

The bindings package earns its place when an addon has to be found across
the several output directories node-gyp might have used. This addon is
built by cmake-rn to one known location, so a plain require says the same
thing with one less dependency — and it exercises the Babel plugin's
ordinary require path rather than its bindings special case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ugFE6vmMUVMTuoupvhMhX

---------

Co-authored-by: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve gyp-to-cmake to handle "cflags"

2 participants