Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ jobs:
- run: npm run build
- run: npm run copy-node-api-headers --workspace react-native-node-api-modules
- run: npm run build-weak-node-api --workspace react-native-node-api-modules
- run: npm run generate-weak-node-api-injector --workspace react-native-node-api-modules
- run: npm test --workspace react-native-node-addon-examples
4 changes: 2 additions & 2 deletions packages/ferric-example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ Cargo.lock
/*.android.node/

# Generated files
/libferric_example.d.ts
/libferric_example.js
/ferric_example.d.ts
/ferric_example.js
4 changes: 2 additions & 2 deletions packages/ferric-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"url": "git+https://github.com/callstackincubator/react-native-node-api-modules.git",
"directory": "packages/ferric-example"
},
"main": "libferric_example.js",
"types": "libferric_example.d.ts",
"main": "ferric_example.js",
"types": "ferric_example.d.ts",
"scripts": {
"build": "ferric build --android --apple"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/ferric/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
determineXCFrameworkFilename,
createXCframework,
createUniversalAppleLibrary,
determineLibraryFilename,
determineLibraryBasename,
prettyPath,
} from "react-native-node-api-modules";

Expand Down Expand Up @@ -146,7 +146,7 @@ export const buildCommand = new Command("build")
}
);

const libraryName = determineLibraryFilename([
const libraryName = determineLibraryBasename([
...androidLibraries.map(([, outputPath]) => outputPath),
]);

Expand Down
43 changes: 36 additions & 7 deletions packages/ferric/src/cargo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
isAppleTarget,
} from "./targets.js";

const WEAK_NODE_API_PATH = new URL(
import.meta.resolve("react-native-node-api-modules/weak-node-api")
).pathname;

const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record<AppleTargetName, string> = {
"aarch64-apple-darwin": "macos-arm64_x86_64", // Universal
"x86_64-apple-darwin": "macos-arm64_x86_64", // Universal
Expand All @@ -28,6 +32,13 @@ const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record<AppleTargetName, string> = {
// "aarch64-apple-visionos-sim": "xros-arm64-simulator",
};

const ANDROID_ARCH_PR_TARGET: Record<AndroidTargetName, string> = {
"aarch64-linux-android": "arm64-v8a",
"armv7-linux-androideabi": "armeabi-v7a",
"i686-linux-android": "x86",
"x86_64-linux-android": "x86_64",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: What about making a type for the values to avoid typos? I'm sure that you listed all values already, so "arm64-v8a" | "armeabi-v7a" | "x86" | "x86_64" should do the trick, wdyt?

nit 2: Maybe we should pick a more general/unified way of describing targets / cpu archs? I'm afraid that adding new platforms will case an explosion of enums.

Copy link
Collaborator Author

@kraenhansen kraenhansen May 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And use that type instead of string in the Record above? I don't see that I will be referencing that type elsewhere 🤔 Where would you use that type? Perhaps a suggestive edit would help me understand your intent.

};

export function joinPathAndAssertExistence(...pathSegments: string[]) {
const joinedPath = path.join(...pathSegments);
assert(fs.existsSync(joinedPath), `Expected ${joinedPath} to exist`);
Expand Down Expand Up @@ -114,17 +125,23 @@ export function getTargetAndroidPlatform(target: AndroidTargetName) {
}

export function getWeakNodeApiFrameworkPath(target: AppleTargetName) {
const weakNodeApiPath = new URL(
import.meta.resolve("react-native-node-api-modules/weak-node-api")
).pathname;
assert(fs.existsSync(weakNodeApiPath), "Expected weak-node-api to exist");
assert(fs.existsSync(WEAK_NODE_API_PATH), "Expected weak-node-api to exist");
return joinPathAndAssertExistence(
weakNodeApiPath,
"libweak-node-api.xcframework",
WEAK_NODE_API_PATH,
"weak-node-api.xcframework",
APPLE_XCFRAMEWORK_CHILDS_PER_TARGET[target]
);
}

export function getWeakNodeApiAndroidLibraryPath(target: AndroidTargetName) {
assert(fs.existsSync(WEAK_NODE_API_PATH), "Expected weak-node-api to exist");
return joinPathAndAssertExistence(
WEAK_NODE_API_PATH,
"weak-node-api.android.node",
ANDROID_ARCH_PR_TARGET[target]
);
}

export function getTargetEnvironmentVariables({
target,
ndkVersion,
Expand All @@ -149,8 +166,15 @@ export function getTargetEnvironmentVariables({
const toolchainBinPath = getLLVMToolchainBinPath(ndkPath);
const targetArch = getTargetAndroidArch(target);
const targetPlatform = getTargetAndroidPlatform(target);
const weakNodeApiPath = getWeakNodeApiAndroidLibraryPath(target);

return {
CARGO_ENCODED_RUSTFLAGS: [
"-L",
weakNodeApiPath,
"-l",
"weak-node-api",
].join(String.fromCharCode(0x1f)),
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: joinPathAndAssertExistence(
toolchainBinPath,
`aarch64-linux-android${androidApiLevel}-clang`
Expand Down Expand Up @@ -186,7 +210,12 @@ export function getTargetEnvironmentVariables({
} else if (isAppleTarget(target)) {
const weakNodeApiFrameworkPath = getWeakNodeApiFrameworkPath(target);
return {
RUSTFLAGS: `-L framework=${weakNodeApiFrameworkPath} -l framework=libweak-node-api`,
CARGO_ENCODED_RUSTFLAGS: [
"-L",
`framework=${weakNodeApiFrameworkPath}`,
"-l",
"framework=weak-node-api",
].join(String.fromCharCode(0x1f)),
};
} else {
throw new Error(`Unexpected target: ${target}`);
Expand Down
109 changes: 60 additions & 49 deletions packages/react-native-node-api-cmake/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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 fs from "node:fs";
import { EventEmitter } from "node:events";

import { Command, Option } from "@commander-js/extra-typings";
Expand Down Expand Up @@ -109,7 +108,7 @@ export const program = new Command("react-native-node-api-cmake")
try {
const buildPath = getBuildPath(globalContext);
if (globalContext.clean) {
await fs.rm(buildPath, { recursive: true, force: true });
await fs.promises.rm(buildPath, { recursive: true, force: true });
}
const triplets = new Set<SupportedTriplet>(tripletValues);
if (globalContext.apple) {
Expand Down Expand Up @@ -162,7 +161,7 @@ export const program = new Command("react-native-node-api-cmake")
tripletContext.map(async (context) => {
// Delete any stale build artifacts before building
// This is important, since we might rename the output files
await fs.rm(context.tripletOutputPath, {
await fs.promises.rm(context.tripletOutputPath, {
recursive: true,
force: true,
});
Expand All @@ -181,32 +180,37 @@ export const program = new Command("react-native-node-api-cmake")
isAppleTriplet(triplet)
);
if (appleTriplets.length > 0) {
const libraryPaths = appleTriplets.flatMap(({ tripletOutputPath }) => {
const configSpecificPath = path.join(
tripletOutputPath,
globalContext.configuration
);
assert(
existsSync(configSpecificPath),
`Expected a directory at ${configSpecificPath}`
);
// Expect binary file(s), either .node or .dylib
return readdirSync(configSpecificPath).map((file) => {
const filePath = path.join(configSpecificPath, file);
if (filePath.endsWith(".dylib")) {
return filePath;
} else if (file.endsWith(".node")) {
// Rename the file to .dylib for xcodebuild to accept it
const newFilePath = filePath.replace(/\.node$/, ".dylib");
renameSync(filePath, newFilePath);
return newFilePath;
} else {
throw new Error(
`Expected a .node or .dylib file, but found ${file}`
);
}
});
});
const libraryPaths = await Promise.all(
appleTriplets.map(async ({ tripletOutputPath }) => {
const configSpecificPath = path.join(
tripletOutputPath,
globalContext.configuration
);
assert(
fs.existsSync(configSpecificPath),
`Expected a directory at ${configSpecificPath}`
);
// Expect binary file(s), either .node or .dylib
const files = await fs.promises.readdir(configSpecificPath);
const result = files.map(async (file) => {
const filePath = path.join(configSpecificPath, file);
if (filePath.endsWith(".dylib")) {
return filePath;
} else if (file.endsWith(".node")) {
// Rename the file to .dylib for xcodebuild to accept it
const newFilePath = filePath.replace(/\.node$/, ".dylib");
await fs.promises.rename(filePath, newFilePath);
return newFilePath;
} else {
throw new Error(
`Expected a .node or .dylib file, but found ${file}`
);
}
});
assert.equal(result.length, 1, "Expected exactly one library file");
return await result[0];
})
);
const frameworkPaths = libraryPaths.map(createAppleFramework);
const xcframeworkFilename =
determineXCFrameworkFilename(frameworkPaths);
Expand Down Expand Up @@ -239,25 +243,32 @@ export const program = new Command("react-native-node-api-cmake")
);
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;
})
await Promise.all(
androidTriplets.map(async ({ tripletOutputPath, triplet }) => {
assert(
fs.existsSync(tripletOutputPath),
`Expected a directory at ${tripletOutputPath}`
);
// Expect binary file(s), either .node or .so
const dirents = await fs.promises.readdir(tripletOutputPath, {
withFileTypes: true,
});
const result = dirents
.filter(
(dirent) =>
dirent.isFile() &&
(dirent.name.endsWith(".so") ||
dirent.name.endsWith(".node"))
)
.map((dirent) => path.join(dirent.parentPath, dirent.name));
assert.equal(
result.length,
1,
"Expected exactly one library file"
);
return [triplet, result[0]] as const;
})
)
) as Record<AndroidTriplet, string>;
const androidLibsFilename = determineAndroidLibsFilename(
Object.values(libraryPathByTriplet)
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native-node-api-cmake/src/weak-node-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string {
);
assert(fs.existsSync(pathname), "Weak Node API path does not exist");
if (isAppleTriplet(triplet)) {
const xcframeworkPath = path.join(pathname, "libweak-node-api.xcframework");
const xcframeworkPath = path.join(pathname, "weak-node-api.xcframework");
assert(
fs.existsSync(xcframeworkPath),
`Expected an XCFramework at ${xcframeworkPath}`
Expand All @@ -26,7 +26,7 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string {
} else if (isAndroidTriplet(triplet)) {
const libraryPath = path.join(
pathname,
"libweak-node-api.android.node",
"weak-node-api.android.node",
ANDROID_ARCHITECTURES[triplet],
"libweak-node-api.so"
);
Expand Down
6 changes: 5 additions & 1 deletion packages/react-native-node-api-modules/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ android/.cxx/
android/build/

# Everything in weak-node-api is generated, except for the configurations
# Generated and built bia `npm run build-weak-node-api-injector`
/weak-node-api/build/
/weak-node-api/*.xcframework
/weak-node-api/*.android.node
/weak-node-api/weak-node-api.cpp
/weak-node-api/weak_node_api.cpp
/weak-node-api/weak_node_api.hpp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: /weak-node-api/weak_node_api.[ch]pp if you like one-liners

# Generated via `npm run generate-weak-node-api-injector`
/cpp/WeakNodeApiInjector.cpp
32 changes: 16 additions & 16 deletions packages/react-native-node-api-modules/android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,35 @@ cmake_minimum_required(VERSION 3.13)
project(react-native-node-api-modules)
set(CMAKE_CXX_STANDARD 20)

find_package(ReactAndroid REQUIRED CONFIG)
find_package(hermes-engine REQUIRED CONFIG)

add_library(weak-node-api SHARED IMPORTED)
set_target_properties(weak-node-api PROPERTIES
IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/../weak-node-api/weak-node-api.android.node/${ANDROID_ABI}/libweak-node-api.so"
)
target_include_directories(weak-node-api INTERFACE
../weak-node-api
../weak-node-api/include
)

add_library(node-api-host SHARED
src/main/cpp/OnLoad.cpp
../cpp/Logger.cpp
../cpp/CxxNodeApiHostModule.cpp
../cpp/WeakNodeApiInjector.cpp
)

target_include_directories(node-api-host PRIVATE
../cpp
../include
)

find_package(ReactAndroid REQUIRED CONFIG)
find_package(hermes-engine REQUIRED CONFIG)

target_link_libraries(node-api-host
# android
log
ReactAndroid::reactnative
ReactAndroid::jsi
hermes-engine::libhermes
weak-node-api
# react_codegen_NodeApiHostSpec
)

add_subdirectory(../weak-node-api weak-node-api)

target_compile_definitions(weak-node-api
PRIVATE
# NAPI_VERSION=8
NODE_API_REEXPORT=1
)
target_link_libraries(weak-node-api
node-api-host
hermes-engine::libhermes
)
8 changes: 1 addition & 7 deletions packages/react-native-node-api-modules/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ android {

externalNativeBuild {
cmake {
targets "node-api-host", "weak-node-api"
targets "node-api-host"
cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
arguments "-DANDROID_STL=c++_shared"
abiFilters (*reactNativeArchitectures())
Expand Down Expand Up @@ -117,12 +117,6 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

packagingOptions {
// TODO: I don't think this works - I needed to add this in the react-native package too.
doNotStrip "**/libhermes.so"
}

}

repositories {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
#include <ReactCommon/CxxTurboModuleUtils.h>

#include <CxxNodeApiHostModule.hpp>
#include <WeakNodeApiInjector.hpp>

// Called when the library is loaded
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
callstack::nodeapihost::injectIntoWeakNodeApi();
// Register the C++ TurboModule
facebook::react::registerCxxModuleToGlobalModuleMap(
callstack::nodeapihost::CxxNodeApiHostModule::kModuleName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import java.util.HashMap
class NodeApiModulesPackage : BaseReactPackage() {
init {
SoLoader.loadLibrary("node-api-host")
SoLoader.loadLibrary("weak-node-api")
}

override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
Expand Down
Loading