diff --git a/.changeset/cold-symbols-refuse.md b/.changeset/cold-symbols-refuse.md new file mode 100644 index 00000000..bf01ac33 --- /dev/null +++ b/.changeset/cold-symbols-refuse.md @@ -0,0 +1,5 @@ +--- +"gyp-to-cmake": minor +--- + +Add --weak-node-api option to emit CMake configuration for use with cmake-rn's default way of Node-API linkage. diff --git a/.changeset/open-ducks-shop.md b/.changeset/open-ducks-shop.md new file mode 100644 index 00000000..c1153f5a --- /dev/null +++ b/.changeset/open-ducks-shop.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": minor +--- + +Breaking: `CMAKE_JS_*` defines are no longer injected by default (use --cmake-js to opt-in) diff --git a/.changeset/three-colts-tie.md b/.changeset/three-colts-tie.md new file mode 100644 index 00000000..e575098a --- /dev/null +++ b/.changeset/three-colts-tie.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": minor +--- + +Expose includable WEAK_NODE_API_CONFIG to CMake projects diff --git a/docs/WEAK-NODE-API.md b/docs/WEAK-NODE-API.md new file mode 100644 index 00000000..0664a3b6 --- /dev/null +++ b/docs/WEAK-NODE-API.md @@ -0,0 +1,7 @@ +# The `weak-node-api` library + +Android's dynamic linker imposes restrictions on the access to global symbols (such as the Node-API free functions): A dynamic library must explicitly declare any dependency bringing symbols it needs as `DT_NEEDED`. + +The implementation of Node-API is split between Hermes and our host package and to avoid addons having to explicitly link against either, we've introduced a `weak-node-api` library (published in `react-native-node-api` package). This library exposes only Node-API and will have its implementation injected by the host. + +While technically not a requirement on non-Android platforms, we choose to make this the general approach across React Native platforms. This keeps things aligned across platforms, while exposing just the Node-API without forcing libraries to build with suppression of errors for undefined symbols. diff --git a/packages/cmake-rn/README.md b/packages/cmake-rn/README.md index e4c64b33..6b6aa888 100644 --- a/packages/cmake-rn/README.md +++ b/packages/cmake-rn/README.md @@ -3,3 +3,24 @@ A wrapper around Cmake making it easier to produce prebuilt binaries targeting iOS and Android matching [the prebuilt binary specification](https://github.com/callstackincubator/react-native-node-api/blob/main/docs/PREBUILDS.md). Serves the same purpose as `cmake-js` does for the Node.js community and could potentially be upstreamed into `cmake-js` eventually. + +## Linking against Node-API + +Android's dynamic linker imposes restrictions on the access to global symbols (such as the Node-API free functions): A dynamic library must explicitly declare any dependency bringing symbols it needs as `DT_NEEDED`. + +The implementation of Node-API is split between Hermes and our host package and to avoid addons having to explicitly link against either, we've introduced a `weak-node-api` library (published in `react-native-node-api` package). This library exposes only Node-API and will have its implementation injected by the host. + +To link against `weak-node-api` just include the CMake config exposed through `WEAK_NODE_API_CONFIG` and add `weak-node-api` to the `target_link_libraries` of the addon's library target. + +```cmake +cmake_minimum_required(VERSION 3.15...3.31) +project(tests-buffers) + +include(${WEAK_NODE_API_CONFIG}) + +add_library(addon SHARED addon.c) +target_link_libraries(addon PRIVATE weak-node-api) +target_compile_features(addon PRIVATE cxx_std_20) +``` + +This is different from how `cmake-js` "injects" the Node-API for linking (via `${CMAKE_JS_INC}`, `${CMAKE_JS_SRC}` and `${CMAKE_JS_LIB}`). To allow for interoperability between these tools, we inject these when you pass `--cmake-js` to `cmake-rn`. diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 7545caf3..5afa8932 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -14,7 +14,10 @@ import { } from "@react-native-node-api/cli-utils"; import { isSupportedTriplet } from "react-native-node-api"; -import { getWeakNodeApiVariables } from "./weak-node-api.js"; +import { + getCmakeJSVariables, + getWeakNodeApiVariables, +} from "./weak-node-api.js"; import { platforms, allTriplets as allTriplets, @@ -85,8 +88,8 @@ const outPathOption = new Option( const defineOption = new Option( "-D,--define ", "Define cache variables passed when configuring projects", -).argParser>( - (input, previous = {}) => { +) + .argParser[]>((input, previous = []) => { // TODO: Implement splitting of value using a regular expression (using named groups) for the format [:]= // and return an object keyed by variable name with the string value as value or alternatively an array of [value, type] const match = input.match( @@ -98,9 +101,10 @@ const defineOption = new Option( ); } const { name, type, value } = match.groups; - return { ...previous, [name]: type ? { value, type } : value }; - }, -); + previous.push({ [type ? `${name}:${type}` : name]: value }); + return previous; + }) + .default([]); const targetOption = new Option( "--target ", @@ -117,6 +121,11 @@ const noWeakNodeApiLinkageOption = new Option( "Don't pass the path of the weak-node-api library from react-native-node-api", ); +const cmakeJsOption = new Option( + "--cmake-js", + "Define CMAKE_JS_* variables used for compatibility with cmake-js", +).default(false); + let program = new Command("cmake-rn") .description("Build React Native Node API modules with CMake") .addOption(tripletOption) @@ -129,7 +138,8 @@ let program = new Command("cmake-rn") .addOption(cleanOption) .addOption(targetOption) .addOption(noAutoLinkOption) - .addOption(noWeakNodeApiLinkageOption); + .addOption(noWeakNodeApiLinkageOption) + .addOption(cmakeJsOption); for (const platform of platforms) { const allOption = new Option( @@ -296,19 +306,26 @@ async function configureProject( options: BaseOpts, ) { const { triplet, buildPath, outputPath } = context; - const { verbose, source, weakNodeApiLinkage } = options; + const { verbose, source, weakNodeApiLinkage, cmakeJs } = options; + + // TODO: Make the two following definitions a part of the platform definition const nodeApiDefinitions = weakNodeApiLinkage && isSupportedTriplet(triplet) - ? getWeakNodeApiVariables(triplet) - : // TODO: Make this a part of the platform definition - {}; + ? [getWeakNodeApiVariables(triplet)] + : []; - const definitions = { + const cmakeJsDefinitions = + cmakeJs && isSupportedTriplet(triplet) + ? [getCmakeJSVariables(triplet)] + : []; + + const definitions = [ ...nodeApiDefinitions, + ...cmakeJsDefinitions, ...options.define, - CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath, - }; + { CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath }, + ]; await spawn( "cmake", @@ -352,18 +369,13 @@ async function buildProject( ); } -type CmakeTypedDefinition = { value: string; type: string }; - -function toDefineArguments( - declarations: Record, -) { - return Object.entries(declarations).flatMap(([key, definition]) => { - if (typeof definition === "string") { - return ["-D", `${key}=${definition}`]; - } else { - return ["-D", `${key}:${definition.type}=${definition.value}`]; - } - }); +function toDefineArguments(declarations: Array>) { + return declarations.flatMap((values) => + Object.entries(values).flatMap(([key, definition]) => [ + "-D", + `${key}=${definition}`, + ]), + ); } export { program }; diff --git a/packages/cmake-rn/src/weak-node-api.ts b/packages/cmake-rn/src/weak-node-api.ts index 496905d7..bc8a7407 100644 --- a/packages/cmake-rn/src/weak-node-api.ts +++ b/packages/cmake-rn/src/weak-node-api.ts @@ -41,7 +41,7 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string { } } -export function getWeakNodeApiVariables(triplet: SupportedTriplet) { +function getNodeApiIncludePaths() { const includePaths = [getNodeApiHeadersPath(), getNodeAddonHeadersPath()]; for (const includePath of includePaths) { assert( @@ -49,8 +49,28 @@ export function getWeakNodeApiVariables(triplet: SupportedTriplet) { `Include path with a ';' is not supported: ${includePath}`, ); } + return includePaths; +} + +export function getWeakNodeApiVariables( + triplet: SupportedTriplet, +): Record { + return { + // Expose an includable CMake config file declaring the weak-node-api target + WEAK_NODE_API_CONFIG: path.join(weakNodeApiPath, "weak-node-api.cmake"), + WEAK_NODE_API_INC: getNodeApiIncludePaths().join(";"), + WEAK_NODE_API_LIB: getWeakNodeApiPath(triplet), + }; +} + +/** + * For compatibility with cmake-js + */ +export function getCmakeJSVariables( + triplet: SupportedTriplet, +): Record { return { - CMAKE_JS_INC: includePaths.join(";"), + CMAKE_JS_INC: getNodeApiIncludePaths().join(";"), CMAKE_JS_LIB: getWeakNodeApiPath(triplet), }; } diff --git a/packages/gyp-to-cmake/src/cli.ts b/packages/gyp-to-cmake/src/cli.ts index 118f0c0d..52d14984 100644 --- a/packages/gyp-to-cmake/src/cli.ts +++ b/packages/gyp-to-cmake/src/cli.ts @@ -62,25 +62,38 @@ export const program = new Command("gyp-to-cmake") "--no-path-transforms", "Don't transform output from command expansions (replacing '\\' with '/')", ) + .option("--weak-node-api", "Link against the weak-node-api library", false) + .option("--define-napi-version", "Define NAPI_VERSION for all targets", false) + .option("--cpp ", "C++ standard version", "17") .argument( "[path]", "Path to the binding.gyp file or directory to traverse recursively", process.cwd(), ) .action( - wrapAction((targetPath: string, { pathTransforms }) => { - const options: TransformOptions = { - unsupportedBehaviour: "throw", - disallowUnknownProperties: false, - transformWinPathsToPosix: pathTransforms, - }; - const stat = fs.statSync(targetPath); - if (stat.isFile()) { - transformBindingGypFile(targetPath, options); - } else if (stat.isDirectory()) { - transformBindingGypsRecursively(targetPath, options); - } else { - throw new Error(`Expected either a file or a directory: ${targetPath}`); - } - }), + wrapAction( + ( + targetPath: string, + { pathTransforms, cpp, defineNapiVersion, weakNodeApi }, + ) => { + const options: TransformOptions = { + unsupportedBehaviour: "throw", + disallowUnknownProperties: false, + transformWinPathsToPosix: pathTransforms, + compileFeatures: cpp ? [`cxx_std_${cpp}`] : [], + defineNapiVersion, + weakNodeApi, + }; + const stat = fs.statSync(targetPath); + if (stat.isFile()) { + transformBindingGypFile(targetPath, options); + } else if (stat.isDirectory()) { + transformBindingGypsRecursively(targetPath, options); + } else { + throw new Error( + `Expected either a file or a directory: ${targetPath}`, + ); + } + }, + ), ); diff --git a/packages/gyp-to-cmake/src/transformer.ts b/packages/gyp-to-cmake/src/transformer.ts index 01096501..fdf5c604 100644 --- a/packages/gyp-to-cmake/src/transformer.ts +++ b/packages/gyp-to-cmake/src/transformer.ts @@ -12,6 +12,9 @@ export type GypToCmakeListsOptions = { executeCmdExpansions?: boolean; unsupportedBehaviour?: "skip" | "warn" | "throw"; transformWinPathsToPosix?: boolean; + compileFeatures?: string[]; + defineNapiVersion?: boolean; + weakNodeApi?: boolean; }; function isCmdExpansion(value: string) { @@ -34,6 +37,9 @@ export function bindingGypToCmakeLists({ executeCmdExpansions = true, unsupportedBehaviour = "skip", transformWinPathsToPosix = true, + defineNapiVersion = true, + weakNodeApi = false, + compileFeatures = [], }: GypToCmakeListsOptions): string { function mapExpansion(value: string): string[] { if (!isCmdExpansion(value)) { @@ -60,65 +66,97 @@ export function bindingGypToCmakeLists({ } const lines: string[] = [ - "cmake_minimum_required(VERSION 3.15)", + "cmake_minimum_required(VERSION 3.15...3.31)", //"cmake_policy(SET CMP0091 NEW)", //"cmake_policy(SET CMP0042 NEW)", `project(${projectName})`, "", // Declaring a project-wide NAPI_VERSION as a fallback for targets that don't explicitly set it - `add_compile_definitions(NAPI_VERSION=${napiVersion})`, + // This is only needed when using cmake-js, as it is injected by cmake-rn + ...(defineNapiVersion + ? [`add_compile_definitions(NAPI_VERSION=${napiVersion})`] + : []), ]; + if (weakNodeApi) { + lines.push(`include(\${WEAK_NODE_API_CONFIG})`, ""); + } + for (const target of gyp.targets) { - const { target_name: targetName } = target; + const { target_name: targetName, defines = [] } = target; // TODO: Handle "conditions" // TODO: Handle "cflags" // TODO: Handle "ldflags" - const escapedJoinedSources = target.sources + const escapedSources = target.sources .flatMap(mapExpansion) .map(transformPath) - .map(escapeSpaces) - .join(" "); + .map(escapeSpaces); - const escapedJoinedIncludes = (target.include_dirs || []) + const escapedIncludes = (target.include_dirs || []) .flatMap(mapExpansion) .map(transformPath) - .map(escapeSpaces) - .join(" "); + .map(escapeSpaces); - const escapedJoinedDefines = (target.defines || []) + const escapedDefines = defines .flatMap(mapExpansion) .map(transformPath) - .map(escapeSpaces) - .join(" "); + .map(escapeSpaces); + + const libraries = []; + if (weakNodeApi) { + libraries.push("weak-node-api"); + } else { + libraries.push("${CMAKE_JS_LIB}"); + escapedSources.push("${CMAKE_JS_SRC}"); + escapedIncludes.push("${CMAKE_JS_INC}"); + } lines.push( - "", - `add_library(${targetName} SHARED ${escapedJoinedSources} \${CMAKE_JS_SRC})`, + `add_library(${targetName} SHARED ${escapedSources.join(" ")})`, `set_target_properties(${targetName} PROPERTIES PREFIX "" SUFFIX ".node")`, - `target_include_directories(${targetName} PRIVATE ${escapedJoinedIncludes} \${CMAKE_JS_INC})`, - `target_link_libraries(${targetName} PRIVATE \${CMAKE_JS_LIB})`, - `target_compile_features(${targetName} PRIVATE cxx_std_17)`, - ...(escapedJoinedDefines - ? [ - `target_compile_definitions(${targetName} PRIVATE ${escapedJoinedDefines})`, - ] - : []), - // or - // `set_target_properties(${targetName} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO)`, ); + + if (libraries.length > 0) { + lines.push( + `target_link_libraries(${targetName} PRIVATE ${libraries.join(" ")})`, + ); + } + + if (escapedIncludes.length > 0) { + lines.push( + `target_include_directories(${targetName} PRIVATE ${escapedIncludes.join( + " ", + )})`, + ); + } + + if (escapedDefines.length > 0) { + lines.push( + `target_compile_definitions(${targetName} PRIVATE ${escapedDefines.join(" ")})`, + ); + } + + if (compileFeatures.length > 0) { + lines.push( + `target_compile_features(${targetName} PRIVATE ${compileFeatures.join(" ")})`, + ); + } + + // `set_target_properties(${targetName} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO)`, } - // Adding this post-amble from the template, although not used by react-native-node-api - lines.push( - "", - "if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)", - " # Generate node.lib", - " execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})", - "endif()", - ); + if (!weakNodeApi) { + // This is required by cmake-js to generate the import library for node.lib on Windows + lines.push( + "", + "if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)", + " # Generate node.lib", + " execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})", + "endif()", + ); + } return lines.join("\n"); } diff --git a/packages/host/weak-node-api/weak-node-api.cmake b/packages/host/weak-node-api/weak-node-api.cmake new file mode 100644 index 00000000..2fda647e --- /dev/null +++ b/packages/host/weak-node-api/weak-node-api.cmake @@ -0,0 +1,6 @@ +add_library(weak-node-api SHARED IMPORTED) + +set_target_properties(weak-node-api PROPERTIES + IMPORTED_LOCATION "${WEAK_NODE_API_LIB}" + INTERFACE_INCLUDE_DIRECTORIES "${WEAK_NODE_API_INC}" +) diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index adae7591..db48db3f 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -11,7 +11,7 @@ }, "scripts": { "copy-examples": "tsx scripts/copy-examples.mts", - "gyp-to-cmake": "gyp-to-cmake .", + "gyp-to-cmake": "gyp-to-cmake --weak-node-api .", "build": "tsx scripts/build-examples.mts", "copy-and-build": "node --run copy-examples && node --run gyp-to-cmake && node --run build", "verify": "tsx scripts/verify-prebuilds.mts", diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index 31d513c0..a94a7716 100644 --- a/packages/node-addon-examples/tests/async/CMakeLists.txt +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -1,15 +1,9 @@ -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.15...3.31) project(tests-async) -add_compile_definitions(NAPI_VERSION=8) +include(${WEAK_NODE_API_CONFIG}) -add_library(addon SHARED addon.c ${CMAKE_JS_SRC}) +add_library(addon SHARED addon.c) set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") -target_include_directories(addon PRIVATE ${CMAKE_JS_INC}) -target_link_libraries(addon PRIVATE ${CMAKE_JS_LIB}) -target_compile_features(addon PRIVATE cxx_std_17) - -if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) - # Generate node.lib - execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) -endif() \ No newline at end of file +target_link_libraries(addon PRIVATE weak-node-api) +target_compile_features(addon PRIVATE cxx_std_17) \ No newline at end of file diff --git a/packages/node-addon-examples/tests/buffers/CMakeLists.txt b/packages/node-addon-examples/tests/buffers/CMakeLists.txt index 8e8cc950..837359fc 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -1,15 +1,9 @@ -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.15...3.31) project(tests-buffers) -add_compile_definitions(NAPI_VERSION=8) +include(${WEAK_NODE_API_CONFIG}) -add_library(addon SHARED addon.c ${CMAKE_JS_SRC}) +add_library(addon SHARED addon.c) set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") -target_include_directories(addon PRIVATE ${CMAKE_JS_INC}) -target_link_libraries(addon PRIVATE ${CMAKE_JS_LIB}) -target_compile_features(addon PRIVATE cxx_std_17) - -if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) - # Generate node.lib - execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) -endif() \ No newline at end of file +target_link_libraries(addon PRIVATE weak-node-api) +target_compile_features(addon PRIVATE cxx_std_17) \ No newline at end of file diff --git a/packages/node-tests/scripts/build-tests.mts b/packages/node-tests/scripts/build-tests.mts index de879e8c..1c38cfb8 100644 --- a/packages/node-tests/scripts/build-tests.mts +++ b/packages/node-tests/scripts/build-tests.mts @@ -1,5 +1,5 @@ import path from "node:path"; -import { spawnSync } from "node:child_process"; +import { execSync } from "node:child_process"; import { findCMakeProjects } from "./utils.mjs"; @@ -13,5 +13,8 @@ for (const projectPath of projectPaths) { projectPath, )} to build for React Native`, ); - spawnSync("cmake-rn", [], { cwd: projectPath, stdio: "inherit" }); + execSync("cmake-rn --cmake-js", { + cwd: projectPath, + stdio: "inherit", + }); }