diff --git a/.changeset/evil-vans-love.md b/.changeset/evil-vans-love.md new file mode 100644 index 00000000..873d1d86 --- /dev/null +++ b/.changeset/evil-vans-love.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Use CMake file API to read shared library target paths diff --git a/package-lock.json b/package-lock.json index 02024acc..16c9a3d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14808,6 +14808,7 @@ "version": "0.4.0", "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", + "cmake-file-api": "0.1.0", "react-native-node-api": "0.5.1" }, "bin": { diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index f78644da..91fa1bde 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", + "cmake-file-api": "0.1.0", "react-native-node-api": "0.5.1" }, "peerDependencies": { diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 5afa8932..a7d56530 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -13,6 +13,7 @@ import { wrapAction, } from "@react-native-node-api/cli-utils"; import { isSupportedTriplet } from "react-native-node-api"; +import * as cmakeFileApi from "cmake-file-api"; import { getCmakeJSVariables, @@ -327,6 +328,8 @@ async function configureProject( { CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath }, ]; + await cmakeFileApi.createSharedStatelessQuery(buildPath, "codemodel", "2"); + await spawn( "cmake", [ diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index 832f806e..655c1d7b 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -5,9 +5,9 @@ import path from "node:path"; import { Option, oraPromise, chalk } from "@react-native-node-api/cli-utils"; import { createAndroidLibsDirectory, - determineAndroidLibsFilename, AndroidTriplet as Triplet, } from "react-native-node-api"; +import * as cmakeFileApi from "cmake-file-api"; import type { Platform } from "./types.js"; @@ -121,50 +121,64 @@ export const platform: Platform = { const { ANDROID_HOME } = process.env; return typeof ANDROID_HOME === "string" && fs.existsSync(ANDROID_HOME); }, - async postBuild({ outputPath, triplets }, { autoLink }) { - // TODO: Include `configuration` in the output path - const libraryPathByTriplet = Object.fromEntries( - await Promise.all( - triplets.map(async ({ triplet, outputPath }) => { - assert( - fs.existsSync(outputPath), - `Expected a directory at ${outputPath}`, - ); - // Expect binary file(s), either .node or .so - const dirents = await fs.promises.readdir(outputPath, { - withFileTypes: true, - }); - const result = dirents - .filter( - (dirent) => - dirent.isFile() && - (dirent.name.endsWith(".so") || dirent.name.endsWith(".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), - ); - const androidLibsOutputPath = path.resolve(outputPath, androidLibsFilename); + async postBuild({ outputPath, triplets }, { autoLink, configuration }) { + const prebuilds: Record< + string, + { triplet: Triplet; libraryPath: string }[] + > = {}; - await oraPromise( - createAndroidLibsDirectory({ - outputPath: androidLibsOutputPath, - libraryPathByTriplet, - autoLink, - }), - { - text: "Assembling Android libs directory", - successText: `Android libs directory assembled into ${chalk.dim( - path.relative(process.cwd(), androidLibsOutputPath), - )}`, - failText: ({ message }) => - `Failed to assemble Android libs directory: ${message}`, - }, - ); + for (const { triplet, buildPath } of triplets) { + assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); + const targets = await cmakeFileApi.readCurrentTargetsDeep( + buildPath, + configuration, + "2.0", + ); + const sharedLibraries = targets.filter( + (target) => target.type === "SHARED_LIBRARY", + ); + assert.equal( + sharedLibraries.length, + 1, + "Expected exactly one shared library", + ); + const [sharedLibrary] = sharedLibraries; + const { artifacts } = sharedLibrary; + assert( + artifacts && artifacts.length === 1, + "Expected exactly one artifact", + ); + const [artifact] = artifacts; + // Add prebuild entry, creating a new entry if needed + if (!(sharedLibrary.name in prebuilds)) { + prebuilds[sharedLibrary.name] = []; + } + prebuilds[sharedLibrary.name].push({ + triplet, + libraryPath: path.join(buildPath, artifact.path), + }); + } + + for (const [libraryName, libraries] of Object.entries(prebuilds)) { + const prebuildOutputPath = path.resolve( + outputPath, + `${libraryName}.android.node`, + ); + await oraPromise( + createAndroidLibsDirectory({ + outputPath: prebuildOutputPath, + libraries, + autoLink, + }), + { + text: `Assembling Android libs directory (${libraryName})`, + successText: `Android libs directory (${libraryName}) assembled into ${chalk.dim( + path.relative(process.cwd(), prebuildOutputPath), + )}`, + failText: ({ message }) => + `Failed to assemble Android libs directory (${libraryName}): ${message}`, + }, + ); + } }, }; diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index 00e21c72..c93d3d92 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -7,10 +7,10 @@ import { AppleTriplet as Triplet, createAppleFramework, createXCframework, - determineXCFrameworkFilename, } from "react-native-node-api"; import type { Platform } from "./types.js"; +import * as cmakeFileApi from "cmake-file-api"; type XcodeSDKName = | "iphoneos" @@ -135,56 +135,63 @@ export const platform: Platform = { { outputPath, triplets }, { configuration, autoLink, xcframeworkExtension }, ) { - const libraryPaths = await Promise.all( - triplets.map(async ({ outputPath }) => { - const configSpecificPath = path.join(outputPath, 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, - xcframeworkExtension ? ".xcframework" : ".apple.node", - ); - - // Create the xcframework - const xcframeworkOutputPath = path.resolve(outputPath, xcframeworkFilename); - - await oraPromise( - createXCframework({ - outputPath: xcframeworkOutputPath, - frameworkPaths, - autoLink, - }), - { - text: "Assembling XCFramework", - successText: `XCFramework assembled into ${chalk.dim( - path.relative(process.cwd(), xcframeworkOutputPath), - )}`, - failText: ({ message }) => `Failed to assemble XCFramework: ${message}`, - }, - ); + const prebuilds: Record = {}; + for (const { buildPath } of triplets) { + assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); + const targets = await cmakeFileApi.readCurrentTargetsDeep( + buildPath, + configuration, + "2.0", + ); + const sharedLibraries = targets.filter( + (target) => target.type === "SHARED_LIBRARY", + ); + assert.equal( + sharedLibraries.length, + 1, + "Expected exactly one shared library", + ); + const [sharedLibrary] = sharedLibraries; + const { artifacts } = sharedLibrary; + assert( + artifacts && artifacts.length === 1, + "Expected exactly one artifact", + ); + const [artifact] = artifacts; + // Add prebuild entry, creating a new entry if needed + if (!(sharedLibrary.name in prebuilds)) { + prebuilds[sharedLibrary.name] = []; + } + prebuilds[sharedLibrary.name].push(path.join(buildPath, artifact.path)); + } + + const extension = xcframeworkExtension ? ".xcframework" : ".apple.node"; + + for (const [libraryName, libraryPaths] of Object.entries(prebuilds)) { + const frameworkPaths = await Promise.all( + libraryPaths.map(createAppleFramework), + ); + // Create the xcframework + const xcframeworkOutputPath = path.resolve( + outputPath, + `${libraryName}${extension}`, + ); + + await oraPromise( + createXCframework({ + outputPath: xcframeworkOutputPath, + frameworkPaths, + autoLink, + }), + { + text: `Assembling XCFramework (${libraryName})`, + successText: `XCFramework (${libraryName}) assembled into ${chalk.dim( + path.relative(process.cwd(), xcframeworkOutputPath), + )}`, + failText: ({ message }) => + `Failed to assemble XCFramework (${libraryName}): ${message}`, + }, + ); + } }, }; diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index d5cbaad7..2bd0bded 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -204,15 +204,13 @@ export const buildCommand = new Command("build") ); if (androidLibraries.length > 0) { - const libraryPathByTriplet = Object.fromEntries( - androidLibraries.map(([target, outputPath]) => [ - ANDROID_TRIPLET_PER_TARGET[target], - outputPath, - ]), - ) as Record; + const libraries = androidLibraries.map(([target, outputPath]) => ({ + triplet: ANDROID_TRIPLET_PER_TARGET[target], + libraryPath: outputPath, + })); const androidLibsFilename = determineAndroidLibsFilename( - Object.values(libraryPathByTriplet), + libraries.map(({ libraryPath }) => libraryPath), ); const androidLibsOutputPath = path.resolve( outputPath, @@ -222,7 +220,7 @@ export const buildCommand = new Command("build") await oraPromise( createAndroidLibsDirectory({ outputPath: androidLibsOutputPath, - libraryPathByTriplet, + libraries, autoLink: true, }), { @@ -238,7 +236,9 @@ export const buildCommand = new Command("build") if (appleLibraries.length > 0) { const libraryPaths = await combineLibraries(appleLibraries); - const frameworkPaths = libraryPaths.map(createAppleFramework); + const frameworkPaths = await Promise.all( + libraryPaths.map(createAppleFramework), + ); const xcframeworkFilename = determineXCFrameworkFilename( frameworkPaths, xcframeworkExtension ? ".xcframework" : ".apple.node", diff --git a/packages/host/src/node/prebuilds/android.ts b/packages/host/src/node/prebuilds/android.ts index b24b422b..ec26408b 100644 --- a/packages/host/src/node/prebuilds/android.ts +++ b/packages/host/src/node/prebuilds/android.ts @@ -32,24 +32,24 @@ export function determineAndroidLibsFilename(libraryPaths: string[]) { type AndroidLibsDirectoryOptions = { outputPath: string; - libraryPathByTriplet: Record; + libraries: { triplet: AndroidTriplet; libraryPath: string }[]; autoLink: boolean; }; export async function createAndroidLibsDirectory({ outputPath, - libraryPathByTriplet, + libraries, autoLink, }: AndroidLibsDirectoryOptions) { // Delete and recreate any existing output directory await fs.promises.rm(outputPath, { recursive: true, force: true }); await fs.promises.mkdir(outputPath, { recursive: true }); - for (const [triplet, libraryPath] of Object.entries(libraryPathByTriplet)) { + for (const { triplet, libraryPath } of libraries) { assert( fs.existsSync(libraryPath), `Library not found: ${libraryPath} for triplet ${triplet}`, ); - const arch = ANDROID_ARCHITECTURES[triplet as AndroidTriplet]; + const arch = ANDROID_ARCHITECTURES[triplet]; const archOutputPath = path.join(outputPath, arch); await fs.promises.mkdir(archOutputPath, { recursive: true }); // Strip the ".node" extension from the library name diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 693c90b6..f3b1efde 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -2,7 +2,6 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; -import cp from "node:child_process"; import { spawn } from "@react-native-node-api/cli-utils"; @@ -45,7 +44,7 @@ type XCframeworkOptions = { autoLink: boolean; }; -export function createAppleFramework(libraryPath: string) { +export async function createAppleFramework(libraryPath: string) { assert(fs.existsSync(libraryPath), `Library not found: ${libraryPath}`); // Write a info.plist file to the framework const libraryName = path.basename(libraryPath, path.extname(libraryPath)); @@ -54,11 +53,11 @@ export function createAppleFramework(libraryPath: string) { `${libraryName}.framework`, ); // Create the framework from scratch - fs.rmSync(frameworkPath, { recursive: true, force: true }); - fs.mkdirSync(frameworkPath); - fs.mkdirSync(path.join(frameworkPath, "Headers")); + await fs.promises.rm(frameworkPath, { recursive: true, force: true }); + await fs.promises.mkdir(frameworkPath); + await fs.promises.mkdir(path.join(frameworkPath, "Headers")); // Create an empty Info.plist file - fs.writeFileSync( + await fs.promises.writeFile( path.join(frameworkPath, "Info.plist"), createPlistContent({ CFBundleDevelopmentRegion: "en", @@ -75,13 +74,15 @@ export function createAppleFramework(libraryPath: string) { ); const newLibraryPath = path.join(frameworkPath, libraryName); // TODO: Consider copying the library instead of renaming it - fs.renameSync(libraryPath, newLibraryPath); + await fs.promises.rename(libraryPath, newLibraryPath); // Update the name of the library - cp.spawnSync("install_name_tool", [ - "-id", - `@rpath/${libraryName}.framework/${libraryName}`, - newLibraryPath, - ]); + await spawn( + "install_name_tool", + ["-id", `@rpath/${libraryName}.framework/${libraryName}`, newLibraryPath], + { + outputMode: "buffered", + }, + ); return frameworkPath; }