From 03bfe573c34bb198b9a9a892b7e1eac74e5f1652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 3 May 2025 22:36:30 +0200 Subject: [PATCH 1/9] Add option to skip auto-linking --- .../react-native-node-api-cmake/src/apple.ts | 18 +++++++++++------- .../react-native-node-api-cmake/src/cli.ts | 6 ++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/react-native-node-api-cmake/src/apple.ts b/packages/react-native-node-api-cmake/src/apple.ts index 6e8ef124..2d9ce6d4 100644 --- a/packages/react-native-node-api-cmake/src/apple.ts +++ b/packages/react-native-node-api-cmake/src/apple.ts @@ -126,6 +126,7 @@ export function getAppleBuildArgs() { type XCframeworkOptions = { frameworkPaths: string[]; outputPath: string; + autoLink: boolean; }; export function createFramework(libraryPath: string) { @@ -171,6 +172,7 @@ export function createFramework(libraryPath: string) { export async function createXCframework({ frameworkPaths, outputPath, + autoLink, }: XCframeworkOptions) { // Delete any existing xcframework to prevent the error: // - A library with the identifier 'macos-arm64' already exists. @@ -189,13 +191,15 @@ export async function createXCframework({ outputMode: "buffered", } ); - // Write a file to mark the xcframework is a Node-API module - // TODO: Consider including this in the Info.plist file instead - fs.writeFileSync( - path.join(outputPath, "react-native-node-api-module"), - "", - "utf8" - ); + if (autoLink) { + // Write a file to mark the xcframework is a Node-API module + // TODO: Consider including this in the Info.plist file instead + fs.writeFileSync( + path.join(outputPath, "react-native-node-api-module"), + "", + "utf8" + ); + } } /** diff --git a/packages/react-native-node-api-cmake/src/cli.ts b/packages/react-native-node-api-cmake/src/cli.ts index 0c723756..418f1a04 100644 --- a/packages/react-native-node-api-cmake/src/cli.ts +++ b/packages/react-native-node-api-cmake/src/cli.ts @@ -72,6 +72,11 @@ const ndkVersionOption = new Option( "The NDK version to use for Android builds" ).default(DEFAULT_NDK_VERSION); +const noAutoLinkOption = new Option( + "--no-auto-link", + "Don't mark the output as auto-linkable by react-native-node-api-modules" +); + const androidOption = new Option("--android", "Enable all Android triplets"); const appleOption = new Option("--apple", "Enable all Apple triplets"); @@ -203,6 +208,7 @@ export const program = new Command("react-native-node-api-cmake") createXCframework({ outputPath: xcframeworkOutputPath, frameworkPaths, + autoLink: globalContext.autoLink, }), { text: "Assembling XCFramework", From f10c45b4d4e79a3f274a3874e5f463f1ce7279d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 3 May 2025 22:37:17 +0200 Subject: [PATCH 2/9] Migrate copy script to TS --- .../{copy-node-api-headers.mjs => copy-node-api-headers.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename packages/react-native-node-api-modules/scripts/{copy-node-api-headers.mjs => copy-node-api-headers.ts} (65%) diff --git a/packages/react-native-node-api-modules/scripts/copy-node-api-headers.mjs b/packages/react-native-node-api-modules/scripts/copy-node-api-headers.ts similarity index 65% rename from packages/react-native-node-api-modules/scripts/copy-node-api-headers.mjs rename to packages/react-native-node-api-modules/scripts/copy-node-api-headers.ts index f1da854c..fef957a2 100644 --- a/packages/react-native-node-api-modules/scripts/copy-node-api-headers.mjs +++ b/packages/react-native-node-api-modules/scripts/copy-node-api-headers.ts @@ -1,9 +1,9 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; +import { include_dir as includeSourcePath } from "node-api-headers"; -const includeSourcePath = new URL(import.meta.resolve("node-api-headers/include")).pathname; -const includeDestinationPath = path.join(import.meta.dirname, "../include"); +const includeDestinationPath = path.join(__dirname, "../include"); assert(fs.existsSync(includeSourcePath), `Expected ${includeSourcePath}`); console.log(`Copying ${includeSourcePath} to ${includeDestinationPath}`); fs.cpSync(includeSourcePath, includeDestinationPath, { recursive: true }); From 7e2aa19eefb241da71444d043bf88fcd7a3770f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 3 May 2025 22:38:00 +0200 Subject: [PATCH 3/9] Fix tsconfig for scripts --- package-lock.json | 13 ++++++++++++- packages/react-native-node-api-modules/package.json | 6 ++++-- .../react-native-node-api-modules/tsconfig.json | 1 + .../tsconfig.node-scripts.json | 12 ++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 packages/react-native-node-api-modules/tsconfig.node-scripts.json diff --git a/package-lock.json b/package-lock.json index 5806eb49..6bb98bb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9861,6 +9861,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/gyp-to-cmake": { "version": "0.1.0", "dependencies": { @@ -10157,7 +10167,8 @@ "@babel/core": "^7.26.10", "@babel/types": "^7.27.0", "metro-config": "0.81.1", - "node-api-headers": "^1.5.0" + "node-api-headers": "^1.5.0", + "zod": "^3.24.3" }, "peerDependencies": { "@babel/core": "^7.26.10", diff --git a/packages/react-native-node-api-modules/package.json b/packages/react-native-node-api-modules/package.json index 57aa8cf8..94999002 100644 --- a/packages/react-native-node-api-modules/package.json +++ b/packages/react-native-node-api-modules/package.json @@ -15,7 +15,8 @@ }, "scripts": { "build": "tsc --build", - "copy-node-api-headers": "node scripts/copy-node-api-headers.mjs", + "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", + "build-weak-node-api": "tsx scripts/build-weak-node-api.ts", "test": "tsx --test src/node/**/*.test.ts src/node/*.test.ts" }, "keywords": [ @@ -57,7 +58,8 @@ "@babel/core": "^7.26.10", "@babel/types": "^7.27.0", "metro-config": "0.81.1", - "node-api-headers": "^1.5.0" + "node-api-headers": "^1.5.0", + "zod": "^3.24.3" }, "peerDependencies": { "@babel/core": "^7.26.10", diff --git a/packages/react-native-node-api-modules/tsconfig.json b/packages/react-native-node-api-modules/tsconfig.json index 1cfb08e3..00410045 100644 --- a/packages/react-native-node-api-modules/tsconfig.json +++ b/packages/react-native-node-api-modules/tsconfig.json @@ -2,6 +2,7 @@ "files": [], "references": [ { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.node-scripts.json" }, { "path": "./tsconfig.node-tests.json" }, { "path": "./tsconfig.react-native.json" } ] diff --git a/packages/react-native-node-api-modules/tsconfig.node-scripts.json b/packages/react-native-node-api-modules/tsconfig.node-scripts.json new file mode 100644 index 00000000..ed9d5a2b --- /dev/null +++ b/packages/react-native-node-api-modules/tsconfig.node-scripts.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "rootDir": "scripts", + "types": ["node"] + }, + "include": ["scripts/**/*.ts"], + "exclude": [] +} From 3eb0180b96c76beab2728c1e865609c390e06a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 3 May 2025 22:38:44 +0200 Subject: [PATCH 4/9] Add tool for building weak-node-api --- .../react-native-node-api-modules/.gitignore | 6 + .../scripts/build-weak-node-api.ts | 183 ++++++++++++++++++ .../scripts/node-api-headers.d.ts | 29 +++ .../weak-node-api/CMakeLists.txt | 9 + 4 files changed, 227 insertions(+) create mode 100644 packages/react-native-node-api-modules/scripts/build-weak-node-api.ts create mode 100644 packages/react-native-node-api-modules/scripts/node-api-headers.d.ts create mode 100644 packages/react-native-node-api-modules/weak-node-api/CMakeLists.txt diff --git a/packages/react-native-node-api-modules/.gitignore b/packages/react-native-node-api-modules/.gitignore index 9f7e7433..26da3310 100644 --- a/packages/react-native-node-api-modules/.gitignore +++ b/packages/react-native-node-api-modules/.gitignore @@ -15,3 +15,9 @@ xcframeworks/ # Android build artifacts android/.cxx/ android/build/ + +# Everything in weak-node-api is generated, except for the configurations +weak-node-api/build/ +weak-node-api/weak-node-api.xcframework +weak-node-api/weak-node-api.android.node +weak-node-api.cpp diff --git a/packages/react-native-node-api-modules/scripts/build-weak-node-api.ts b/packages/react-native-node-api-modules/scripts/build-weak-node-api.ts new file mode 100644 index 00000000..4f6f1740 --- /dev/null +++ b/packages/react-native-node-api-modules/scripts/build-weak-node-api.ts @@ -0,0 +1,183 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import cp from "node:child_process"; + +import { + type NodeApiVersion, + symbols, + include_dir as nodeApiIncludePath, +} from "node-api-headers"; +import { z } from "zod"; + +export const WEAK_NODE_API_PATH = path.join(__dirname, "../weak-node-api"); + +export function getNodeApiSymbols( + version: NodeApiVersion, + filter?: "js_native_api_symbols" | "node_api_symbols" +) { + const symbolsPerInterface = symbols[version]; + if (filter === "js_native_api_symbols") { + return symbolsPerInterface.js_native_api_symbols; + } else if (filter === "node_api_symbols") { + return symbolsPerInterface.node_api_symbols; + } else { + return [ + ...symbolsPerInterface.js_native_api_symbols, + ...symbolsPerInterface.node_api_symbols, + ]; + } +} + +export function generateVersionScript( + libraryName: string, + globalSymbols: string[] +) { + return [ + `${libraryName} {`, + ` global:`, + ...globalSymbols.map((symbol) => ` ${symbol};`), + ` local: *;`, + `};`, + ].join("\n"); +} + +const clangAstDump = z.object({ + kind: z.literal("TranslationUnitDecl"), + inner: z.array( + z.object({ + kind: z.string(), + name: z.string().optional(), + type: z + .object({ + qualType: z.string(), + }) + .optional(), + }) + ), +}); + +/** + * Generates source code for a version script for the given Node API version. + * @param version + */ +export function getNodeApiHeaderAST(version: NodeApiVersion) { + const output = cp.execFileSync( + "clang", + [ + // Declare the Node API version + "-D", + `NAPI_VERSION=${version.replace(/^v/, "")}`, + // Pass the next option directly to the Clang frontend + "-Xclang", + // Ask the Clang frontend to dump the AST + "-ast-dump=json", + // Parse and analyze the source file but not compile it + "-fsyntax-only", + // Include from the node-api-headers package + `-I${nodeApiIncludePath}`, + path.join(nodeApiIncludePath, "node_api.h"), + ], + { + encoding: "utf-8", + // Emitting the AST can produce a lot of output + maxBuffer: 1024 * 1024 * 10, + } + ); + const parsed = JSON.parse(output); + return clangAstDump.parse(parsed); +} + +/** + * Generates source code for a version script for the given Node API version. + * @param version + */ +export function generateFakeNodeApiSource(version: NodeApiVersion) { + const lines = [ + "// This file is generated by react-native-node-api-modules", + "#include ", + ]; + const root = getNodeApiHeaderAST(version); + assert.equal(root.kind, "TranslationUnitDecl"); + assert(Array.isArray(root.inner)); + const foundSymbols = new Set(); + const nodeApiSymbols = new Set(getNodeApiSymbols(version)); + for (const node of root.inner) { + if ( + node.kind === "FunctionDecl" && + node.name && + nodeApiSymbols.has(node.name) + ) { + foundSymbols.add(node.name); + + assert(node.type, `Expected type for ${node.name}`); + + const match = node.type.qualType.match( + /^(?[^(]+) \((?[^)]+)\)/ + ); + assert( + match && match.groups, + `Failed to parse function type: ${node.type.qualType}` + ); + const { returnType, argumentTypes } = match.groups; + assert( + returnType, + `Failed to get return type from ${node.type.qualType}` + ); + assert( + argumentTypes, + `Failed to get argument types from ${node.type.qualType}` + ); + assert( + returnType === "napi_status" || returnType === "void", + `Expected return type to be napi_status, got ${returnType}` + ); + lines.push( + `__attribute__((weak)) ${returnType} ${node.name}(${argumentTypes}) {`, + returnType === "void" ? "" : " napi_status::napi_generic_failure;", + "}" + ); + } + } + for (const knownSymbol of nodeApiSymbols) { + if (!foundSymbols.has(knownSymbol)) { + throw new Error( + `Missing symbol '${knownSymbol}' in the AST for Node API ${version}` + ); + } + } + return lines.join("\n"); +} + +export async function ensureNodeApiVersionScript(version: NodeApiVersion) { + const outputPath = path.join(WEAK_NODE_API_PATH, `fakenode-${version}.map`); + if (!fs.existsSync(outputPath)) { + // Make sure the output directory exists + fs.mkdirSync(WEAK_NODE_API_PATH, { recursive: true }); + const symbols = getNodeApiSymbols(version); + const content = generateVersionScript("libfakenode", symbols); + fs.writeFileSync(outputPath, content, "utf-8"); + } + return outputPath; +} + +async function run() { + const sourceCode = generateFakeNodeApiSource("v10"); + await fs.promises.mkdir(WEAK_NODE_API_PATH, { recursive: true }); + await fs.promises.writeFile( + path.join(WEAK_NODE_API_PATH, "weak-node-api.cpp"), + sourceCode, + "utf-8" + ); + // Build for all supported platforms + cp.spawnSync( + "react-native-node-api-cmake", + ["--android", "--apple", "--no-auto-link", "--source", WEAK_NODE_API_PATH], + { stdio: "inherit" } + ); +} + +run().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/packages/react-native-node-api-modules/scripts/node-api-headers.d.ts b/packages/react-native-node-api-modules/scripts/node-api-headers.d.ts new file mode 100644 index 00000000..dc6ab254 --- /dev/null +++ b/packages/react-native-node-api-modules/scripts/node-api-headers.d.ts @@ -0,0 +1,29 @@ +module "node-api-headers" { + type SymbolsPerInterface = { + js_native_api_symbols: string[]; + node_api_symbols: string[]; + }; + type Exported = { + include_dir: string; + def_paths: { + js_native_api_def: string; + node_api_def: string; + }; + symbols: { + v1: SymbolsPerInterface; + v2: SymbolsPerInterface; + v3: SymbolsPerInterface; + v4: SymbolsPerInterface; + v5: SymbolsPerInterface; + v6: SymbolsPerInterface; + v7: SymbolsPerInterface; + v8: SymbolsPerInterface; + v9: SymbolsPerInterface; + v10: SymbolsPerInterface; + }; + }; + export type NodeApiVersion = keyof Exported["symbols"]; + + const exported: Exported; + export = exported; +} diff --git a/packages/react-native-node-api-modules/weak-node-api/CMakeLists.txt b/packages/react-native-node-api-modules/weak-node-api/CMakeLists.txt new file mode 100644 index 00000000..12d6f6fc --- /dev/null +++ b/packages/react-native-node-api-modules/weak-node-api/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.15) +project(weak-node-api) + +add_compile_definitions(-DNAPI_VERSION=8) + +add_library(${PROJECT_NAME} SHARED weak-node-api.cpp ${CMAKE_JS_SRC}) +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") +target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/../include) +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) From 71421db89ce5aef0cafd87e2fd49f0de102f6a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 3 May 2025 22:39:12 +0200 Subject: [PATCH 5/9] WIP building Node-API modules for Android --- .../src/android.ts | 74 ++++++++++++++++++- .../react-native-node-api-cmake/src/apple.ts | 30 ++++++-- .../react-native-node-api-cmake/src/cli.ts | 59 ++++++++++++++- 3 files changed, 150 insertions(+), 13 deletions(-) diff --git a/packages/react-native-node-api-cmake/src/android.ts b/packages/react-native-node-api-cmake/src/android.ts index ed5de290..5857d8e6 100644 --- a/packages/react-native-node-api-cmake/src/android.ts +++ b/packages/react-native-node-api-cmake/src/android.ts @@ -1,4 +1,4 @@ -import assert from "node:assert"; +import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; @@ -31,7 +31,7 @@ export function isAndroidTriplet( type AndroidArchitecture = "armeabi-v7a" | "arm64-v8a" | "x86" | "x86_64"; -export const ARCHITECTURES = { +export const ANDROID_ARCHITECTURES = { "armv7a-linux-androideabi": "armeabi-v7a", "aarch64-linux-android": "arm64-v8a", "i686-linux-android": "x86", @@ -59,12 +59,20 @@ export function getAndroidConfigureCmakeArgs({ fs.existsSync(ndkPath), `Missing Android NDK v${ndkVersion} (at ${ndkPath}) - run: ${installNdkCommand}` ); + // TODO: Link against a fake node library const toolchainPath = path.join( ndkPath, "build/cmake/android.toolchain.cmake" ); - const architecture = ARCHITECTURES[triplet]; + const architecture = ANDROID_ARCHITECTURES[triplet]; + + const linkerFlags: string[] = [ + // `--no-version-undefined`, + // `--whole-archive`, + // `--no-whole-archive`, + ]; + return [ // Use the XCode as generator for Apple platforms "-G", @@ -97,7 +105,65 @@ export function getAndroidConfigureCmakeArgs({ "ANDROID_STL=c++_shared", // Pass linker flags to avoid errors from undefined symbols // TODO: Link against a fake libhermes to avoid this (or whatever other lib which will be providing the symbols) + // "-D", + // `CMAKE_SHARED_LINKER_FLAGS="-Wl,--allow-shlib-undefined"`, "-D", - `CMAKE_SHARED_LINKER_FLAGS="-Wl,--allow-shlib-undefined"`, + `CMAKE_SHARED_LINKER_FLAGS=${linkerFlags + .map((flag) => `-Wl,${flag}`) + .join(" ")}`, ]; } + +/** + * Determine the filename of the Android libs directory based on the framework paths. + * Ensuring that all framework paths have the same base name. + */ +export function determineAndroidLibsFilename(frameworkPaths: string[]) { + const frameworkNames = frameworkPaths.map((p) => + path.basename(p, path.extname(p)) + ); + const candidates = new Set(frameworkNames); + assert( + candidates.size === 1, + "Expected all frameworks to have the same name" + ); + const [name] = candidates; + return `${name}.android.node`; +} + +type AndroidLibsDirectoryOptions = { + outputPath: string; + libraryPathByTriplet: Record; + autoLink: boolean; +}; + +export async function createAndroidLibsDirectory({ + outputPath, + libraryPathByTriplet, + autoLink, +}: AndroidLibsDirectoryOptions) { + // Delete and recreate any existing output directory + await fs.promises.rm(outputPath, { recursive: true, force: true }); + await fs.promises.mkdir(outputPath, { recursive: true }); + for (const [triplet] of Object.entries(libraryPathByTriplet)) { + const libraryPath = libraryPathByTriplet[triplet as AndroidTriplet]; + assert( + fs.existsSync(libraryPath), + `Library not found: ${libraryPath} for triplet ${triplet}` + ); + const arch = ANDROID_ARCHITECTURES[triplet as AndroidTriplet]; + const archOutputPath = path.join(outputPath, arch); + await fs.promises.mkdir(archOutputPath, { recursive: true }); + const libraryName = path.basename(libraryPath, path.extname(libraryPath)); + const libraryOutputPath = path.join(archOutputPath, libraryName); + await fs.promises.copyFile(libraryPath, libraryOutputPath); + } + if (autoLink) { + // Write a file to mark the Android libs directory is a Node-API module + await fs.promises.writeFile( + path.join(outputPath, "react-native-node-api-module"), + "", + "utf8" + ); + } +} diff --git a/packages/react-native-node-api-cmake/src/apple.ts b/packages/react-native-node-api-cmake/src/apple.ts index 2d9ce6d4..46f8c918 100644 --- a/packages/react-native-node-api-cmake/src/apple.ts +++ b/packages/react-native-node-api-cmake/src/apple.ts @@ -41,7 +41,7 @@ type XcodeSDKName = | "appletvsimulator" | "macosx"; -const SDK_NAMES = { +const XCODE_SDK_NAMES = { "x86_64-apple-darwin": "macosx", "arm64-apple-darwin": "macosx", "arm64;x86_64-apple-darwin": "macosx", @@ -56,7 +56,7 @@ const SDK_NAMES = { type AppleArchitecture = "arm64" | "x86_64" | "arm64;x86_64"; -export const ARCHITECTURES = { +export const APPLE_ARCHITECTURES = { "x86_64-apple-darwin": "x86_64", "arm64-apple-darwin": "arm64", "arm64;x86_64-apple-darwin": "arm64;x86_64", @@ -77,9 +77,13 @@ export function isAppleTriplet( export function getAppleSDKPath(triplet: AppleTriplet) { return cp - .spawnSync("xcrun", ["--sdk", SDK_NAMES[triplet], "--show-sdk-path"], { - encoding: "utf-8", - }) + .spawnSync( + "xcrun", + ["--sdk", XCODE_SDK_NAMES[triplet], "--show-sdk-path"], + { + encoding: "utf-8", + } + ) .stdout.trim(); } @@ -102,19 +106,31 @@ export function getAppleConfigureCmakeArgs(triplet: AppleTriplet) { assert(isAppleTriplet(triplet)); const sdkPath = getAppleSDKPath(triplet); + // TODO: Make this Node-API version configurable + // const nodeApiSymbols = getNodeApiSymbols("v10"); + + const linkerFlags = [ + // Link against weak-node-api and remove this + "-Wl,-undefined,dynamic_lookup", + // Tread undefined symbols as errors + // "-Wl,-undefined,error", + // Pass all Node-API symbols to the linker + // ...nodeApiSymbols.map((symbol) => `-Wl,-U,_${symbol}`), + ]; + return [ // Use the XCode as generator for Apple platforms "-G", "Xcode", // Pass linker flags to avoid errors from undefined symbols "-D", - `CMAKE_SHARED_LINKER_FLAGS="-Wl,-undefined,dynamic_lookup"`, + `CMAKE_SHARED_LINKER_FLAGS=${linkerFlags.join(" ")}`, // Set the SDK path for the target platform "-D", `CMAKE_OSX_SYSROOT=${sdkPath}`, // Set the target architecture "-D", - `CMAKE_OSX_ARCHITECTURES=${ARCHITECTURES[triplet]}`, + `CMAKE_OSX_ARCHITECTURES=${APPLE_ARCHITECTURES[triplet]}`, ]; } diff --git a/packages/react-native-node-api-cmake/src/cli.ts b/packages/react-native-node-api-cmake/src/cli.ts index 418f1a04..24a6d1d2 100644 --- a/packages/react-native-node-api-cmake/src/cli.ts +++ b/packages/react-native-node-api-cmake/src/cli.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs/promises"; import { existsSync, readdirSync, renameSync } from "node:fs"; +import { EventEmitter } from "node:events"; import { Command, Option } from "@commander-js/extra-typings"; import { spawn, SpawnFailure } from "bufout"; @@ -23,11 +24,13 @@ import { DEFAULT_ANDROID_TRIPLETS, getAndroidConfigureCmakeArgs, isAndroidTriplet, + determineAndroidLibsFilename, + createAndroidLibsDirectory, + AndroidTriplet, } from "./android.js"; // We're attaching a lot of listeners when spawning in parallel -process.stdout.setMaxListeners(100); -process.stderr.setMaxListeners(100); +EventEmitter.defaultMaxListeners = 100; // This should match https://github.com/react-native-community/template/blob/main/template/android/build.gradle#L7 const DEFAULT_NDK_VERSION = "27.1.12297006"; @@ -91,6 +94,7 @@ export const program = new Command("react-native-node-api-cmake") .addOption(outPathOption) .addOption(cleanOption) .addOption(ndkVersionOption) + .addOption(noAutoLinkOption) .action(async ({ triplet: tripletValues, ...globalContext }) => { try { const buildPath = getBuildPath(globalContext); @@ -220,6 +224,57 @@ export const program = new Command("react-native-node-api-cmake") } ); } + + const androidTriplets = tripletContext.filter(({ triplet }) => + isAndroidTriplet(triplet) + ); + if (androidTriplets.length > 0) { + const libraryPathByTriplet = Object.fromEntries( + androidTriplets.map(({ tripletOutputPath, triplet }) => { + assert( + existsSync(tripletOutputPath), + `Expected a directory at ${tripletOutputPath}` + ); + // Expect binary file(s), either .node or .so + const result = readdirSync(tripletOutputPath).map((file) => { + const filePath = path.join(tripletOutputPath, file); + if (file.endsWith(".so") || file.endsWith(".node")) { + return filePath; + } else { + throw new Error( + `Expected a .node or .so file, but found ${file}` + ); + } + }); + assert.equal(result.length, 1, "Expected exactly library file"); + return [triplet, result[0]] as const; + }) + ) as Record; + const androidLibsFilename = determineAndroidLibsFilename( + Object.values(libraryPathByTriplet) + ); + const androidLibsOutputPath = path.resolve( + // Defaults to storing the xcframework next to the CMakeLists.txt file + globalContext.out || globalContext.source, + androidLibsFilename + ); + + await oraPromise( + createAndroidLibsDirectory({ + outputPath: androidLibsOutputPath, + libraryPathByTriplet, + autoLink: globalContext.autoLink, + }), + { + text: "Assembling Android libs directory", + successText: `Android libs directory assembled into ${chalk.dim( + path.relative(process.cwd(), androidLibsOutputPath) + )}`, + failText: ({ message }) => + `Failed to assemble Android libs directory: ${message}`, + } + ); + } } catch (error) { if (error instanceof SpawnFailure) { error.flushOutput("both"); From 47dc20170017c9a3e4fe0a05af24c2855869995f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 4 May 2025 13:39:57 +0200 Subject: [PATCH 6/9] Move types --- .../react-native-node-api-modules/tsconfig.node-scripts.json | 2 +- packages/react-native-node-api-modules/tsconfig.node.json | 2 +- .../node-api-headers.d.ts => types/node-api-headers/index.d.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/react-native-node-api-modules/{scripts/node-api-headers.d.ts => types/node-api-headers/index.d.ts} (100%) diff --git a/packages/react-native-node-api-modules/tsconfig.node-scripts.json b/packages/react-native-node-api-modules/tsconfig.node-scripts.json index ed9d5a2b..4e11d816 100644 --- a/packages/react-native-node-api-modules/tsconfig.node-scripts.json +++ b/packages/react-native-node-api-modules/tsconfig.node-scripts.json @@ -7,6 +7,6 @@ "rootDir": "scripts", "types": ["node"] }, - "include": ["scripts/**/*.ts"], + "include": ["scripts/**/*.ts", "types/**/*.d.ts"], "exclude": [] } diff --git a/packages/react-native-node-api-modules/tsconfig.node.json b/packages/react-native-node-api-modules/tsconfig.node.json index 9472d4dd..94c6bdf7 100644 --- a/packages/react-native-node-api-modules/tsconfig.node.json +++ b/packages/react-native-node-api-modules/tsconfig.node.json @@ -7,6 +7,6 @@ "rootDir": "src", "types": ["node"] }, - "include": ["src/node/**/*.ts"], + "include": ["src/node/**/*.ts", "types/**/*.d.ts"], "exclude": ["**.test.ts"] } diff --git a/packages/react-native-node-api-modules/scripts/node-api-headers.d.ts b/packages/react-native-node-api-modules/types/node-api-headers/index.d.ts similarity index 100% rename from packages/react-native-node-api-modules/scripts/node-api-headers.d.ts rename to packages/react-native-node-api-modules/types/node-api-headers/index.d.ts From b34b355f589248c5d14148a277526fcf055a78eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 4 May 2025 13:40:33 +0200 Subject: [PATCH 7/9] Add no-weak-node-api-linkage --- .../src/android.ts | 11 +++++-- .../react-native-node-api-cmake/src/apple.ts | 16 ++++++++-- .../react-native-node-api-cmake/src/cli.ts | 31 ++++++++++++++----- .../scripts/build-weak-node-api.ts | 9 +++++- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/packages/react-native-node-api-cmake/src/android.ts b/packages/react-native-node-api-cmake/src/android.ts index 5857d8e6..cfd63ffb 100644 --- a/packages/react-native-node-api-cmake/src/android.ts +++ b/packages/react-native-node-api-cmake/src/android.ts @@ -41,11 +41,13 @@ export const ANDROID_ARCHITECTURES = { type AndroidConfigureOptions = { triplet: AndroidTriplet; ndkVersion: string; + weakNodeApiLinkage: boolean; }; export function getAndroidConfigureCmakeArgs({ triplet, ndkVersion, + weakNodeApiLinkage, }: AndroidConfigureOptions) { const { ANDROID_HOME } = process.env; assert(typeof ANDROID_HOME === "string", "Missing env variable ANDROID_HOME"); @@ -59,7 +61,8 @@ export function getAndroidConfigureCmakeArgs({ fs.existsSync(ndkPath), `Missing Android NDK v${ndkVersion} (at ${ndkPath}) - run: ${installNdkCommand}` ); - // TODO: Link against a fake node library + + // TODO: Link against a weak-node-api library const toolchainPath = path.join( ndkPath, @@ -67,6 +70,10 @@ export function getAndroidConfigureCmakeArgs({ ); const architecture = ANDROID_ARCHITECTURES[triplet]; + if (weakNodeApiLinkage) { + throw new Error("Weak Node-API linkage is not supported yet"); + } + const linkerFlags: string[] = [ // `--no-version-undefined`, // `--whole-archive`, @@ -104,7 +111,7 @@ export function getAndroidConfigureCmakeArgs({ "-D", "ANDROID_STL=c++_shared", // Pass linker flags to avoid errors from undefined symbols - // TODO: Link against a fake libhermes to avoid this (or whatever other lib which will be providing the symbols) + // TODO: Link against a weak-node-api to avoid this (or whatever other lib which will be providing the symbols) // "-D", // `CMAKE_SHARED_LINKER_FLAGS="-Wl,--allow-shlib-undefined"`, "-D", diff --git a/packages/react-native-node-api-cmake/src/apple.ts b/packages/react-native-node-api-cmake/src/apple.ts index 46f8c918..f81975fd 100644 --- a/packages/react-native-node-api-cmake/src/apple.ts +++ b/packages/react-native-node-api-cmake/src/apple.ts @@ -102,20 +102,30 @@ export function createPlistContent(values: Record) { ].join("\n"); } -export function getAppleConfigureCmakeArgs(triplet: AppleTriplet) { +type AppleConfigureOptions = { + triplet: AppleTriplet; + weakNodeApiLinkage: boolean; +}; + +export function getAppleConfigureCmakeArgs({ + triplet, + weakNodeApiLinkage, +}: AppleConfigureOptions) { assert(isAppleTriplet(triplet)); const sdkPath = getAppleSDKPath(triplet); // TODO: Make this Node-API version configurable // const nodeApiSymbols = getNodeApiSymbols("v10"); + if (weakNodeApiLinkage) { + throw new Error("Weak Node-API linkage is not supported yet"); + } + const linkerFlags = [ // Link against weak-node-api and remove this "-Wl,-undefined,dynamic_lookup", // Tread undefined symbols as errors // "-Wl,-undefined,error", - // Pass all Node-API symbols to the linker - // ...nodeApiSymbols.map((symbol) => `-Wl,-U,_${symbol}`), ]; return [ diff --git a/packages/react-native-node-api-cmake/src/cli.ts b/packages/react-native-node-api-cmake/src/cli.ts index 24a6d1d2..78ec9a48 100644 --- a/packages/react-native-node-api-cmake/src/cli.ts +++ b/packages/react-native-node-api-cmake/src/cli.ts @@ -55,6 +55,9 @@ const tripletOption = new Option( "Triplets to build for" ).choices(SUPPORTED_TRIPLETS); +const androidOption = new Option("--android", "Enable all Android triplets"); +const appleOption = new Option("--apple", "Enable all Apple triplets"); + const buildPathOption = new Option( "--build ", "Specify the build directory to store the configured CMake project" @@ -80,8 +83,10 @@ const noAutoLinkOption = new Option( "Don't mark the output as auto-linkable by react-native-node-api-modules" ); -const androidOption = new Option("--android", "Enable all Android triplets"); -const appleOption = new Option("--apple", "Enable all Apple triplets"); +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-modules" +); export const program = new Command("react-native-node-api-cmake") .description("Build React Native Node API modules with CMake") @@ -95,6 +100,7 @@ export const program = new Command("react-native-node-api-cmake") .addOption(cleanOption) .addOption(ndkVersionOption) .addOption(noAutoLinkOption) + .addOption(noWeakNodeApiLinkageOption) .action(async ({ triplet: tripletValues, ...globalContext }) => { try { const buildPath = getBuildPath(globalContext); @@ -310,12 +316,19 @@ function getTripletBuildPath(buildPath: string, triplet: SupportedTriplet) { function getTripletConfigureCmakeArgs( triplet: SupportedTriplet, - { ndkVersion }: Pick + { + ndkVersion, + weakNodeApiLinkage, + }: Pick ) { if (isAndroidTriplet(triplet)) { - return getAndroidConfigureCmakeArgs({ triplet, ndkVersion }); + return getAndroidConfigureCmakeArgs({ + triplet, + ndkVersion, + weakNodeApiLinkage, + }); } else if (isAppleTriplet(triplet)) { - return getAppleConfigureCmakeArgs(triplet); + return getAppleConfigureCmakeArgs({ triplet, weakNodeApiLinkage }); } else { throw new Error(`Support for '${triplet}' is not implemented yet`); } @@ -332,7 +345,8 @@ function getBuildArgs(triplet: SupportedTriplet) { } async function configureProject(context: TripletScopedContext) { - const { triplet, tripletBuildPath, source, ndkVersion } = context; + const { triplet, tripletBuildPath, source, ndkVersion, weakNodeApiLinkage } = + context; const variables = getVariables(context); const variablesArgs = Object.entries(variables).flatMap(([key, value]) => [ "-D", @@ -347,7 +361,10 @@ async function configureProject(context: TripletScopedContext) { "-B", tripletBuildPath, ...variablesArgs, - ...getTripletConfigureCmakeArgs(triplet, { ndkVersion }), + ...getTripletConfigureCmakeArgs(triplet, { + ndkVersion, + weakNodeApiLinkage, + }), ], { outputMode: "buffered", diff --git a/packages/react-native-node-api-modules/scripts/build-weak-node-api.ts b/packages/react-native-node-api-modules/scripts/build-weak-node-api.ts index 4f6f1740..bbe75c3b 100644 --- a/packages/react-native-node-api-modules/scripts/build-weak-node-api.ts +++ b/packages/react-native-node-api-modules/scripts/build-weak-node-api.ts @@ -172,7 +172,14 @@ async function run() { // Build for all supported platforms cp.spawnSync( "react-native-node-api-cmake", - ["--android", "--apple", "--no-auto-link", "--source", WEAK_NODE_API_PATH], + [ + "--android", + "--apple", + "--no-auto-link", + "--no-weak-node-api-linkage", + "--source", + WEAK_NODE_API_PATH, + ], { stdio: "inherit" } ); } From 7ace493daa0b36df08f22b1b335facc75dcdb5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 4 May 2025 13:40:59 +0200 Subject: [PATCH 8/9] Make the node-api headers public for weak-node-api --- .../react-native-node-api-modules/weak-node-api/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-node-api-modules/weak-node-api/CMakeLists.txt b/packages/react-native-node-api-modules/weak-node-api/CMakeLists.txt index 12d6f6fc..918b666f 100644 --- a/packages/react-native-node-api-modules/weak-node-api/CMakeLists.txt +++ b/packages/react-native-node-api-modules/weak-node-api/CMakeLists.txt @@ -5,5 +5,5 @@ add_compile_definitions(-DNAPI_VERSION=8) add_library(${PROJECT_NAME} SHARED weak-node-api.cpp ${CMAKE_JS_SRC}) set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") -target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/../include) +target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_SOURCE_DIR}/../include) target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) From 5ad833d06a690d6739c13977d82ddcf52c4085e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 4 May 2025 21:36:26 +0200 Subject: [PATCH 9/9] Fix linking with the weak-node-api dynamic library --- .../src/android.ts | 32 +--------- .../react-native-node-api-cmake/src/apple.ts | 61 ++++++------------- .../react-native-node-api-cmake/src/cli.ts | 47 ++++++-------- .../src/triplets.ts | 40 +++++++++++- .../src/weak-node-api.ts | 50 +++++++++++++++ .../package.json | 3 +- 6 files changed, 132 insertions(+), 101 deletions(-) create mode 100644 packages/react-native-node-api-cmake/src/weak-node-api.ts diff --git a/packages/react-native-node-api-cmake/src/android.ts b/packages/react-native-node-api-cmake/src/android.ts index cfd63ffb..db8054c9 100644 --- a/packages/react-native-node-api-cmake/src/android.ts +++ b/packages/react-native-node-api-cmake/src/android.ts @@ -2,19 +2,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { type SupportedTriplet } from "./triplets.js"; - -/** - * https://developer.android.com/ndk/guides/other_build_systems - */ -export const ANDROID_TRIPLETS = [ - "aarch64-linux-android", - "armv7a-linux-androideabi", - "i686-linux-android", - "x86_64-linux-android", -] as const; - -export type AndroidTriplet = (typeof ANDROID_TRIPLETS)[number]; +import { AndroidTriplet } from "./triplets.js"; export const DEFAULT_ANDROID_TRIPLETS = [ "aarch64-linux-android", @@ -23,12 +11,6 @@ export const DEFAULT_ANDROID_TRIPLETS = [ "x86_64-linux-android", ] as const satisfies AndroidTriplet[]; -export function isAndroidTriplet( - triplet: SupportedTriplet -): triplet is AndroidTriplet { - return ANDROID_TRIPLETS.includes(triplet as AndroidTriplet); -} - type AndroidArchitecture = "armeabi-v7a" | "arm64-v8a" | "x86" | "x86_64"; export const ANDROID_ARCHITECTURES = { @@ -41,13 +23,11 @@ export const ANDROID_ARCHITECTURES = { type AndroidConfigureOptions = { triplet: AndroidTriplet; ndkVersion: string; - weakNodeApiLinkage: boolean; }; export function getAndroidConfigureCmakeArgs({ triplet, ndkVersion, - weakNodeApiLinkage, }: AndroidConfigureOptions) { const { ANDROID_HOME } = process.env; assert(typeof ANDROID_HOME === "string", "Missing env variable ANDROID_HOME"); @@ -62,18 +42,12 @@ export function getAndroidConfigureCmakeArgs({ `Missing Android NDK v${ndkVersion} (at ${ndkPath}) - run: ${installNdkCommand}` ); - // TODO: Link against a weak-node-api library - const toolchainPath = path.join( ndkPath, "build/cmake/android.toolchain.cmake" ); const architecture = ANDROID_ARCHITECTURES[triplet]; - if (weakNodeApiLinkage) { - throw new Error("Weak Node-API linkage is not supported yet"); - } - const linkerFlags: string[] = [ // `--no-version-undefined`, // `--whole-archive`, @@ -88,8 +62,8 @@ export function getAndroidConfigureCmakeArgs({ toolchainPath, "-D", "CMAKE_SYSTEM_NAME=Android", - "-D", - `CPACK_SYSTEM_NAME=Android-${architecture}`, + // "-D", + // `CPACK_SYSTEM_NAME=Android-${architecture}`, // "-D", // `CMAKE_INSTALL_PREFIX=${installPath}`, // "-D", diff --git a/packages/react-native-node-api-cmake/src/apple.ts b/packages/react-native-node-api-cmake/src/apple.ts index f81975fd..ca8303ac 100644 --- a/packages/react-native-node-api-cmake/src/apple.ts +++ b/packages/react-native-node-api-cmake/src/apple.ts @@ -3,23 +3,9 @@ import cp from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import type { SupportedTriplet } from "./triplets.js"; import { spawn } from "bufout"; -export const APPLE_TRIPLETS = [ - "arm64;x86_64-apple-darwin", - "x86_64-apple-darwin", - "arm64-apple-darwin", - "arm64-apple-ios", - "arm64-apple-ios-sim", - "arm64-apple-tvos", - "arm64-apple-tvos-sim", - // "x86_64-apple-tvos", - "arm64-apple-visionos", - "arm64-apple-visionos-sim", -] as const; - -export type AppleTriplet = (typeof APPLE_TRIPLETS)[number]; +import { AppleTriplet, isAppleTriplet } from "./triplets.js"; export const DEFAULT_APPLE_TRIPLETS = [ "arm64;x86_64-apple-darwin", @@ -54,6 +40,21 @@ const XCODE_SDK_NAMES = { "arm64-apple-visionos-sim": "xrsimulator", } satisfies Record; +type CMakeSystemName = "Darwin" | "iOS" | "tvOS" | "watchOS" | "visionOS"; + +const CMAKE_SYSTEM_NAMES = { + "x86_64-apple-darwin": "Darwin", + "arm64-apple-darwin": "Darwin", + "arm64;x86_64-apple-darwin": "Darwin", + "arm64-apple-ios": "iOS", + "arm64-apple-ios-sim": "iOS", + "arm64-apple-tvos": "tvOS", + // "x86_64-apple-tvos": "appletvos", + "arm64-apple-tvos-sim": "tvOS", + "arm64-apple-visionos": "visionOS", + "arm64-apple-visionos-sim": "visionOS", +} satisfies Record; + type AppleArchitecture = "arm64" | "x86_64" | "arm64;x86_64"; export const APPLE_ARCHITECTURES = { @@ -69,12 +70,6 @@ export const APPLE_ARCHITECTURES = { "arm64-apple-visionos-sim": "arm64", } satisfies Record; -export function isAppleTriplet( - triplet: SupportedTriplet -): triplet is AppleTriplet { - return APPLE_TRIPLETS.includes(triplet as AppleTriplet); -} - export function getAppleSDKPath(triplet: AppleTriplet) { return cp .spawnSync( @@ -104,37 +99,19 @@ export function createPlistContent(values: Record) { type AppleConfigureOptions = { triplet: AppleTriplet; - weakNodeApiLinkage: boolean; }; -export function getAppleConfigureCmakeArgs({ - triplet, - weakNodeApiLinkage, -}: AppleConfigureOptions) { +export function getAppleConfigureCmakeArgs({ triplet }: AppleConfigureOptions) { assert(isAppleTriplet(triplet)); const sdkPath = getAppleSDKPath(triplet); - - // TODO: Make this Node-API version configurable - // const nodeApiSymbols = getNodeApiSymbols("v10"); - - if (weakNodeApiLinkage) { - throw new Error("Weak Node-API linkage is not supported yet"); - } - - const linkerFlags = [ - // Link against weak-node-api and remove this - "-Wl,-undefined,dynamic_lookup", - // Tread undefined symbols as errors - // "-Wl,-undefined,error", - ]; + const systemName = CMAKE_SYSTEM_NAMES[triplet]; return [ // Use the XCode as generator for Apple platforms "-G", "Xcode", - // Pass linker flags to avoid errors from undefined symbols "-D", - `CMAKE_SHARED_LINKER_FLAGS=${linkerFlags.join(" ")}`, + `CMAKE_SYSTEM_NAME=${systemName}`, // Set the SDK path for the target platform "-D", `CMAKE_OSX_SYSROOT=${sdkPath}`, diff --git a/packages/react-native-node-api-cmake/src/cli.ts b/packages/react-native-node-api-cmake/src/cli.ts index 78ec9a48..e3c641a1 100644 --- a/packages/react-native-node-api-cmake/src/cli.ts +++ b/packages/react-native-node-api-cmake/src/cli.ts @@ -7,9 +7,15 @@ import { EventEmitter } from "node:events"; import { Command, Option } from "@commander-js/extra-typings"; import { spawn, SpawnFailure } from "bufout"; import { oraPromise } from "ora"; +import chalk from "chalk"; -import { SUPPORTED_TRIPLETS, SupportedTriplet } from "./triplets.js"; -import { getNodeApiHeadersPath, getNodeAddonHeadersPath } from "./headers.js"; +import { + SUPPORTED_TRIPLETS, + SupportedTriplet, + AndroidTriplet, + isAndroidTriplet, + isAppleTriplet, +} from "./triplets.js"; import { createFramework, createXCframework, @@ -17,17 +23,14 @@ import { determineXCFrameworkFilename, getAppleBuildArgs, getAppleConfigureCmakeArgs, - isAppleTriplet, } from "./apple.js"; -import chalk from "chalk"; import { DEFAULT_ANDROID_TRIPLETS, getAndroidConfigureCmakeArgs, - isAndroidTriplet, determineAndroidLibsFilename, createAndroidLibsDirectory, - AndroidTriplet, } from "./android.js"; +import { getWeakNodeApiVariables } from "./weak-node-api.js"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -316,19 +319,15 @@ function getTripletBuildPath(buildPath: string, triplet: SupportedTriplet) { function getTripletConfigureCmakeArgs( triplet: SupportedTriplet, - { - ndkVersion, - weakNodeApiLinkage, - }: Pick + { ndkVersion }: Pick ) { if (isAndroidTriplet(triplet)) { return getAndroidConfigureCmakeArgs({ triplet, ndkVersion, - weakNodeApiLinkage, }); } else if (isAppleTriplet(triplet)) { - return getAppleConfigureCmakeArgs({ triplet, weakNodeApiLinkage }); + return getAppleConfigureCmakeArgs({ triplet }); } else { throw new Error(`Support for '${triplet}' is not implemented yet`); } @@ -347,12 +346,6 @@ function getBuildArgs(triplet: SupportedTriplet) { async function configureProject(context: TripletScopedContext) { const { triplet, tripletBuildPath, source, ndkVersion, weakNodeApiLinkage } = context; - const variables = getVariables(context); - const variablesArgs = Object.entries(variables).flatMap(([key, value]) => [ - "-D", - `${key}=${value}`, - ]); - await spawn( "cmake", [ @@ -360,7 +353,7 @@ async function configureProject(context: TripletScopedContext) { source, "-B", tripletBuildPath, - ...variablesArgs, + ...getVariablesArgs(getVariables(context)), ...getTripletConfigureCmakeArgs(triplet, { ndkVersion, weakNodeApiLinkage, @@ -391,15 +384,15 @@ async function buildProject(context: TripletScopedContext) { } function getVariables(context: TripletScopedContext): Record { - const includePaths = [getNodeApiHeadersPath(), getNodeAddonHeadersPath()]; - for (const includePath of includePaths) { - assert( - !includePath.includes(";"), - `Include path with a ';' is not supported: ${includePath}` - ); - } return { - CMAKE_JS_INC: includePaths.join(";"), + ...(context.weakNodeApiLinkage && getWeakNodeApiVariables(context.triplet)), CMAKE_LIBRARY_OUTPUT_DIRECTORY: context.tripletOutputPath, }; } + +function getVariablesArgs(variables: Record) { + return Object.entries(variables).flatMap(([key, value]) => [ + "-D", + `${key}=${value}`, + ]); +} diff --git a/packages/react-native-node-api-cmake/src/triplets.ts b/packages/react-native-node-api-cmake/src/triplets.ts index 71e7dd15..fcfbf6a9 100644 --- a/packages/react-native-node-api-cmake/src/triplets.ts +++ b/packages/react-native-node-api-cmake/src/triplets.ts @@ -1,5 +1,29 @@ -import { ANDROID_TRIPLETS } from "./android.js"; -import { APPLE_TRIPLETS } from "./apple.js"; +/** + * https://developer.android.com/ndk/guides/other_build_systems + */ +export const ANDROID_TRIPLETS = [ + "aarch64-linux-android", + "armv7a-linux-androideabi", + "i686-linux-android", + "x86_64-linux-android", +] as const; + +export type AndroidTriplet = (typeof ANDROID_TRIPLETS)[number]; + +export const APPLE_TRIPLETS = [ + "arm64;x86_64-apple-darwin", + "x86_64-apple-darwin", + "arm64-apple-darwin", + "arm64-apple-ios", + "arm64-apple-ios-sim", + "arm64-apple-tvos", + "arm64-apple-tvos-sim", + // "x86_64-apple-tvos", + "arm64-apple-visionos", + "arm64-apple-visionos-sim", +] as const; + +export type AppleTriplet = (typeof APPLE_TRIPLETS)[number]; export const SUPPORTED_TRIPLETS = [ ...APPLE_TRIPLETS, @@ -7,3 +31,15 @@ export const SUPPORTED_TRIPLETS = [ ] as const; export type SupportedTriplet = (typeof SUPPORTED_TRIPLETS)[number]; + +export function isAndroidTriplet( + triplet: SupportedTriplet +): triplet is AndroidTriplet { + return ANDROID_TRIPLETS.includes(triplet as AndroidTriplet); +} + +export function isAppleTriplet( + triplet: SupportedTriplet +): triplet is AppleTriplet { + return APPLE_TRIPLETS.includes(triplet as AppleTriplet); +} diff --git a/packages/react-native-node-api-cmake/src/weak-node-api.ts b/packages/react-native-node-api-cmake/src/weak-node-api.ts new file mode 100644 index 00000000..ac3e288e --- /dev/null +++ b/packages/react-native-node-api-cmake/src/weak-node-api.ts @@ -0,0 +1,50 @@ +import fs from "node:fs"; +import assert from "node:assert/strict"; +import path from "node:path"; + +import { + isAndroidTriplet, + isAppleTriplet, + SupportedTriplet, +} from "./triplets.js"; +import { ANDROID_ARCHITECTURES } from "./android.js"; +import { getNodeAddonHeadersPath, getNodeApiHeadersPath } from "./headers.js"; + +export function getWeakNodeApiPath(triplet: SupportedTriplet): string { + const { pathname } = new URL( + import.meta.resolve("react-native-node-api-modules/weak-node-api") + ); + assert(fs.existsSync(pathname), "Weak Node API path does not exist"); + if (isAppleTriplet(triplet)) { + const xcframeworkPath = path.join(pathname, "weak-node-api.xcframework"); + assert( + fs.existsSync(xcframeworkPath), + `Expected an XCFramework at ${xcframeworkPath}` + ); + return xcframeworkPath; + } else if (isAndroidTriplet(triplet)) { + const libraryPath = path.join( + pathname, + "weak-node-api.android.node", + ANDROID_ARCHITECTURES[triplet], + "weak-node-api" + ); + assert(fs.existsSync(libraryPath), `Expected library at ${libraryPath}`); + return libraryPath; + } + return pathname; +} + +export function getWeakNodeApiVariables(triplet: SupportedTriplet) { + const includePaths = [getNodeApiHeadersPath(), getNodeAddonHeadersPath()]; + for (const includePath of includePaths) { + assert( + !includePath.includes(";"), + `Include path with a ';' is not supported: ${includePath}` + ); + } + return { + CMAKE_JS_INC: includePaths.join(";"), + CMAKE_JS_LIB: getWeakNodeApiPath(triplet), + }; +} diff --git a/packages/react-native-node-api-modules/package.json b/packages/react-native-node-api-modules/package.json index 94999002..4db2024b 100644 --- a/packages/react-native-node-api-modules/package.json +++ b/packages/react-native-node-api-modules/package.json @@ -11,7 +11,8 @@ "exports": { ".": "./dist/react-native/index.js", "./babel-plugin": "./dist/node/babel-plugin/index.js", - "./cli": "./dist/node/cli/run.js" + "./cli": "./dist/node/cli/run.js", + "./weak-node-api": "./weak-node-api" }, "scripts": { "build": "tsc --build",