Skip to content
Draft
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
18 changes: 14 additions & 4 deletions docs/PREBUILDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/cmake-rn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
44 changes: 12 additions & 32 deletions packages/cmake-rn/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, unknown>)[platform.id]) {
for (const target of platform.targets) {
// Skip redundant targets
if (platform.redundantTargets?.includes(target)) {
continue;
}
targets.add(target);
}
}
Expand Down Expand Up @@ -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"));
}

Expand All @@ -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, "_"));
}

Expand All @@ -260,18 +263,7 @@ async function configureProject<T extends string>(
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",
Expand All @@ -281,7 +273,9 @@ async function configureProject<T extends string>(
"-B",
buildPath,
...platform.configureArgs(context, options),
...toDeclarationArguments(declarations),
...toDeclarationArguments({
CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath,
}),
],
{
outputMode: verbose ? "inherit" : "buffered",
Expand All @@ -296,29 +290,15 @@ async function buildProject<T extends string>(
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,
},
);
}

function toDeclarationArguments(declarations: Record<string, string>) {
return Object.entries(declarations).flatMap(([key, value]) => [
"-D",
`${key}=${value}`,
]);
}

export { program };
6 changes: 6 additions & 0 deletions packages/cmake-rn/src/cmake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function toDeclarationArguments(declarations: Record<string, string>) {
return Object.entries(declarations).flatMap(([key, value]) => [
"-D",
`${key}=${value}`,
]);
}
15 changes: 15 additions & 0 deletions packages/cmake-rn/src/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(";");
}
9 changes: 7 additions & 2 deletions packages/cmake-rn/src/platforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<P extends Platform>(
platform: P,
Expand Down
79 changes: 42 additions & 37 deletions packages/cmake-rn/src/platforms/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<Target, AndroidArchitecture>;
} satisfies Record<AndroidTriplet, AndroidArchitecture>;

const ndkVersionOption = new Option(
"--ndk-version <version>",
Expand All @@ -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<Target[], AndroidOpts> = {
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 [];
}
Expand All @@ -61,7 +72,10 @@ export const platform: Platform<Target[], AndroidOpts> = {
.addOption(ndkVersionOption)
.addOption(androidSdkVersionOption);
},
configureArgs({ target }, { ndkVersion, androidSdkVersion }) {
configureArgs(
{ target },
{ configuration, ndkVersion, androidSdkVersion, weakNodeApiLinkage },
) {
const { ANDROID_HOME } = process.env;
assert(
typeof ANDROID_HOME === "string",
Expand All @@ -82,38 +96,28 @@ export const platform: Platform<Target[], AndroidOpts> = {
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() {
Expand Down Expand Up @@ -144,10 +148,11 @@ export const platform: Platform<Target[], AndroidOpts> = {
)
.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<Target, string>;
) as Record<AndroidTriplet, string>;
const androidLibsFilename = determineAndroidLibsFilename(
Object.values(libraryPathByTriplet),
);
Expand Down
Loading
Loading