diff --git a/docs/PREBUILDS.md b/docs/PREBUILDS.md index 5f057953..82e4e386 100644 --- a/docs/PREBUILDS.md +++ b/docs/PREBUILDS.md @@ -4,11 +4,11 @@ This document codifies the naming and directory structure of prebuilt binaries, At the time of writing, our auto-linking host package (`react-native-node-api`) support two kinds of prebuilds: -## `*.android.node` (for Android) +## `{name}.android.node` (for Android) -A jniLibs-like directory structure of CPU-architecture specific directories containing a single `.so` library file. +A jniLibs-like directory structure of CPU-architecture specific directories containing a single shared object / dynamic library file. -The name of all the `.so` library files: +The name of all the library files: - must be the same across all CPU-architectures - can have a "lib" prefix, but doesn't have to @@ -21,7 +21,7 @@ The name of all the `.so` library files: The directory must have a `react-native-node-api-module` file (the content doesn't matter), to signal that the directory is intended for auto-linking by the `react-native-node-api-module` package. -## `*.apple.node` (for Apple) +## `{name}.apple.node` (for Apple) An XCFramework of dynamic libraries wrapped in `.framework` bundles, renamed from `.xcframework` to `.apple.node` to ease discoverability. @@ -31,6 +31,16 @@ The Apple Developer documentation on ["Creating a multiplatform binary framework The directory must have a `react-native-node-api-module` file (the content doesn't matter), to signal that the directory is intended for auto-linking by the `react-native-node-api-module` package. +## `{name}.nodejs.node` (for Node.js) + +A directory of OS + CPU architecture -specific directories (named `{process.platform}-{process.arch}`) containing a single shared object / dynamic library file. + +The name of all the library files: + +- must be the same across all CPU-architectures +- can have a "lib" prefix, but doesn't have to +- must have a `.node` file extension + ## Why did we choose this naming scheme? To align with prior art and established patterns around the distribution of Node-API modules for Node.js, we've chosen to use the ".node" filename extension for prebuilds of Node-API modules, targeting React Native. diff --git a/package-lock.json b/package-lock.json index 051c13fb..b543a8a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "prettier": "^3.6.2", "react-native": "0.79.5", "tsx": "^4.20.3", - "typescript": "^5.8", + "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" } }, @@ -14347,8 +14347,8 @@ "cmake-rn": "bin/cmake-rn.js" }, "peerDependencies": { - "node-addon-api": "^8.3.1", - "node-api-headers": "^1.5.0" + "node-addon-api": "^8", + "node-api-headers": "^1" } }, "packages/cmake-rn/node_modules/@commander-js/extra-typings": { diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 4f56012f..100ffe57 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -31,7 +31,7 @@ "react-native-node-api": "0.3.2" }, "peerDependencies": { - "node-addon-api": "^8.3.1", - "node-api-headers": "^1.5.0" + "node-addon-api": "^8", + "node-api-headers": "^1" } } diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index b9070c31..d6a38684 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -8,15 +8,14 @@ import { spawn, SpawnFailure } from "bufout"; import { oraPromise } from "ora"; import chalk from "chalk"; -import { getWeakNodeApiVariables } from "./weak-node-api.js"; import { platforms, allTargets, findPlatformForTarget, platformHasTarget, } from "./platforms.js"; +import { toDeclarationArguments } from "./cmake.js"; import { BaseOpts, TargetContext, Platform } from "./platforms/types.js"; -import { isSupportedTriplet } from "react-native-node-api"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -118,6 +117,10 @@ program = program.action( // Forcing the types a bit here, since the platform id option is dynamically added if ((baseOptions as Record)[platform.id]) { for (const target of platform.targets) { + // Skip redundant targets + if (platform.redundantTargets?.includes(target)) { + continue; + } targets.add(target); } } @@ -242,7 +245,6 @@ function getTargetsSummary( } function getBuildPath({ build, source }: BaseOpts) { - // TODO: Add configuration (debug vs release) return path.resolve(process.cwd(), build || path.join(source, "build")); } @@ -251,6 +253,7 @@ function getBuildPath({ build, source }: BaseOpts) { */ function getTargetBuildPath(buildPath: string, target: unknown) { assert(typeof target === "string", "Expected target to be a string"); + // TODO: Add configuration (debug vs release) for platforms using single-config CMake generators return path.join(buildPath, target.replace(/;/g, "_")); } @@ -260,18 +263,7 @@ async function configureProject( options: BaseOpts, ) { const { target, buildPath, outputPath } = context; - const { verbose, source, weakNodeApiLinkage } = options; - - const nodeApiVariables = - weakNodeApiLinkage && isSupportedTriplet(target) - ? getWeakNodeApiVariables(target) - : // TODO: Make this a part of the platform definition - {}; - - const declarations = { - ...nodeApiVariables, - CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath, - }; + const { verbose, source } = options; await spawn( "cmake", @@ -281,7 +273,9 @@ async function configureProject( "-B", buildPath, ...platform.configureArgs(context, options), - ...toDeclarationArguments(declarations), + ...toDeclarationArguments({ + CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath, + }), ], { outputMode: verbose ? "inherit" : "buffered", @@ -296,17 +290,10 @@ async function buildProject( options: BaseOpts, ) { const { target, buildPath } = context; - const { verbose, configuration } = options; + const { verbose } = options; await spawn( "cmake", - [ - "--build", - buildPath, - "--config", - configuration, - "--", - ...platform.buildArgs(context, options), - ], + ["--build", buildPath, ...platform.buildArgs(context, options)], { outputMode: verbose ? "inherit" : "buffered", outputPrefix: verbose ? chalk.dim(`[${target}] `) : undefined, @@ -314,11 +301,4 @@ async function buildProject( ); } -function toDeclarationArguments(declarations: Record) { - return Object.entries(declarations).flatMap(([key, value]) => [ - "-D", - `${key}=${value}`, - ]); -} - export { program }; diff --git a/packages/cmake-rn/src/cmake.ts b/packages/cmake-rn/src/cmake.ts new file mode 100644 index 00000000..a601f7c4 --- /dev/null +++ b/packages/cmake-rn/src/cmake.ts @@ -0,0 +1,6 @@ +export function toDeclarationArguments(declarations: Record) { + return Object.entries(declarations).flatMap(([key, value]) => [ + "-D", + `${key}=${value}`, + ]); +} diff --git a/packages/cmake-rn/src/headers.ts b/packages/cmake-rn/src/headers.ts index 5ca91be8..7b0e12a4 100644 --- a/packages/cmake-rn/src/headers.ts +++ b/packages/cmake-rn/src/headers.ts @@ -45,3 +45,18 @@ export function getNodeAddonHeadersPath(): string { ); } } + +/** + * @returns A semicolon-separated list of include directories for Node-API headers. + * @note Using semicolon as a path separator for CMake regardless of platform + */ +export function getNodeApiIncludeDirectories(): string { + const includePaths = [getNodeApiHeadersPath(), getNodeAddonHeadersPath()]; + for (const includePath of includePaths) { + assert( + !includePath.includes(";"), + `Include path with a ';' is not supported: ${includePath}`, + ); + } + return includePaths.join(";"); +} diff --git a/packages/cmake-rn/src/platforms.ts b/packages/cmake-rn/src/platforms.ts index cf892263..d48bc83a 100644 --- a/packages/cmake-rn/src/platforms.ts +++ b/packages/cmake-rn/src/platforms.ts @@ -2,10 +2,15 @@ import assert from "node:assert/strict"; import { platform as android } from "./platforms/android.js"; import { platform as apple } from "./platforms/apple.js"; +import { platform as node } from "./platforms/node.js"; import { Platform } from "./platforms/types.js"; -export const platforms: Platform[] = [android, apple] as const; -export const allTargets = [...android.targets, ...apple.targets] as const; +export const platforms: Platform[] = [android, apple, node] as const; +export const allTargets = [ + ...android.targets, + ...apple.targets, + ...node.targets, +] as const; export function platformHasTarget

( platform: P, diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index 63397aa0..4d09ea3e 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -6,12 +6,17 @@ import { Option } from "@commander-js/extra-typings"; import { createAndroidLibsDirectory, determineAndroidLibsFilename, - AndroidTriplet as Target, + AndroidTriplet, + isAndroidTriplet, } from "react-native-node-api"; import type { Platform } from "./types.js"; import { oraPromise } from "ora"; import chalk from "chalk"; +import { getWeakNodeApiVariables } from "../weak-node-api.js"; +import { toDeclarationArguments } from "../cmake.js"; + +type Target = `${AndroidTriplet}-reactnative`; // This should match https://github.com/react-native-community/template/blob/main/template/android/build.gradle#L7 const DEFAULT_NDK_VERSION = "27.1.12297006"; @@ -24,7 +29,7 @@ export const ANDROID_ARCHITECTURES = { "aarch64-linux-android": "arm64-v8a", "i686-linux-android": "x86", "x86_64-linux-android": "x86_64", -} satisfies Record; +} satisfies Record; const ndkVersionOption = new Option( "--ndk-version ", @@ -38,20 +43,26 @@ const androidSdkVersionOption = new Option( type AndroidOpts = { ndkVersion: string; androidSdkVersion: string }; +function tripletFromTarget(target: Target): AndroidTriplet { + const result = target.replaceAll(/-reactnative$/g, "") as AndroidTriplet; + assert(isAndroidTriplet(result), `Invalid Android triplet: ${target}`); + return result; +} + export const platform: Platform = { id: "android", name: "Android", targets: [ - "aarch64-linux-android", - "armv7a-linux-androideabi", - "i686-linux-android", - "x86_64-linux-android", + "aarch64-linux-android-reactnative", + "armv7a-linux-androideabi-reactnative", + "i686-linux-android-reactnative", + "x86_64-linux-android-reactnative", ], defaultTargets() { if (process.arch === "arm64") { - return ["aarch64-linux-android"]; + return ["aarch64-linux-android-reactnative"]; } else if (process.arch === "x64") { - return ["x86_64-linux-android"]; + return ["x86_64-linux-android-reactnative"]; } else { return []; } @@ -61,7 +72,10 @@ export const platform: Platform = { .addOption(ndkVersionOption) .addOption(androidSdkVersionOption); }, - configureArgs({ target }, { ndkVersion, androidSdkVersion }) { + configureArgs( + { target }, + { configuration, ndkVersion, androidSdkVersion, weakNodeApiLinkage }, + ) { const { ANDROID_HOME } = process.env; assert( typeof ANDROID_HOME === "string", @@ -82,38 +96,28 @@ export const platform: Platform = { ndkPath, "build/cmake/android.toolchain.cmake", ); - const architecture = ANDROID_ARCHITECTURES[target]; + + const triplet = tripletFromTarget(target); return [ "-G", "Ninja", "--toolchain", toolchainPath, - "-D", - "CMAKE_SYSTEM_NAME=Android", - // "-D", - // `CPACK_SYSTEM_NAME=Android-${architecture}`, - // "-D", - // `CMAKE_INSTALL_PREFIX=${installPath}`, - // "-D", - // `CMAKE_BUILD_TYPE=${configuration}`, - "-D", - "CMAKE_MAKE_PROGRAM=ninja", - // "-D", - // "CMAKE_C_COMPILER_LAUNCHER=ccache", - // "-D", - // "CMAKE_CXX_COMPILER_LAUNCHER=ccache", - "-D", - `ANDROID_NDK=${ndkPath}`, - "-D", - `ANDROID_ABI=${architecture}`, - "-D", - "ANDROID_TOOLCHAIN=clang", - "-D", - `ANDROID_PLATFORM=${androidSdkVersion}`, - "-D", - // TODO: Make this configurable - "ANDROID_STL=c++_shared", + ...toDeclarationArguments({ + CMAKE_SYSTEM_NAME: "Android", + CMAKE_MAKE_PROGRAM: "ninja", + CMAKE_BUILD_TYPE: configuration, + // "CMAKE_C_COMPILER_LAUNCHER": "ccache", + // "CMAKE_CXX_COMPILER_LAUNCHER": "ccache", + ANDROID_NDK: ndkPath, + ANDROID_ABI: ANDROID_ARCHITECTURES[triplet], + ANDROID_TOOLCHAIN: "clang", + ANDROID_PLATFORM: androidSdkVersion, + // TODO: Make this configurable + ANDROID_STL: "c++_shared", + ...(weakNodeApiLinkage ? getWeakNodeApiVariables(triplet) : {}), + }), ]; }, buildArgs() { @@ -144,10 +148,11 @@ export const platform: Platform = { ) .map((dirent) => path.join(dirent.parentPath, dirent.name)); assert.equal(result.length, 1, "Expected exactly one library file"); - return [target, result[0]] as const; + const triplet = tripletFromTarget(target); + return [triplet, result[0]] as const; }), ), - ) as Record; + ) as Record; const androidLibsFilename = determineAndroidLibsFilename( Object.values(libraryPathByTriplet), ); diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index ce091dc4..c4f70cf1 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -5,14 +5,19 @@ import fs from "node:fs"; import { Option } from "@commander-js/extra-typings"; import { oraPromise } from "ora"; import { - AppleTriplet as Target, + AppleTriplet, createAppleFramework, createXCframework, determineXCFrameworkFilename, + isAppleTriplet, } from "react-native-node-api"; import type { Platform } from "./types.js"; import chalk from "chalk"; +import { toDeclarationArguments } from "../cmake.js"; +import { getWeakNodeApiVariables } from "../weak-node-api.js"; + +type Target = `${AppleTriplet}-reactnative`; type XcodeSDKName = | "iphoneos" @@ -35,7 +40,7 @@ const XCODE_SDK_NAMES = { "arm64-apple-tvos-sim": "appletvsimulator", "arm64-apple-visionos": "xros", "arm64-apple-visionos-sim": "xrsimulator", -} satisfies Record; +} satisfies Record; type CMakeSystemName = "Darwin" | "iOS" | "tvOS" | "watchOS" | "visionOS"; @@ -50,7 +55,7 @@ const CMAKE_SYSTEM_NAMES = { "arm64-apple-tvos-sim": "tvOS", "arm64-apple-visionos": "visionOS", "arm64-apple-visionos-sim": "visionOS", -} satisfies Record; +} satisfies Record; type AppleArchitecture = "arm64" | "x86_64" | "arm64;x86_64"; @@ -65,7 +70,13 @@ export const APPLE_ARCHITECTURES = { "arm64-apple-tvos-sim": "arm64", "arm64-apple-visionos": "arm64", "arm64-apple-visionos-sim": "arm64", -} satisfies Record; +} satisfies Record; + +function tripletFromTarget(target: Target): AppleTriplet { + const result = target.replaceAll(/-reactnative$/g, "") as AppleTriplet; + assert(isAppleTriplet(result), `Invalid Apple triplet: ${target}`); + return result; +} export function createPlistContent(values: Record) { return [ @@ -100,35 +111,41 @@ export const platform: Platform = { id: "apple", name: "Apple", targets: [ - "arm64;x86_64-apple-darwin", - "arm64-apple-ios", - "arm64-apple-ios-sim", - "arm64-apple-tvos", - "arm64-apple-tvos-sim", - "arm64-apple-visionos", - "arm64-apple-visionos-sim", + "arm64;x86_64-apple-darwin-reactnative", + "arm64-apple-ios-reactnative", + "arm64-apple-ios-sim-reactnative", + "arm64-apple-tvos-reactnative", + "arm64-apple-tvos-sim-reactnative", + "arm64-apple-visionos-reactnative", + "arm64-apple-visionos-sim-reactnative", ], defaultTargets() { - return process.arch === "arm64" ? ["arm64-apple-ios-sim"] : []; + return process.arch === "arm64" ? ["arm64-apple-ios-sim-reactnative"] : []; }, amendCommand(command) { return command.addOption(xcframeworkExtensionOption); }, - configureArgs({ target }) { + configureArgs({ target }, { weakNodeApiLinkage }) { + const triplet = tripletFromTarget(target); return [ "-G", "Xcode", - "-D", - `CMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAMES[target]}`, - "-D", - `CMAKE_OSX_SYSROOT=${XCODE_SDK_NAMES[target]}`, - "-D", - `CMAKE_OSX_ARCHITECTURES=${APPLE_ARCHITECTURES[target]}`, + ...toDeclarationArguments({ + CMAKE_SYSTEM_NAME: CMAKE_SYSTEM_NAMES[triplet], + CMAKE_OSX_SYSROOT: XCODE_SDK_NAMES[triplet], + CMAKE_OSX_ARCHITECTURES: APPLE_ARCHITECTURES[triplet], + ...(weakNodeApiLinkage ? getWeakNodeApiVariables(triplet) : {}), + }), ]; }, - buildArgs() { - // We expect the final application to sign these binaries - return ["CODE_SIGNING_ALLOWED=NO"]; + buildArgs(context, { configuration }) { + return [ + "--config", + configuration, + "--", + // We expect the final application to sign these binaries + "CODE_SIGNING_ALLOWED=NO", + ]; }, isSupportedByHost: function (): boolean | Promise { return process.platform === "darwin"; diff --git a/packages/cmake-rn/src/platforms/node.ts b/packages/cmake-rn/src/platforms/node.ts new file mode 100644 index 00000000..d3272bd5 --- /dev/null +++ b/packages/cmake-rn/src/platforms/node.ts @@ -0,0 +1,147 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { oraPromise } from "ora"; +import chalk from "chalk"; + +import { + createNodeLibsDirectory, + determineNodeLibsFilename, + isNodeTriplet, + NodeTriplet, +} from "react-native-node-api"; + +import type { Platform } from "./types.js"; +import { toDeclarationArguments } from "../cmake.js"; +import { getNodeApiIncludeDirectories } from "../headers.js"; + +type Architecture = "arm64" | "x86_64" | "arm64;x86_64"; + +const ARCHITECTURES = { + "arm64-apple-darwin": "arm64", + "x86_64-apple-darwin": "x86_64", + "arm64;x86_64-apple-darwin": "arm64;x86_64", +} satisfies Record; + +type Target = `${NodeTriplet}-node`; + +type NodeOpts = Record; + +function tripletFromTarget(target: Target): NodeTriplet { + const result = target.replaceAll(/-node$/g, "") as NodeTriplet; + assert(isNodeTriplet(result), `Invalid Node triplet: ${target}`); + return result; +} + +function getLinkerFlags(target: NodeTriplet): string { + if ( + target === "arm64-apple-darwin" || + target === "x86_64-apple-darwin" || + target === "arm64;x86_64-apple-darwin" + ) { + return "-undefined dynamic_lookup"; + } else if ( + target === "linux" // TODO: Use the right triplet for Linux + ) { + return "-Wl,--unresolved-symbols=ignore-in-object-files"; + } else { + throw new Error( + `Determining linker flags for target ${target as string} is not implemented`, + ); + } +} + +export const platform: Platform = { + id: "nodejs", + name: "Node.js", + targets: [ + "arm64-apple-darwin-node", + "x86_64-apple-darwin-node", + "arm64;x86_64-apple-darwin-node", + ], + redundantTargets: ["arm64-apple-darwin-node", "x86_64-apple-darwin-node"], + defaultTargets() { + if (process.platform === "darwin") { + if (process.arch === "arm64") { + return ["arm64-apple-darwin-node"]; + } else if (process.arch === "x64") { + return ["x86_64-apple-darwin-node"]; + } + } + return []; + }, + amendCommand(command) { + return command; + }, + configureArgs({ target }, { configuration }) { + const triplet = tripletFromTarget(target); + return [ + "-G", + "Ninja", + ...toDeclarationArguments({ + CMAKE_BUILD_TYPE: configuration, + CMAKE_OSX_ARCHITECTURES: ARCHITECTURES[triplet], + // TODO: Make this names less "cmake-js" specific with an option to use the CMAKE_JS prefix + CMAKE_JS_INC: getNodeApiIncludeDirectories(), + CMAKE_SHARED_LINKER_FLAGS: getLinkerFlags(triplet), + }), + ]; + }, + buildArgs() { + // TODO: Include the arch in the build command + return []; + }, + isSupportedByHost() { + const { ANDROID_HOME } = process.env; + return typeof ANDROID_HOME === "string" && fs.existsSync(ANDROID_HOME); + }, + async postBuild({ outputPath, targets }, { autoLink }) { + // TODO: Include `configuration` in the output path + const libraryPathByTriplet = Object.fromEntries( + await Promise.all( + targets.map(async ({ target, outputPath }) => { + assert( + fs.existsSync(outputPath), + `Expected a directory at ${outputPath}`, + ); + // Expect binary file(s), either .node or .so + const dirents = await fs.promises.readdir(outputPath, { + withFileTypes: true, + }); + const result = dirents + .filter( + (dirent) => + dirent.isFile() && + (dirent.name.endsWith(".so") || + dirent.name.endsWith(".dylib") || + dirent.name.endsWith(".node")), + ) + .map((dirent) => path.join(dirent.parentPath, dirent.name)); + assert.equal(result.length, 1, "Expected exactly one library file"); + const triplet = tripletFromTarget(target); + return [triplet, result[0]] as const; + }), + ), + ) as Record; + const nodeLibsFilename = determineNodeLibsFilename( + Object.values(libraryPathByTriplet), + ); + const nodeLibsOutputPath = path.resolve(outputPath, nodeLibsFilename); + + await oraPromise( + createNodeLibsDirectory({ + outputPath: nodeLibsOutputPath, + libraryPathByTriplet, + autoLink, + }), + { + text: "Assembling Node.js libs directory", + successText: `Node.js libs directory assembled into ${chalk.dim( + path.relative(process.cwd(), nodeLibsOutputPath), + )}`, + failText: ({ message }) => + `Failed to assemble Node.js libs directory: ${message}`, + }, + ); + }, +}; diff --git a/packages/cmake-rn/src/platforms/types.ts b/packages/cmake-rn/src/platforms/types.ts index fb7c8f5f..7702ba07 100644 --- a/packages/cmake-rn/src/platforms/types.ts +++ b/packages/cmake-rn/src/platforms/types.ts @@ -37,6 +37,11 @@ export type Platform< * All the targets supported by this platform. */ targets: Readonly; + /** + * Targets which are redundant when building all supported targets. + * Ex. universal / multi-arch targets that are already produced by other targets. + */ + redundantTargets?: Readonly; /** * Get the limited subset of targets that should be built by default for this platform, to support a development workflow. */ diff --git a/packages/cmake-rn/src/weak-node-api.ts b/packages/cmake-rn/src/weak-node-api.ts index 496905d7..a18e932b 100644 --- a/packages/cmake-rn/src/weak-node-api.ts +++ b/packages/cmake-rn/src/weak-node-api.ts @@ -1,56 +1,17 @@ -import fs from "node:fs"; -import assert from "node:assert/strict"; import path from "node:path"; -import { - isAndroidTriplet, - isAppleTriplet, - SupportedTriplet, - weakNodeApiPath, -} from "react-native-node-api"; +import { getWeakNodeApiPath, SupportedTriplet } from "react-native-node-api"; -import { ANDROID_ARCHITECTURES } from "./platforms/android.js"; -import { getNodeAddonHeadersPath, getNodeApiHeadersPath } from "./headers.js"; +import { getNodeApiIncludeDirectories } from "./headers.js"; export function toCmakePath(input: string) { return input.split(path.win32.sep).join(path.posix.sep); } -export function getWeakNodeApiPath(triplet: SupportedTriplet): string { - if (isAppleTriplet(triplet)) { - const xcframeworkPath = path.join( - weakNodeApiPath, - "weak-node-api.xcframework", - ); - assert( - fs.existsSync(xcframeworkPath), - `Expected an XCFramework at ${xcframeworkPath}`, - ); - return xcframeworkPath; - } else if (isAndroidTriplet(triplet)) { - const libraryPath = path.join( - weakNodeApiPath, - "weak-node-api.android.node", - 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 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(";"), + // TODO: Make these names less "cmake-js" specific with an option to use the CMAKE_JS prefix + CMAKE_JS_INC: getNodeApiIncludeDirectories(), CMAKE_JS_LIB: getWeakNodeApiPath(triplet), }; } diff --git a/packages/host/.gitignore b/packages/host/.gitignore index b3f12e5b..5dc296ab 100644 --- a/packages/host/.gitignore +++ b/packages/host/.gitignore @@ -21,6 +21,7 @@ android/build/ /weak-node-api/build/ /weak-node-api/*.xcframework /weak-node-api/*.android.node +/weak-node-api/*.nodejs.node /weak-node-api/weak_node_api.cpp /weak-node-api/weak_node_api.hpp # Generated via `npm run generate-weak-node-api-injector` diff --git a/packages/host/src/node/index.ts b/packages/host/src/node/index.ts index 878eeb01..9070c609 100644 --- a/packages/host/src/node/index.ts +++ b/packages/host/src/node/index.ts @@ -2,12 +2,15 @@ export { SUPPORTED_TRIPLETS, ANDROID_TRIPLETS, APPLE_TRIPLETS, + NODE_TRIPLETS, type SupportedTriplet, type AndroidTriplet, type AppleTriplet, + type NodeTriplet, isSupportedTriplet, isAppleTriplet, isAndroidTriplet, + isNodeTriplet, } from "./prebuilds/triplets.js"; export { @@ -22,6 +25,11 @@ export { determineXCFrameworkFilename, } from "./prebuilds/apple.js"; +export { + createNodeLibsDirectory, + determineNodeLibsFilename, +} from "./prebuilds/node.js"; + export { determineLibraryBasename, prettyPath } from "./path-utils.js"; -export { weakNodeApiPath } from "./weak-node-api.js"; +export { weakNodeApiPath, getWeakNodeApiPath } from "./weak-node-api.js"; diff --git a/packages/host/src/node/prebuilds/android.ts b/packages/host/src/node/prebuilds/android.ts index b24b422b..dc602238 100644 --- a/packages/host/src/node/prebuilds/android.ts +++ b/packages/host/src/node/prebuilds/android.ts @@ -5,13 +5,6 @@ import path from "node:path"; import { AndroidTriplet } from "./triplets.js"; import { determineLibraryBasename } from "../path-utils.js"; -export const DEFAULT_ANDROID_TRIPLETS = [ - "aarch64-linux-android", - "armv7a-linux-androideabi", - "i686-linux-android", - "x86_64-linux-android", -] as const satisfies AndroidTriplet[]; - type AndroidArchitecture = "armeabi-v7a" | "arm64-v8a" | "x86" | "x86_64"; export const ANDROID_ARCHITECTURES = { @@ -44,12 +37,15 @@ export async function createAndroidLibsDirectory({ // 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, libraryPath] of Object.entries(libraryPathByTriplet)) { + for (const [triplet, libraryPath] of Object.entries(libraryPathByTriplet) as [ + AndroidTriplet, + string, + ][]) { assert( fs.existsSync(libraryPath), `Library not found: ${libraryPath} for triplet ${triplet}`, ); - const arch = ANDROID_ARCHITECTURES[triplet as AndroidTriplet]; + const arch = ANDROID_ARCHITECTURES[triplet]; const archOutputPath = path.join(outputPath, arch); await fs.promises.mkdir(archOutputPath, { recursive: true }); // Strip the ".node" extension from the library name diff --git a/packages/host/src/node/prebuilds/node.ts b/packages/host/src/node/prebuilds/node.ts new file mode 100644 index 00000000..4c7d2c15 --- /dev/null +++ b/packages/host/src/node/prebuilds/node.ts @@ -0,0 +1,70 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import { determineLibraryBasename } from "../path-utils.js"; +import { NodeTriplet } from "./triplets.js"; + +type OSArchName = + | `${typeof process.platform}-${typeof process.arch}` + | "darwin-arm64;x64"; + +const DIRECTORY_NAMES_PER_TARGET = { + "arm64;x86_64-apple-darwin": "darwin-arm64;x64", + "arm64-apple-darwin": "darwin-arm64", + "x86_64-apple-darwin": "darwin-x64", +} satisfies Record; + +/** + * 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 determineNodeLibsFilename(libraryPaths: string[]) { + const libraryName = determineLibraryBasename(libraryPaths); + return `${libraryName}.nodejs.node`; +} + +type NodeLibsDirectoryOptions = { + outputPath: string; + libraryPathByTriplet: Record; + autoLink: boolean; +}; + +export async function createNodeLibsDirectory({ + outputPath, + libraryPathByTriplet, + autoLink, +}: NodeLibsDirectoryOptions) { + // 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, libraryPath] of Object.entries(libraryPathByTriplet) as [ + NodeTriplet, + string, + ][]) { + assert( + fs.existsSync(libraryPath), + `Library not found: ${libraryPath} for triplet ${triplet}`, + ); + // Create the architecture-specific directory + const osArch = DIRECTORY_NAMES_PER_TARGET[triplet]; + const osArchOutputPath = path.join(outputPath, osArch); + await fs.promises.mkdir(osArchOutputPath, { recursive: true }); + // Strip any extension from the library name and rename it to .node + const libraryName = path + .basename(libraryPath) + .replaceAll(/\.so$|\.dylib$|\.node$/g, ""); + const nodeSuffixedName = `${libraryName}.node`; + const libraryOutputPath = path.join(osArchOutputPath, nodeSuffixedName); + 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", + ); + } + return outputPath; +} diff --git a/packages/host/src/node/prebuilds/triplets.ts b/packages/host/src/node/prebuilds/triplets.ts index 7471b15b..916115b0 100644 --- a/packages/host/src/node/prebuilds/triplets.ts +++ b/packages/host/src/node/prebuilds/triplets.ts @@ -25,9 +25,18 @@ export const APPLE_TRIPLETS = [ export type AppleTriplet = (typeof APPLE_TRIPLETS)[number]; +export const NODE_TRIPLETS = [ + "arm64;x86_64-apple-darwin", + "x86_64-apple-darwin", + "arm64-apple-darwin", +] as const; + +export type NodeTriplet = (typeof NODE_TRIPLETS)[number]; + export const SUPPORTED_TRIPLETS = [ ...APPLE_TRIPLETS, ...ANDROID_TRIPLETS, + ...NODE_TRIPLETS, ] as const; export type SupportedTriplet = (typeof SUPPORTED_TRIPLETS)[number]; @@ -49,3 +58,9 @@ export function isAppleTriplet( ): triplet is AppleTriplet { return (APPLE_TRIPLETS as readonly unknown[]).includes(triplet); } + +export function isNodeTriplet( + triplet: SupportedTriplet, +): triplet is NodeTriplet { + return (NODE_TRIPLETS as readonly unknown[]).includes(triplet); +} diff --git a/packages/host/src/node/weak-node-api.ts b/packages/host/src/node/weak-node-api.ts index 02e3befe..3b967da6 100644 --- a/packages/host/src/node/weak-node-api.ts +++ b/packages/host/src/node/weak-node-api.ts @@ -2,9 +2,41 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; +import { + isAndroidTriplet, + isAppleTriplet, + SupportedTriplet, +} from "./prebuilds/triplets"; +import { ANDROID_ARCHITECTURES } from "./prebuilds/android"; + export const weakNodeApiPath = path.resolve(__dirname, "../../weak-node-api"); assert( fs.existsSync(weakNodeApiPath), `Expected Weak Node API path to exist: ${weakNodeApiPath}`, ); + +export function getWeakNodeApiPath(triplet: SupportedTriplet): string { + if (isAppleTriplet(triplet)) { + const xcframeworkPath = path.join( + weakNodeApiPath, + "weak-node-api.xcframework", + ); + assert( + fs.existsSync(xcframeworkPath), + `Expected an XCFramework at ${xcframeworkPath}`, + ); + return xcframeworkPath; + } else if (isAndroidTriplet(triplet)) { + const libraryPath = path.join( + weakNodeApiPath, + "weak-node-api.android.node", + 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}`); + } +}