diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 56ed8951..88939b6f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -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 diff --git a/packages/ferric-example/.gitignore b/packages/ferric-example/.gitignore index a08b424c..c73e3a58 100644 --- a/packages/ferric-example/.gitignore +++ b/packages/ferric-example/.gitignore @@ -5,5 +5,5 @@ Cargo.lock /*.android.node/ # Generated files -/libferric_example.d.ts -/libferric_example.js +/ferric_example.d.ts +/ferric_example.js diff --git a/packages/ferric-example/package.json b/packages/ferric-example/package.json index 70eaf152..d4fcbd0d 100644 --- a/packages/ferric-example/package.json +++ b/packages/ferric-example/package.json @@ -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" }, diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 80084228..739c049e 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -14,7 +14,7 @@ import { determineXCFrameworkFilename, createXCframework, createUniversalAppleLibrary, - determineLibraryFilename, + determineLibraryBasename, prettyPath, } from "react-native-node-api-modules"; @@ -146,7 +146,7 @@ export const buildCommand = new Command("build") } ); - const libraryName = determineLibraryFilename([ + const libraryName = determineLibraryBasename([ ...androidLibraries.map(([, outputPath]) => outputPath), ]); diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index 38cbf159..e227a849 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -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 = { "aarch64-apple-darwin": "macos-arm64_x86_64", // Universal "x86_64-apple-darwin": "macos-arm64_x86_64", // Universal @@ -28,6 +32,13 @@ const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { // "aarch64-apple-visionos-sim": "xros-arm64-simulator", }; +const ANDROID_ARCH_PR_TARGET: Record = { + "aarch64-linux-android": "arm64-v8a", + "armv7-linux-androideabi": "armeabi-v7a", + "i686-linux-android": "x86", + "x86_64-linux-android": "x86_64", +}; + export function joinPathAndAssertExistence(...pathSegments: string[]) { const joinedPath = path.join(...pathSegments); assert(fs.existsSync(joinedPath), `Expected ${joinedPath} to exist`); @@ -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, @@ -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` @@ -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}`); diff --git a/packages/react-native-node-api-cmake/src/cli.ts b/packages/react-native-node-api-cmake/src/cli.ts index 46ce29fa..5a58b736 100644 --- a/packages/react-native-node-api-cmake/src/cli.ts +++ b/packages/react-native-node-api-cmake/src/cli.ts @@ -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"; @@ -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(tripletValues); if (globalContext.apple) { @@ -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, }); @@ -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); @@ -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; const androidLibsFilename = determineAndroidLibsFilename( Object.values(libraryPathByTriplet) 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 index 7d831137..42d97507 100644 --- a/packages/react-native-node-api-cmake/src/weak-node-api.ts +++ b/packages/react-native-node-api-cmake/src/weak-node-api.ts @@ -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}` @@ -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" ); diff --git a/packages/react-native-node-api-modules/.gitignore b/packages/react-native-node-api-modules/.gitignore index 1759ce44..b3f12e5b 100644 --- a/packages/react-native-node-api-modules/.gitignore +++ b/packages/react-native-node-api-modules/.gitignore @@ -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 +# Generated via `npm run generate-weak-node-api-injector` +/cpp/WeakNodeApiInjector.cpp diff --git a/packages/react-native-node-api-modules/android/CMakeLists.txt b/packages/react-native-node-api-modules/android/CMakeLists.txt index fa535e0c..6b4d2a5f 100644 --- a/packages/react-native-node-api-modules/android/CMakeLists.txt +++ b/packages/react-native-node-api-modules/android/CMakeLists.txt @@ -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 -) diff --git a/packages/react-native-node-api-modules/android/build.gradle b/packages/react-native-node-api-modules/android/build.gradle index 6201a2df..5c04e7d3 100644 --- a/packages/react-native-node-api-modules/android/build.gradle +++ b/packages/react-native-node-api-modules/android/build.gradle @@ -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()) @@ -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 { diff --git a/packages/react-native-node-api-modules/android/src/main/cpp/OnLoad.cpp b/packages/react-native-node-api-modules/android/src/main/cpp/OnLoad.cpp index 31717497..35e27128 100644 --- a/packages/react-native-node-api-modules/android/src/main/cpp/OnLoad.cpp +++ b/packages/react-native-node-api-modules/android/src/main/cpp/OnLoad.cpp @@ -3,9 +3,11 @@ #include #include +#include // 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, diff --git a/packages/react-native-node-api-modules/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt b/packages/react-native-node-api-modules/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt index 89a38313..105d347e 100644 --- a/packages/react-native-node-api-modules/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt +++ b/packages/react-native-node-api-modules/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt @@ -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? { diff --git a/packages/react-native-node-api-modules/cpp/AddonLoaders.hpp b/packages/react-native-node-api-modules/cpp/AddonLoaders.hpp index de1e4a98..d0d5d269 100644 --- a/packages/react-native-node-api-modules/cpp/AddonLoaders.hpp +++ b/packages/react-native-node-api-modules/cpp/AddonLoaders.hpp @@ -1,20 +1,25 @@ #pragma once +#include "Logger.hpp" + #include #if defined(__APPLE__) || defined(__ANDROID__) #include #include +using callstack::nodeapihost::log_debug; + struct PosixLoader { using Module = void *; using Symbol = void *; static Module loadLibrary(const char *filePath) { assert(NULL != filePath); + Module result = dlopen(filePath, RTLD_NOW | RTLD_LOCAL); if (NULL == result) { - fprintf(stderr, "NapiHost: Failed to load library '%s': %s", filePath, - dlerror()); + log_debug("NapiHost: Failed to load library '%s': %s", filePath, + dlerror()); } return result; } diff --git a/packages/react-native-node-api-modules/cpp/CxxNodeApiHostModule.cpp b/packages/react-native-node-api-modules/cpp/CxxNodeApiHostModule.cpp index f9697709..af448026 100644 --- a/packages/react-native-node-api-modules/cpp/CxxNodeApiHostModule.cpp +++ b/packages/react-native-node-api-modules/cpp/CxxNodeApiHostModule.cpp @@ -1,4 +1,5 @@ #include "CxxNodeApiHostModule.hpp" +#include "Logger.hpp" using namespace facebook; @@ -58,10 +59,14 @@ bool CxxNodeApiHostModule::loadNodeAddon(NodeAddon &addon, abort() #endif + log_debug("[%s] Loading addon by '%s'", libraryName.c_str(), + libraryPath.c_str()); + typename LoaderPolicy::Symbol initFn = NULL; typename LoaderPolicy::Module library = LoaderPolicy::loadLibrary(libraryPath.c_str()); if (NULL != library) { + log_debug("[%s] Loaded addon", libraryName.c_str()); addon.moduleHandle = library; // Generate a name allowing us to reference the exports object from JSI @@ -73,12 +78,20 @@ bool CxxNodeApiHostModule::loadNodeAddon(NodeAddon &addon, initFn = LoaderPolicy::getSymbol(library, "napi_register_module_v1"); if (NULL != initFn) { + log_debug("[%s] Found napi_register_module_v1 (%p)", libraryName.c_str(), + initFn); addon.init = (napi_addon_register_func)initFn; + } else { + log_debug("[%s] Failed to find napi_register_module_v1. Expecting the " + "addon to call napi_module_register to register itself.", + libraryName.c_str()); } // TODO: Read "node_api_module_get_api_version_v1" to support the addon // declaring its Node-API version // @see // https://github.com/callstackincubator/react-native-node-api-modules/issues/4 + } else { + log_debug("[%s] Failed to load library", libraryName.c_str()); } return NULL != initFn; } diff --git a/packages/react-native-node-api-modules/cpp/Logger.cpp b/packages/react-native-node-api-modules/cpp/Logger.cpp new file mode 100644 index 00000000..44233f43 --- /dev/null +++ b/packages/react-native-node-api-modules/cpp/Logger.cpp @@ -0,0 +1,35 @@ +#include "Logger.hpp" +#include +#include + +#if defined(__ANDROID__) +#include +#define LOG_TAG "NodeApiHost" +#elif defined(__APPLE__) +#include +#endif + +namespace callstack::nodeapihost { +void log_debug(const char *format, ...) { + // TODO: Disable logging in release builds + + va_list args; + va_start(args, format); + +#if defined(__ANDROID__) + __android_log_vprint(ANDROID_LOG_DEBUG, LOG_TAG, format, args); +#elif defined(__APPLE__) + // iOS or macOS + fprintf(stderr, "[NodeApiHost] "); + vfprintf(stderr, format, args); + fprintf(stderr, "\n"); +#else + // Fallback for other platforms + fprintf(stderr, "[NodeApiHost] "); + vfprintf(stdout, format, args); + fprintf(stdout, "\n"); +#endif + + va_end(args); +} +} // namespace callstack::nodeapihost \ No newline at end of file diff --git a/packages/react-native-node-api-modules/cpp/Logger.hpp b/packages/react-native-node-api-modules/cpp/Logger.hpp new file mode 100644 index 00000000..af373f31 --- /dev/null +++ b/packages/react-native-node-api-modules/cpp/Logger.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace callstack::nodeapihost { +void log_debug(const char *format, ...); +} diff --git a/packages/react-native-node-api-modules/cpp/WeakNodeApiInjector.hpp b/packages/react-native-node-api-modules/cpp/WeakNodeApiInjector.hpp new file mode 100644 index 00000000..1b0a718d --- /dev/null +++ b/packages/react-native-node-api-modules/cpp/WeakNodeApiInjector.hpp @@ -0,0 +1,5 @@ +#include + +namespace callstack::nodeapihost { +void injectIntoWeakNodeApi(); +} diff --git a/packages/react-native-node-api-modules/ios/NodeApiHostModuleProvider.mm b/packages/react-native-node-api-modules/ios/NodeApiHostModuleProvider.mm index d2c7117f..d4ecd94f 100644 --- a/packages/react-native-node-api-modules/ios/NodeApiHostModuleProvider.mm +++ b/packages/react-native-node-api-modules/ios/NodeApiHostModuleProvider.mm @@ -1,4 +1,5 @@ #import "CxxNodeApiHostModule.hpp" +#import "WeakNodeApiInjector.hpp" #define USE_CXX_TURBO_MODULE_UTILS 0 #if defined(__has_include) @@ -21,6 +22,8 @@ @interface NodeApiHost : NSObject @implementation NodeApiHost #if USE_CXX_TURBO_MODULE_UTILS + (void)load { + callstack::nodeapihost::injectIntoWeakNodeApi(); + facebook::react::registerCxxModuleToGlobalModuleMap( callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, [](std::shared_ptr jsInvoker) { diff --git a/packages/react-native-node-api-modules/package.json b/packages/react-native-node-api-modules/package.json index 99a2b20e..8bdc38f2 100644 --- a/packages/react-native-node-api-modules/package.json +++ b/packages/react-native-node-api-modules/package.json @@ -41,6 +41,7 @@ "build": "tsc --build", "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", "generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts", + "generate-weak-node-api-injector": "tsx scripts/generate-weak-node-api-injector.ts", "build-weak-node-api": "npm run generate-weak-node-api && react-native-node-api-cmake --android --apple --no-auto-link --no-weak-node-api-linkage --source ./weak-node-api", "test": "tsx --test src/node/**/*.test.ts src/node/*.test.ts" }, diff --git a/packages/react-native-node-api-modules/react-native-node-api-modules.podspec b/packages/react-native-node-api-modules/react-native-node-api-modules.podspec index 7ab81982..618b4346 100644 --- a/packages/react-native-node-api-modules/react-native-node-api-modules.podspec +++ b/packages/react-native-node-api-modules/react-native-node-api-modules.podspec @@ -29,10 +29,10 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/callstackincubator/react-native-node-api-modules.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "include/*.h" - s.public_header_files = "include/*.h" + s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "weak-node-api/include/*.h", "weak-node-api/*.hpp" + s.public_header_files = "weak-node-api/include/*.h" - s.vendored_frameworks = "auto-linked/apple/*.xcframework", "weak-node-api/libweak-node-api.xcframework" + s.vendored_frameworks = "auto-linked/apple/*.xcframework", "weak-node-api/weak-node-api.xcframework" s.script_phase = { :name => 'Copy Node-API xcframeworks', :execution_position => :before_compile, diff --git a/packages/react-native-node-api-modules/scripts/copy-node-api-headers.ts b/packages/react-native-node-api-modules/scripts/copy-node-api-headers.ts index fef957a2..9632627e 100644 --- a/packages/react-native-node-api-modules/scripts/copy-node-api-headers.ts +++ b/packages/react-native-node-api-modules/scripts/copy-node-api-headers.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { include_dir as includeSourcePath } from "node-api-headers"; -const includeDestinationPath = path.join(__dirname, "../include"); +const includeDestinationPath = path.join(__dirname, "../weak-node-api/include"); assert(fs.existsSync(includeSourcePath), `Expected ${includeSourcePath}`); console.log(`Copying ${includeSourcePath} to ${includeDestinationPath}`); fs.cpSync(includeSourcePath, includeDestinationPath, { recursive: true }); diff --git a/packages/react-native-node-api-modules/scripts/generate-weak-node-api-injector.ts b/packages/react-native-node-api-modules/scripts/generate-weak-node-api-injector.ts new file mode 100644 index 00000000..0d3a61a8 --- /dev/null +++ b/packages/react-native-node-api-modules/scripts/generate-weak-node-api-injector.ts @@ -0,0 +1,67 @@ +import fs from "node:fs"; +import path from "node:path"; +import cp from "node:child_process"; + +import { FunctionDecl, getNodeApiFunctions } from "./node-api-functions"; + +export const CPP_SOURCE_PATH = path.join(__dirname, "../cpp"); + +/** + * Generates source code which injects the Node API functions from the host. + */ +export function generateSource(functions: FunctionDecl[]) { + return ` + // This file is generated by react-native-node-api-modules + #include + #include + #include + + #if defined(__APPLE__) + #define WEAK_NODE_API_LIBRARY_NAME "@rpath/weak-node-api.framework/weak-node-api" + #elif defined(__ANDROID__) + #define WEAK_NODE_API_LIBRARY_NAME "libweak-node-api.so" + #else + #error "WEAK_NODE_API_LIBRARY_NAME cannot be defined for this platform" + #endif + + namespace callstack::nodeapihost { + + void injectIntoWeakNodeApi() { + void *module = dlopen(WEAK_NODE_API_LIBRARY_NAME, RTLD_NOW | RTLD_LOCAL); + if (nullptr == module) { + log_debug("NapiHost: Failed to load weak-node-api: %s", dlerror()); + abort(); + } + + auto inject_weak_node_api_host = (InjectHostFunction)dlsym( + module, "inject_weak_node_api_host"); + if (nullptr == inject_weak_node_api_host) { + log_debug("NapiHost: Failed to find 'inject_weak_node_api_host' function: %s", dlerror()); + abort(); + } + + log_debug("Injecting WeakNodeApiHost"); + inject_weak_node_api_host(WeakNodeApiHost { + ${functions + .filter(({ kind }) => kind === "engine") + .flatMap(({ name }) => `.${name} = ${name},`) + .join("\n")} + }); + } + } // namespace callstack::nodeapihost + `; +} + +async function run() { + const nodeApiFunctions = getNodeApiFunctions(); + + const source = generateSource(nodeApiFunctions); + const sourcePath = path.join(CPP_SOURCE_PATH, "WeakNodeApiInjector.cpp"); + await fs.promises.writeFile(sourcePath, source, "utf-8"); + cp.spawnSync("clang-format", ["-i", sourcePath], { stdio: "inherit" }); +} + +run().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/packages/react-native-node-api-modules/scripts/generate-weak-node-api.ts b/packages/react-native-node-api-modules/scripts/generate-weak-node-api.ts index 1eb810f3..27be13db 100644 --- a/packages/react-native-node-api-modules/scripts/generate-weak-node-api.ts +++ b/packages/react-native-node-api-modules/scripts/generate-weak-node-api.ts @@ -1,222 +1,85 @@ -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"; +import { FunctionDecl, getNodeApiFunctions } from "./node-api-functions"; export const WEAK_NODE_API_PATH = path.join(__dirname, "../weak-node-api"); -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); -} - -type FunctionDecl = { - name: string; - returnType: string; - argumentTypes: string[]; - libraryPath: string; - fallbackReturnStatement: string; -}; - -export function generateNodeApiFunctionStubBody({ - name, - returnType, - argumentTypes, - libraryPath, - fallbackReturnStatement, -}: FunctionDecl) { - const returnStatement = - name === "napi_fatal_error" - ? "abort();" - : returnType === "void" - ? "" - : `return real_func(${argumentTypes - .map((t, index) => `arg${index}`) - .join(", ")}); // Call the real function`; - return ` - static ${name}_t real_func = NULL; - - if (!real_func) { - void* handle = dlopen("${libraryPath}", RTLD_LAZY | RTLD_GLOBAL); - if (!handle) { - fprintf(stderr, "Failed to load ${libraryPath} while deferring ${name}: %s\\n", dlerror()); - ${fallbackReturnStatement} - } - - real_func = (${name}_t)dlsym(handle, "${name}"); - if (!real_func) { - fprintf(stderr, "Failed to find symbol while deferring ${name}: %s\\n", dlerror()); - ${fallbackReturnStatement} - } - } - - ${returnStatement} - `; -} - -export function generateNodeApiFunction(decl: FunctionDecl) { - const { name, returnType, argumentTypes, fallbackReturnStatement } = decl; - return ` -typedef ${returnType} (*${name}_t)(${argumentTypes.join(", ")}); -${returnType} ${name}(${argumentTypes - .map((type, index) => `${type} arg${index}`) - .join(", ")}) { - fprintf(stdout, "Calling ${name} [weak-node-api]\\n"); - #ifdef NODE_API_REEXPORT - ${generateNodeApiFunctionStubBody(decl)} - #else - fprintf(stderr, "Returning generic error for ${name}\\n"); - ${fallbackReturnStatement} - #endif -}`; +export function generateHeader(functions: FunctionDecl[]) { + return [ + "// This file is generated by react-native-node-api-modules", + "#include ", // Node-API + "#include ", // fprintf() + "#include ", // abort() + // Generate the struct of function pointers + "struct WeakNodeApiHost {", + ...functions.map( + ({ returnType, noReturn, name, argumentTypes }) => + `${returnType} ${ + noReturn ? " __attribute__((noreturn))" : "" + }(*${name})(${argumentTypes.join(", ")});` + ), + "};", + "typedef void(*InjectHostFunction)(const WeakNodeApiHost&);", + `extern "C" void inject_weak_node_api_host(const WeakNodeApiHost& host);`, + ].join("\n"); } /** * Generates source code for a version script for the given Node API version. - * @param version */ -export function generateFakeNodeApiSource(version: NodeApiVersion) { - const lines = [ +export function generateSource(functions: FunctionDecl[]) { + return [ "// This file is generated by react-native-node-api-modules", - "#include ", // Node-API - "#include ", // dlopen(), dlsym() - "#include ", // fprintf() - "#include ", // abort() - ]; - const root = getNodeApiHeaderAST(version); - assert.equal(root.kind, "TranslationUnitDecl"); - assert(Array.isArray(root.inner)); - const foundSymbols = new Set(); - - const symbolsPerInterface = symbols[version]; - const engineSymbols = new Set(symbolsPerInterface.js_native_api_symbols); - const runtimeSymbols = new Set(symbolsPerInterface.node_api_symbols); - const allSymbols = new Set([...engineSymbols, ...runtimeSymbols]); - - for (const node of root.inner) { - const { name, kind } = node; - if (kind === "FunctionDecl" && name && allSymbols.has(name)) { - assert(name, "Expected a name"); - foundSymbols.add(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 ${argumentTypes}` - ); - assert( - returnType === "napi_status" || returnType === "void", - `Expected return type to be napi_status, got ${returnType}` - ); - - lines.push( - generateNodeApiFunction({ - name, - returnType, - argumentTypes: argumentTypes.split(",").map((arg) => arg.trim()), - // Defer to the right library - libraryPath: engineSymbols.has(name) - ? "libhermes.so" - : "libnode-api-host.so", - fallbackReturnStatement: - returnType === "void" - ? "abort();" - : "return napi_status::napi_generic_failure;", - }) - ); - } - } - for (const knownSymbol of allSymbols) { - if (!foundSymbols.has(knownSymbol)) { - throw new Error( - `Missing symbol '${knownSymbol}' in the AST for Node API ${version}` - ); - } - } - return lines.join("\n"); + `#include "weak_node_api.hpp"`, // Generated header + // Generate the struct of function pointers + "WeakNodeApiHost g_host;", + "void inject_weak_node_api_host(const WeakNodeApiHost& host) {", + " g_host = host;", + "};", + ``, + // Generate function calling into the host + ...functions.flatMap(({ returnType, noReturn, name, argumentTypes }) => { + return [ + `extern "C" ${returnType} ${ + noReturn ? " __attribute__((noreturn))" : "" + }${name}(${argumentTypes + .map((type, index) => `${type} arg${index}`) + .join(", ")}) {`, + `if (g_host.${name} == nullptr) {`, + ` fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n");`, + " abort();", + "}", + (returnType === "void" ? "" : "return ") + + "g_host." + + name + + "(" + + argumentTypes.map((_, index) => `arg${index}`).join(", ") + + ");", + "};", + ]; + }), + ].join("\n"); } 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" - ); + + const nodeApiFunctions = getNodeApiFunctions(); + + const header = generateHeader(nodeApiFunctions); + const headerPath = path.join(WEAK_NODE_API_PATH, "weak_node_api.hpp"); + await fs.promises.writeFile(headerPath, header, "utf-8"); + cp.spawnSync("clang-format", ["-i", headerPath], { stdio: "inherit" }); + + const source = generateSource(nodeApiFunctions); + const sourcePath = path.join(WEAK_NODE_API_PATH, "weak_node_api.cpp"); + await fs.promises.writeFile(sourcePath, source, "utf-8"); + cp.spawnSync("clang-format", ["-i", sourcePath], { stdio: "inherit" }); } run().catch((err) => { diff --git a/packages/react-native-node-api-modules/scripts/node-api-functions.ts b/packages/react-native-node-api-modules/scripts/node-api-functions.ts new file mode 100644 index 00000000..ad532ae1 --- /dev/null +++ b/packages/react-native-node-api-modules/scripts/node-api-functions.ts @@ -0,0 +1,137 @@ +import assert from "node:assert/strict"; +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"; + +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); +} + +export type FunctionDecl = { + name: string; + kind: "engine" | "runtime"; + returnType: string; + noReturn: boolean; + argumentTypes: string[]; + libraryPath: string; + fallbackReturnStatement: string; +}; + +export function getNodeApiFunctions(version: NodeApiVersion = "v8") { + const root = getNodeApiHeaderAST(version); + assert.equal(root.kind, "TranslationUnitDecl"); + assert(Array.isArray(root.inner)); + const foundSymbols = new Set(); + + const symbolsPerInterface = symbols[version]; + const engineSymbols = new Set(symbolsPerInterface.js_native_api_symbols); + const runtimeSymbols = new Set(symbolsPerInterface.node_api_symbols); + const allSymbols = new Set([...engineSymbols, ...runtimeSymbols]); + + const nodeApiFunctions: FunctionDecl[] = []; + + for (const node of root.inner) { + const { name, kind } = node; + if (kind === "FunctionDecl" && name && allSymbols.has(name)) { + assert(name, "Expected a name"); + foundSymbols.add(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 ${argumentTypes}` + ); + assert( + returnType === "napi_status" || returnType === "void", + `Expected return type to be napi_status, got ${returnType}` + ); + + nodeApiFunctions.push({ + name, + returnType, + noReturn: node.type.qualType.includes("__attribute__((noreturn))"), + kind: engineSymbols.has(name) ? "engine" : "runtime", + argumentTypes: argumentTypes + .split(",") + .map((arg) => arg.trim().replace("_Bool", "bool")), + // Defer to the right library + libraryPath: engineSymbols.has(name) + ? "libhermes.so" + : "libnode-api-host.so", + fallbackReturnStatement: + returnType === "void" + ? "abort();" + : "return napi_status::napi_generic_failure;", + }); + } + } + for (const knownSymbol of allSymbols) { + if (!foundSymbols.has(knownSymbol)) { + throw new Error( + `Missing symbol '${knownSymbol}' in the AST for Node API ${version}` + ); + } + } + + return nodeApiFunctions; +} diff --git a/packages/react-native-node-api-modules/src/node/babel-plugin/plugin.ts b/packages/react-native-node-api-modules/src/node/babel-plugin/plugin.ts index 1644fd6d..2431af69 100644 --- a/packages/react-native-node-api-modules/src/node/babel-plugin/plugin.ts +++ b/packages/react-native-node-api-modules/src/node/babel-plugin/plugin.ts @@ -4,12 +4,7 @@ import path from "node:path"; import type { PluginObj, NodePath } from "@babel/core"; import * as t from "@babel/types"; -import { - getLibraryName, - isNodeApiModule, - replaceWithNodeExtension, - NamingStrategy, -} from "../path-utils"; +import { getLibraryName, isNodeApiModule, NamingStrategy } from "../path-utils"; type PluginOptions = { stripPathSuffix?: boolean; @@ -30,10 +25,7 @@ export function replaceWithRequireNodeAddon( modulePath: string, naming: NamingStrategy ) { - const requireCallArgument = getLibraryName( - replaceWithNodeExtension(modulePath), - naming - ); + const requireCallArgument = getLibraryName(modulePath, naming); p.replaceWith( t.callExpression( t.memberExpression( diff --git a/packages/react-native-node-api-modules/src/node/index.ts b/packages/react-native-node-api-modules/src/node/index.ts index 7b4ee6e1..4a9c0207 100644 --- a/packages/react-native-node-api-modules/src/node/index.ts +++ b/packages/react-native-node-api-modules/src/node/index.ts @@ -21,4 +21,4 @@ export { determineXCFrameworkFilename, } from "./prebuilds/apple.js"; -export { determineLibraryFilename, prettyPath } from "./path-utils.js"; +export { determineLibraryBasename, prettyPath } from "./path-utils.js"; diff --git a/packages/react-native-node-api-modules/src/node/path-utils.test.ts b/packages/react-native-node-api-modules/src/node/path-utils.test.ts index f7c63b3d..6ead343e 100644 --- a/packages/react-native-node-api-modules/src/node/path-utils.test.ts +++ b/packages/react-native-node-api-modules/src/node/path-utils.test.ts @@ -49,7 +49,10 @@ describe("isNodeApiModule", () => { // remove read permission on file fs.chmodSync(candidate, 0); try { - assert.throws(() => isNodeApiModule(path.join(tempDirectoryPath, "addon")), /Found an unreadable module addon\.android\.node/); + assert.throws( + () => isNodeApiModule(path.join(tempDirectoryPath, "addon")), + /Found an unreadable module addon\.android\.node/ + ); } finally { fs.chmodSync(candidate, 0o600); } @@ -81,7 +84,10 @@ describe("isNodeApiModule", () => { const unreadable = path.join(tempDirectoryPath, "addon.android.node"); // only android module is unreadable fs.chmodSync(unreadable, 0); - assert.throws(() => isNodeApiModule(path.join(tempDirectoryPath, "addon")), /Found an unreadable module addon\.android\.node/); + assert.throws( + () => isNodeApiModule(path.join(tempDirectoryPath, "addon")), + /Found an unreadable module addon\.android\.node/ + ); fs.chmodSync(unreadable, 0o600); }); }); @@ -108,12 +114,9 @@ describe("replaceExtensionWithNode", () => { }); describe("determineModuleContext", () => { - it("works", (context) => { + it("strips the file extension", (context) => { const tempDirectoryPath = setupTempDirectory(context, { "package.json": `{ "name": "my-package" }`, - // Two sub-packages with the same name - "sub-package-a/package.json": `{ "name": "my-sub-package" }`, - "sub-package-b/package.json": `{ "name": "my-sub-package" }`, }); { @@ -123,12 +126,35 @@ describe("determineModuleContext", () => { assert.equal(packageName, "my-package"); assert.equal(relativePath, "some-dir/some-file"); } + }); + + it("strips a lib prefix", (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "package.json": `{ "name": "my-package" }`, + }); + + { + const { packageName, relativePath } = determineModuleContext( + path.join(tempDirectoryPath, "some-dir/libsome-file.node") + ); + assert.equal(packageName, "my-package"); + assert.equal(relativePath, "some-dir/some-file"); + } + }); + + it("resolves the correct package name", (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "package.json": `{ "name": "root-package" }`, + // Two sub-packages with the same name + "sub-package-a/package.json": `{ "name": "my-sub-package-a" }`, + "sub-package-b/package.json": `{ "name": "my-sub-package-b" }`, + }); { const { packageName, relativePath } = determineModuleContext( path.join(tempDirectoryPath, "sub-package-a/some-file.node") ); - assert.equal(packageName, "my-sub-package"); + assert.equal(packageName, "my-sub-package-a"); assert.equal(relativePath, "some-file"); } @@ -136,7 +162,7 @@ describe("determineModuleContext", () => { const { packageName, relativePath } = determineModuleContext( path.join(tempDirectoryPath, "sub-package-b/some-file.node") ); - assert.equal(packageName, "my-sub-package"); + assert.equal(packageName, "my-sub-package-b"); assert.equal(relativePath, "some-file"); } }); @@ -295,22 +321,30 @@ describe("determineModuleContext", () => { it("should read package.json only once across multiple module paths for the same package", (context) => { const tempDir = setupTempDirectory(context, { "package.json": `{ "name": "cached-pkg" }`, - "subdir1/file1.node": "", + "subdir1/file1.node": "", "subdir2/file2.node": "", - "subdir1/file1.xcframework": "" + "subdir1/file1.xcframework": "", }); let readCount = 0; const orig = fs.readFileSync; - context.mock.method(fs, "readFileSync", (...args: Parameters) => { - const [pathArg] = args; - if (typeof pathArg === "string" && pathArg.endsWith("package.json")) { - readCount++; + context.mock.method( + fs, + "readFileSync", + (...args: Parameters) => { + const [pathArg] = args; + if (typeof pathArg === "string" && pathArg.endsWith("package.json")) { + readCount++; + } + return orig(...args); } - return orig(...args); - }); + ); - const ctx1 = determineModuleContext(path.join(tempDir, "subdir1/file1.node")); - const ctx2 = determineModuleContext(path.join(tempDir, "subdir2/file2.node")); + const ctx1 = determineModuleContext( + path.join(tempDir, "subdir1/file1.node") + ); + const ctx2 = determineModuleContext( + path.join(tempDir, "subdir2/file2.node") + ); assert.equal(ctx1.packageName, "cached-pkg"); assert.equal(ctx2.packageName, "cached-pkg"); assert.equal(readCount, 1); diff --git a/packages/react-native-node-api-modules/src/node/path-utils.ts b/packages/react-native-node-api-modules/src/node/path-utils.ts index 5b6e77da..2eb67a18 100644 --- a/packages/react-native-node-api-modules/src/node/path-utils.ts +++ b/packages/react-native-node-api-modules/src/node/path-utils.ts @@ -41,7 +41,7 @@ export function isNodeApiModule(modulePath: string): boolean { // Cannot read directory: treat as no module return false; } - return Object.values(PLATFORM_EXTENSIONS).some(extension => { + return Object.values(PLATFORM_EXTENSIONS).some((extension) => { const fileName = baseName + extension; if (!entries.includes(fileName)) { return false; @@ -103,25 +103,32 @@ export function determineModuleContext( let pkgName = packageNameCache.get(pkgDir); if (!pkgName) { const pkg = readPackageSync({ cwd: pkgDir }); - assert(typeof pkg.name === "string", "Expected package.json to have a name"); + assert( + typeof pkg.name === "string", + "Expected package.json to have a name" + ); pkgName = pkg.name; packageNameCache.set(pkgDir, pkgName); } // Compute module-relative path - const relPath = normalizeModulePath( - path.relative(pkgDir, originalPath) - ); + const relPath = normalizeModulePath(path.relative(pkgDir, originalPath)); return { packageName: pkgName, relativePath: relPath }; } export function normalizeModulePath(modulePath: string) { - return path.normalize(stripExtension(modulePath)); + const dirname = path.normalize(path.dirname(modulePath)); + const basename = path.basename(modulePath); + const strippedBasename = stripExtension(basename).replace(/^lib/, ""); + return path.join(dirname, strippedBasename); } export function escapePath(modulePath: string) { return modulePath.replace(/[^a-zA-Z0-9]/g, "-"); } +/** + * Get the name of the library which will be used when the module is linked in. + */ export function getLibraryName(modulePath: string, naming: NamingStrategy) { const { packageName, relativePath } = determineModuleContext(modulePath); return naming.stripPathSuffix @@ -252,19 +259,21 @@ export function findNodeApiModulePaths( return []; } const candidatePath = path.join(fromPath, suffix); - return fs.readdirSync(candidatePath, { withFileTypes: true }).flatMap((file) => { - if ( - file.isFile() && - file.name === MAGIC_FILENAME && - hasPlatformExtension(platform, candidatePath) - ) { - return [candidatePath]; - } else if (file.isDirectory()) { - // Traverse into the child directory - return findNodeApiModulePaths(options, path.join(suffix, file.name)); - } - return []; - }); + return fs + .readdirSync(candidatePath, { withFileTypes: true }) + .flatMap((file) => { + if ( + file.isFile() && + file.name === MAGIC_FILENAME && + hasPlatformExtension(platform, candidatePath) + ) { + return [candidatePath]; + } else if (file.isDirectory()) { + // Traverse into the child directory + return findNodeApiModulePaths(options, path.join(suffix, file.name)); + } + return []; + }); } /** @@ -308,12 +317,13 @@ export function findNodeApiModulePathsByDependency({ } /** - * Determine the library filename based on the library paths. - * Ensuring that all framework paths have the same base name. + * Determine the library basename (no file extension nor "lib" prefix) based on the library paths. + * Errors if all framework paths doesn't produce the same basename. */ -export function determineLibraryFilename(libraryPaths: string[]) { +export function determineLibraryBasename(libraryPaths: string[]) { const libraryNames = libraryPaths.map((p) => - path.basename(p, path.extname(p)) + // Strip the "lib" prefix and any file extension + path.basename(p, path.extname(p)).replace(/^lib/, "") ); const candidates = new Set(libraryNames); assert(candidates.size === 1, "Expected all libraries to have the same name"); diff --git a/packages/react-native-node-api-modules/src/node/prebuilds/android.ts b/packages/react-native-node-api-modules/src/node/prebuilds/android.ts index f2a9c895..b999d8e3 100644 --- a/packages/react-native-node-api-modules/src/node/prebuilds/android.ts +++ b/packages/react-native-node-api-modules/src/node/prebuilds/android.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { AndroidTriplet } from "./triplets.js"; -import { determineLibraryFilename } from "../path-utils.js"; +import { determineLibraryBasename } from "../path-utils.js"; export const DEFAULT_ANDROID_TRIPLETS = [ "aarch64-linux-android", @@ -26,7 +26,7 @@ export const ANDROID_ARCHITECTURES = { * Ensuring that all framework paths have the same base name. */ export function determineAndroidLibsFilename(libraryPaths: string[]) { - const libraryName = determineLibraryFilename(libraryPaths); + const libraryName = determineLibraryBasename(libraryPaths); return `${libraryName}.android.node`; } diff --git a/packages/react-native-node-api-modules/src/node/prebuilds/apple.ts b/packages/react-native-node-api-modules/src/node/prebuilds/apple.ts index 86ac5a09..3e9455ce 100644 --- a/packages/react-native-node-api-modules/src/node/prebuilds/apple.ts +++ b/packages/react-native-node-api-modules/src/node/prebuilds/apple.ts @@ -6,7 +6,8 @@ import cp from "node:child_process"; import { spawn } from "bufout"; -import { AppleTriplet } from "react-native-node-api-modules"; +import { AppleTriplet } from "./triplets.js"; +import { determineLibraryBasename } from "../path-utils.js"; type AppleArchitecture = "arm64" | "x86_64" | "arm64;x86_64"; @@ -122,15 +123,7 @@ export async function createXCframework({ * Ensuring that all framework paths have the same base name. */ export function determineXCFrameworkFilename(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; + const name = determineLibraryBasename(frameworkPaths); return `${name}.xcframework`; } 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 63374f2d..49e9bf85 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 @@ -1,7 +1,25 @@ cmake_minimum_required(VERSION 3.15) project(weak-node-api) -add_library(${PROJECT_NAME} SHARED weak-node-api.cpp ${CMAKE_JS_SRC}) -target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_SOURCE_DIR}/../include) +add_library(${PROJECT_NAME} SHARED + weak_node_api.cpp + ${CMAKE_JS_SRC} +) + +# Stripping the prefix from the library name +# to make sure the name of the XCFramework will match the name of the library +if(APPLE) + set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") +endif() + +target_include_directories(${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include +) target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8) + +target_compile_options(${PROJECT_NAME} PRIVATE + $<$:/W4 /WX> + $<$>:-Wall -Wextra -Werror> +)