Skip to content
Open
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
12 changes: 12 additions & 0 deletions .changeset/node-api-linkage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"cmake-rn": major
"react-native-node-api": minor
"weak-node-api": minor
"node-api-stub": minor
---

Add `--node-api-linkage <weak-node-api|hermes|none>` to `cmake-rn`, replacing `--no-weak-node-api-linkage` (use `--node-api-linkage none`).

Addons linking the new `node-api` CMake target (`find_package(node-api REQUIRED CONFIG)`) link whatever the mode selects. With `hermes` they link a link-time stub of `libhermesvm`, built along with the addon, so they record `hermesvm` and call Hermes directly instead of going through `weak-node-api`, which doesn't need to be built. The stub never ships in an app. Addons naming the `weak-node-api` target directly keep building alongside. The default stays `weak-node-api`.

The new `node-api-stub` package holds the stub and `getNodeApiFunctions()`, which moved there from `weak-node-api`.
23 changes: 23 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,13 @@ jobs:
- name: Build test app
run: xcodebuild archive -workspace ReactTestApp.xcworkspace -configuration Release -scheme ReactTestApp -destination "generic/platform=iOS Simulator" -archivePath ./build/test-app.xcarchive CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=YES C_COMPILER_LAUNCHER=ccache CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER=YES
working-directory: apps/test-app/ios
# The async test addon is built with `--node-api-linkage hermes` (#494).
- name: Verify the async addon links Hermes directly
run: |
node --run verify:hermes-linkage -- \
tests/async/build/RelWithDebInfo/addon.apple.node/ios-*-simulator/addon.framework/addon \
../../apps/test-app/ios/build/test-app.xcarchive/Products/Applications/ReactTestApp.app/Frameworks/hermesvm.framework/hermesvm
working-directory: packages/node-addon-examples
- name: Run test app
run: |
# Install the app
Expand Down Expand Up @@ -467,6 +474,22 @@ jobs:
sleep 5
# Stop logcat
kill $LOGCAT_PID || true
# The async test addon is built with `--node-api-linkage hermes` (#494).
- name: Verify the async addon links Hermes directly
run: |
RNTA_DIR=$(node -p "path.dirname(require.resolve('react-native-test-app/package.json', { paths: ['../../apps/test-app'] }))")
APK=$(find ../../apps/test-app/android "$RNTA_DIR/android" -name '*.apk' 2>/dev/null | head -n 1)
if [ -z "$APK" ]; then
echo "::error::Found no APK of the test app"
exit 1
fi
echo "Extracting Hermes from $APK"
unzip -o -j "$APK" lib/x86_64/libhermesvm.so -d "$RUNNER_TEMP/apk"
export LLVM_BIN="$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/linux-x86_64/bin"
node --run verify:hermes-linkage -- \
tests/async/build/RelWithDebInfo/addon.android.node/x86_64/libaddon.so \
"$RUNNER_TEMP/apk/libhermesvm.so"
working-directory: packages/node-addon-examples
- name: Upload device logs
if: always()
uses: actions/upload-artifact@v7
Expand Down
17 changes: 17 additions & 0 deletions packages/cmake-rn/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,21 @@ else()
endif()
```

### Choosing what to link with `--node-api-linkage`

An addon can instead link the `node-api` target, which links whatever `--node-api-linkage <mode>` selects:

- `weak-node-api` (the default): the `weak-node-api` library described above.
- `hermes`: a link-time stub of Hermes' `hermesvm` library from [`node-api-stub`](../node-api-stub), built along with the addon, so the addon records `hermesvm` and calls Hermes' Node-API directly. The stub is never shipped: the app's own Hermes provides the implementation at runtime. This doesn't need `weak-node-api` to be built, but requires a Hermes exporting Node-API (such as the one vendored by `react-native-node-api`), and the addon gets Hermes' own `napi_fatal_error` instead of the host's.
- `none`: neither target is defined.

```cmake
# Defines the "node-api" target
find_package(node-api REQUIRED CONFIG)

target_link_libraries(addon PRIVATE node-api)
```

Addons linking the `weak-node-api` target directly can be built alongside: it's defined in the `weak-node-api` mode, and in the `hermes` mode whenever `weak-node-api` is built.

This is different from how `cmake-js` "injects" the Node-API for linking (via `${CMAKE_JS_INC}`, `${CMAKE_JS_SRC}` and `${CMAKE_JS_LIB}`). To allow for interoperability between these tools, we inject these when you pass `--cmake-js` to `cmake-rn`.
32 changes: 32 additions & 0 deletions packages/cmake-rn/cmake/node-api-config.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Defines the `node-api` target, linking what cmake-rn's --node-api-linkage
# selected (NODE_API_LINKAGE).

if(TARGET node-api)
return()
endif()

foreach(variable NODE_API_LINKAGE NODE_API_INC)
if(NOT DEFINED ${variable})
message(FATAL_ERROR "${variable} is not set: build with cmake-rn and a --node-api-linkage other than 'none'")
endif()
endforeach()

if(NODE_API_LINKAGE STREQUAL "hermes")
if(NOT TARGET node-api-stub)
add_subdirectory("${NODE_API_STUB_DIR}" "${CMAKE_BINARY_DIR}/node-api-stub")
endif()
add_library(node-api INTERFACE)
target_link_libraries(node-api INTERFACE node-api-stub)
target_include_directories(node-api INTERFACE ${NODE_API_INC})
message(STATUS "Linking Node-API against a stub of Hermes")
else()
if(NOT DEFINED NODE_API_LIB)
message(FATAL_ERROR "NODE_API_LIB is not set")
endif()
add_library(node-api SHARED IMPORTED)
set_target_properties(node-api PROPERTIES
IMPORTED_LOCATION "${NODE_API_LIB}"
INTERFACE_INCLUDE_DIRECTORIES "${NODE_API_INC}"
)
message(STATUS "Linking Node-API against ${NODE_API_LINKAGE}: ${NODE_API_LIB}")
endif()
6 changes: 4 additions & 2 deletions packages/cmake-rn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"dist",
"!dist/**/*.test.d.ts",
"!dist/**/*.test.d.ts.map",
"templates"
"templates",
"cmake"
],
"scripts": {
"build": "tsc",
Expand All @@ -29,7 +30,8 @@
"cmake-file-api": "workspace:*",
"react-native-node-api": "workspace:*",
"zod": "^4.1.11",
"weak-node-api": "workspace:*"
"weak-node-api": "workspace:*",
"node-api-stub": "workspace:*"
},
"peerDependencies": {
"node-addon-api": "^8.3.1",
Expand Down
8 changes: 2 additions & 6 deletions packages/cmake-rn/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
pLimit,
InvalidArgumentError,
} from "@react-native-node-api/cli-utils";
import { nodeApiLinkageOption } from "react-native-node-api";

import {
platforms,
Expand Down Expand Up @@ -115,11 +116,6 @@ const noAutoLinkOption = new Option(
"Don't mark the output as auto-linkable by react-native-node-api",
);

const noWeakNodeApiLinkageOption = new Option(
"--no-weak-node-api-linkage",
"Don't pass the path of the weak-node-api library from react-native-node-api",
);

const cmakeJsOption = new Option(
"--cmake-js",
"Define CMAKE_JS_* variables used for compatibility with cmake-js",
Expand Down Expand Up @@ -159,7 +155,7 @@ let buildCommand = new Command("build")
.addOption(targetOption)
.addOption(stripOption)
.addOption(noAutoLinkOption)
.addOption(noWeakNodeApiLinkageOption)
.addOption(nodeApiLinkageOption)
.addOption(cmakeJsOption)
.addOption(ccachePathOption)
.addOption(concurrencyOption);
Expand Down
8 changes: 5 additions & 3 deletions packages/cmake-rn/src/platforms/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import {
AndroidTriplet as Triplet,
} from "react-native-node-api";
import * as cmakeFileApi from "cmake-file-api";
import { NODE_API_STUB_TARGET } from "node-api-stub";

import type { BaseOpts, Platform } from "./types.js";
import { getArtifactName, toDefineArguments } from "../helpers.js";
import {
getCmakeJSVariables,
getWeakNodeApiVariables,
getNodeApiLinkageVariables,
} from "../weak-node-api.js";
import { isNinjaAvailable } from "../ninja.js";

Expand Down Expand Up @@ -183,7 +184,7 @@ export const platform: Platform<Triplet[], AndroidOpts> = {
source,
define,
build,
weakNodeApiLinkage,
nodeApiLinkage,
cmakeJs,
ccachePath,
},
Expand Down Expand Up @@ -228,7 +229,7 @@ export const platform: Platform<Triplet[], AndroidOpts> = {
"--toolchain",
toolchainPath,
...toDefineArguments([
...(weakNodeApiLinkage ? [getWeakNodeApiVariables(triplet)] : []),
getNodeApiLinkageVariables(nodeApiLinkage, triplet),
...(cmakeJs ? [getCmakeJSVariables(triplet)] : []),
...commonDefinitions,
{
Expand Down Expand Up @@ -280,6 +281,7 @@ export const platform: Platform<Triplet[], AndroidOpts> = {
const sharedLibraries = targets.filter(
({ type, name }) =>
type === "SHARED_LIBRARY" &&
name !== NODE_API_STUB_TARGET &&
(target.length === 0 || target.includes(name)),
);
await Promise.all(
Expand Down
8 changes: 5 additions & 3 deletions packages/cmake-rn/src/platforms/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import {

import type { Platform } from "./types.js";
import * as cmakeFileApi from "cmake-file-api";
import { NODE_API_STUB_TARGET } from "node-api-stub";
import { getArtifactName, toDefineArguments } from "../helpers.js";
import {
getCmakeJSVariables,
getWeakNodeApiVariables,
getNodeApiLinkageVariables,
} from "../weak-node-api.js";

import * as z from "zod";
Expand Down Expand Up @@ -189,6 +190,7 @@ async function readCmakeSharedLibraryTargets(
return targets.filter(
({ type, name }) =>
type === "SHARED_LIBRARY" &&
name !== NODE_API_STUB_TARGET &&
(target.length === 0 || target.includes(name)),
);
}
Expand Down Expand Up @@ -292,7 +294,7 @@ export const platform: Platform<Triplet[], AppleOpts> = {
},
async configure(
triplets,
{ source, build, define, weakNodeApiLinkage, cmakeJs, ccachePath },
{ source, build, define, nodeApiLinkage, cmakeJs, ccachePath },
) {
// When using ccache, we're creating symlinks for the clang and clang++ binaries to the ccache binary
// This is needed for ccache to understand it's being invoked as clang and clang++ respectively.
Expand Down Expand Up @@ -343,7 +345,7 @@ export const platform: Platform<Triplet[], AppleOpts> = {
"Xcode",
...toDefineArguments([
...define,
weakNodeApiLinkage ? getWeakNodeApiVariables("apple") : {},
getNodeApiLinkageVariables(nodeApiLinkage, "apple"),
cmakeJs ? getCmakeJSVariables("apple") : {},
compilerDefinitions,
{
Expand Down
57 changes: 47 additions & 10 deletions packages/cmake-rn/src/weak-node-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from "node:path";
import {
isAndroidTriplet,
isAppleTriplet,
type NodeApiLinkage,
SupportedTriplet,
} from "react-native-node-api";

Expand All @@ -13,6 +14,7 @@ import {
androidPrebuildPath,
weakNodeApiCmakePath,
} from "weak-node-api";
import { nodeApiStubPath } from "node-api-stub";

import { ANDROID_ARCHITECTURES } from "./platforms/android.js";
import { getNodeAddonHeadersPath, getNodeApiHeadersPath } from "./headers.js";
Expand All @@ -21,28 +23,30 @@ export function toCmakePath(input: string) {
return input.split(path.win32.sep).join(path.posix.sep);
}

export function getWeakNodeApiPath(
triplet: SupportedTriplet | "apple",
): string {
const NODE_API_CONFIG_DIR = path.resolve(import.meta.dirname, "../cmake");

function findWeakNodeApiPath(triplet: SupportedTriplet | "apple") {
if (triplet === "apple" || isAppleTriplet(triplet)) {
assert(
fs.existsSync(applePrebuildPath),
`Expected an XCFramework at ${applePrebuildPath}`,
);
return applePrebuildPath;
} else if (isAndroidTriplet(triplet)) {
const libraryPath = path.join(
return path.join(
androidPrebuildPath,
ANDROID_ARCHITECTURES[triplet],
"libweak-node-api.so",
);
assert(fs.existsSync(libraryPath), `Expected library at ${libraryPath}`);
return libraryPath;
} else {
throw new Error(`Unexpected triplet: ${triplet as string}`);
}
}

export function getWeakNodeApiPath(
triplet: SupportedTriplet | "apple",
): string {
const result = findWeakNodeApiPath(triplet);
assert(fs.existsSync(result), `Expected weak-node-api at ${result}`);
return result;
}

function getNodeApiIncludePaths() {
const includePaths = [getNodeApiHeadersPath(), getNodeAddonHeadersPath()];
for (const includePath of includePaths) {
Expand All @@ -67,6 +71,39 @@ export function getWeakNodeApiVariables(
};
}

/**
* Variables for the selected --node-api-linkage, which the `node-api` target follows.
*/
export function getNodeApiLinkageVariables(
linkage: NodeApiLinkage,
triplet: SupportedTriplet | "apple",
): Record<string, string> {
if (linkage === "none") {
return {};
}
const nodeApiVariables = {
// Enable use of `find_package(node-api REQUIRED CONFIG)`
"node-api_DIR": NODE_API_CONFIG_DIR,
NODE_API_LINKAGE: linkage,
NODE_API_INC: getNodeApiIncludePaths().join(";"),
};
if (linkage === "hermes") {
return {
// Keeps the `weak-node-api` target available to addons naming it, without requiring it
...(fs.existsSync(findWeakNodeApiPath(triplet))
? getWeakNodeApiVariables(triplet)
: {}),
...nodeApiVariables,
NODE_API_STUB_DIR: nodeApiStubPath,
};
}
return {
...getWeakNodeApiVariables(triplet),
...nodeApiVariables,
NODE_API_LIB: getWeakNodeApiPath(triplet),
};
}

/**
* For compatibility with cmake-js
*/
Expand Down
7 changes: 6 additions & 1 deletion packages/cmake-rn/tsconfig.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["**/*.test.ts"]
"exclude": ["**/*.test.ts"],
"references": [
{
"path": "../node-api-stub/tsconfig.node.json"
}
]
}
1 change: 1 addition & 0 deletions packages/host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@babel/types": "^7.27.0",
"@types/babel__core": "^7.20.5",
"fswin": "^3.24.829",
"node-api-stub": "workspace:*",
"weak-node-api": "workspace:*"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/host/scripts/generate-injector.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import cp from "node:child_process";

import { type FunctionDecl, getNodeApiFunctions } from "weak-node-api";
import { type FunctionDecl, getNodeApiFunctions } from "node-api-stub";

export const CPP_SOURCE_PATH = path.join(import.meta.dirname, "../cpp");

Expand Down
11 changes: 11 additions & 0 deletions packages/host/src/node/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,14 @@ export const pathSuffixOption = new Option(
)
.choices(LIBRARY_NAMING_CHOICES)
.default(NODE_API_PATH_SUFFIX || "strip");

export const NODE_API_LINKAGES = ["weak-node-api", "hermes", "none"] as const;

export type NodeApiLinkage = (typeof NODE_API_LINKAGES)[number];

export const nodeApiLinkageOption = new Option(
"--node-api-linkage <mode>",
"What addons link against for Node-API: weak-node-api, the Hermes library (via a link-time stub) or nothing at all",
)
.choices(NODE_API_LINKAGES)
.default("weak-node-api" satisfies NodeApiLinkage);
6 changes: 6 additions & 0 deletions packages/host/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@ export {
} from "./path-utils.js";

export { readAndParsePlist } from "./plist.js";

export {
NODE_API_LINKAGES,
type NodeApiLinkage,
nodeApiLinkageOption,
} from "./cli/options.js";
2 changes: 1 addition & 1 deletion packages/host/tsconfig.node-scripts.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"exclude": [],
"references": [
{
"path": "../weak-node-api/tsconfig.node.json"
"path": "../node-api-stub/tsconfig.node.json"
}
]
}
Loading
Loading