diff --git a/.changeset/chilly-trains-nail.md b/.changeset/chilly-trains-nail.md new file mode 100644 index 00000000..e9576eb7 --- /dev/null +++ b/.changeset/chilly-trains-nail.md @@ -0,0 +1,23 @@ +--- +"cmake-rn": minor +--- + +Add support for building projects declaring multiple shared object libraries into Node-API addons. + +Each addon is emitted next to the sources it was built from, so that a project +declaring many addons produces the same layout as building each of them on its +own. Both the location and the name of an artifact are derived from the target +that produced it: + +- `--out` supports a new `{targetSourceDir}` placeholder, expanding to the source + directory of the target being emitted, and now defaults to + `{targetSourceDir}/build/{configuration}`. This resolves to the same path as + before, unless `--build` is pointed outside of the source directory. +- The artifact is named after the target's `OUTPUT_NAME` rather than the CMake + target name. These are the same unless `OUTPUT_NAME` is set explicitly, which is + how a project can give its targets the unique names CMake requires without + affecting the name of the addon. + +Also adds `--concurrency`, limiting how many build tasks run at once. It defaults +to the available parallelism, or to 1 when `--verbose` is enabled, since +interleaved output from concurrent builds is hard to read. diff --git a/.changeset/real-emus-jam.md b/.changeset/real-emus-jam.md new file mode 100644 index 00000000..13a4194c --- /dev/null +++ b/.changeset/real-emus-jam.md @@ -0,0 +1,10 @@ +--- +"gyp-to-cmake": minor +--- + +Add --namespaced-targets to allow a root project to add many sub-projects. + +CMake requires target names to be unique across a project tree, so sub-projects +that each declare an `addon` target cannot be added to a single root project. This +prefixes the target name with the project name, while setting `OUTPUT_NAME` so the +artifact keeps the name a `require` resolves against. diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index a2b07e74..be687979 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; +import os from "node:os"; import { chalk, @@ -10,6 +11,8 @@ import { oraPromise, assertFixable, wrapAction, + pLimit, + InvalidArgumentError, } from "@react-native-node-api/cli-utils"; import { @@ -20,6 +23,7 @@ import { } from "./platforms.js"; import { Platform } from "./platforms/types.js"; import { getCcachePath } from "./ccache.js"; +import { createOutputPathResolver, expandTemplate } from "./output-path.js"; const verboseOption = new Option( "--verbose", @@ -71,8 +75,8 @@ const cleanOption = new Option( const outPathOption = new Option( "--out ", - "Specify the output directory to store the final build artifacts", -).default("{build}/{configuration}"); + "Specify the output directory to store the final build artifacts. Supports the {targetSourceDir} placeholder, which expands to the source directory of the target being emitted", +).default("{targetSourceDir}/build/{configuration}"); const defineOption = new Option( "-D,--define ", @@ -125,6 +129,22 @@ const ccachePathOption = new Option( "Specify the path to the ccache executable", ).default(getCcachePath()); +const concurrencyOption = new Option( + "--concurrency ", + "Limit the number of concurrent tasks", +) + .argParser((value) => { + const result = Number(value); + if (!Number.isSafeInteger(result) || result < 1) { + throw new InvalidArgumentError("Expected a positive integer."); + } + return result; + }) + .default( + undefined, + `${os.availableParallelism()} or 1 when --verbose is enabled`, + ); + let program = new Command("cmake-rn") .description("Build React Native Node API modules with CMake") .addOption(tripletOption) @@ -140,7 +160,8 @@ let program = new Command("cmake-rn") .addOption(noAutoLinkOption) .addOption(noWeakNodeApiLinkageOption) .addOption(cmakeJsOption) - .addOption(ccachePathOption); + .addOption(ccachePathOption) + .addOption(concurrencyOption); for (const platform of platforms) { const allOption = new Option( @@ -151,25 +172,15 @@ for (const platform of platforms) { program = platform.amendCommand(program); } -function expandTemplate( - input: string, - values: Record, -): string { - return input.replaceAll(/{([^}]+)}/g, (_, key: string) => - typeof values[key] === "string" ? values[key] : "", - ); -} - program = program.action( wrapAction(async ({ triplet: requestedTriplets, ...baseOptions }) => { baseOptions.build = path.resolve( process.cwd(), expandTemplate(baseOptions.build, baseOptions), ); - baseOptions.out = path.resolve( - process.cwd(), - expandTemplate(baseOptions.out, baseOptions), - ); + // Note: {targetSourceDir} is deliberately left unexpanded here, as it is + // only known per target, once the CMake File API has been read. + baseOptions.out = expandTemplate(baseOptions.out, baseOptions); const { verbose, clean, @@ -228,6 +239,13 @@ program = program.action( } } + // Interleaved output from concurrent builds is unreadable, so verbose + // builds default to running one task at a time. + const concurrency = + baseOptions.concurrency ?? (verbose ? 1 : os.availableParallelism()); + const limit = pLimit(concurrency); + const resolveOutputPath = createOutputPathResolver(out, source); + const tripletContexts = [...triplets].map((triplet) => { const platform = findPlatformForTriplet(triplet); @@ -240,17 +258,21 @@ program = program.action( triplet, platform, async spawn(command: string, args: string[], cwd?: string) { - const outputPrefix = verbose ? chalk.dim(`[${triplet}] `) : undefined; - if (verbose) { - console.log( - `${outputPrefix}» ${command} ${args.map((arg) => chalk.dim(`${arg}`)).join(" ")}`, - cwd ? `(in ${chalk.dim(cwd)})` : "", - ); - } - await spawn(command, args, { - outputMode: verbose ? "inherit" : "buffered", - outputPrefix, - cwd, + await limit(async () => { + const outputPrefix = verbose + ? chalk.dim(`[${triplet}] `) + : undefined; + if (verbose) { + console.log( + `${outputPrefix}» ${command} ${args.map((arg) => chalk.dim(`${arg}`)).join(" ")}`, + cwd ? `(in ${chalk.dim(cwd)})` : "", + ); + } + await spawn(command, args, { + outputMode: verbose ? "inherit" : "buffered", + outputPrefix, + cwd, + }); }); }, }; @@ -276,13 +298,15 @@ program = program.action( relevantTriplets, baseOptions, (command, args, cwd) => - spawn(command, args, { - outputMode: verbose ? "inherit" : "buffered", - outputPrefix: verbose - ? chalk.dim(`[${platform.name}] `) - : undefined, - cwd, - }), + limit(() => + spawn(command, args, { + outputMode: verbose ? "inherit" : "buffered", + outputPrefix: verbose + ? chalk.dim(`[${platform.name}] `) + : undefined, + cwd, + }), + ), ); } }), @@ -325,7 +349,11 @@ program = program.action( if (relevantTriplets.length == 0) { continue; } - await platform.postBuild(out, relevantTriplets, baseOptions); + await platform.postBuild( + resolveOutputPath, + relevantTriplets, + baseOptions, + ); } }), ); diff --git a/packages/cmake-rn/src/helpers.ts b/packages/cmake-rn/src/helpers.ts index 83db44ad..ecbc921f 100644 --- a/packages/cmake-rn/src/helpers.ts +++ b/packages/cmake-rn/src/helpers.ts @@ -1,3 +1,24 @@ +import path from "node:path"; + +/** + * The name of the emitted prebuild is derived from the artifact on disk (i.e. + * the target's OUTPUT_NAME) rather than the CMake target name. + * + * A project declaring multiple addons has to give its targets unique names, + * which for generated projects means namespacing them (see gyp-to-cmake's + * --namespaced-targets). The artifact keeps the name the JS `require` expects, + * so deriving from it keeps the prebuild's name independent of how the target + * had to be named to avoid a clash. + */ +export function getArtifactName(artifactPath: string) { + const basename = path.basename(artifactPath, path.extname(artifactPath)); + // Unless a target clears PREFIX (as the generated addon projects do), CMake + // prefixes a shared library with "lib". The prebuild is named after the + // library rather than the file, mirroring how createAndroidLibsDirectory adds + // the prefix back when copying the library into the libs directory. + return basename.startsWith("lib") ? basename.slice("lib".length) : basename; +} + export function toDefineArguments( declarations: Array>, ) { diff --git a/packages/cmake-rn/src/output-path.test.ts b/packages/cmake-rn/src/output-path.test.ts new file mode 100644 index 00000000..f657eee1 --- /dev/null +++ b/packages/cmake-rn/src/output-path.test.ts @@ -0,0 +1,89 @@ +import assert from "node:assert/strict"; +import path from "node:path"; +import { describe, it } from "node:test"; + +import { createOutputPathResolver, expandTemplate } from "./output-path.js"; +import { getArtifactName } from "./helpers.js"; + +describe("expandTemplate", () => { + it("expands known placeholders", () => { + assert.equal( + expandTemplate("{build}/{configuration}", { + build: "/tmp/build", + configuration: "Release", + }), + "/tmp/build/Release", + ); + }); + + it("leaves unknown placeholders untouched, to allow a later pass", () => { + assert.equal( + expandTemplate("{targetSourceDir}/build/{configuration}", { + configuration: "Release", + }), + "{targetSourceDir}/build/Release", + ); + }); +}); + +describe("createOutputPathResolver", () => { + const source = path.resolve("/projects/my-app"); + + it("resolves a top-level target next to the source directory", () => { + const resolve = createOutputPathResolver( + "{targetSourceDir}/build/Release", + source, + ); + // A single-addon project reports "." as the target's source directory, + // which has to keep emitting where it always has. + assert.equal(resolve("."), path.join(source, "build/Release")); + }); + + it("resolves each target of a multi-addon project next to its own sources", () => { + const resolve = createOutputPathResolver( + "{targetSourceDir}/build/Release", + source, + ); + assert.equal( + resolve("examples/hello"), + path.join(source, "examples/hello/build/Release"), + ); + assert.equal( + resolve("examples/goodbye"), + path.join(source, "examples/goodbye/build/Release"), + ); + }); + + it("handles a target source directory outside the top-level source", () => { + const resolve = createOutputPathResolver( + "{targetSourceDir}/build/Release", + source, + ); + const outside = path.resolve("/elsewhere/vendored"); + assert.equal(resolve(outside), path.join(outside, "build/Release")); + }); + + it("supports a template without the placeholder", () => { + const resolve = createOutputPathResolver("/tmp/out", source); + assert.equal(resolve("examples/hello"), path.resolve("/tmp/out")); + }); +}); + +describe("getArtifactName", () => { + it("derives the name from the artifact rather than the target", () => { + // gyp-to-cmake --namespaced-targets builds "addon.node" from a target named + // "-addon", and the prebuild has to keep the artifact's name. + assert.equal(getArtifactName("examples/hello/addon.node"), "addon"); + }); + + it("handles framework artifacts", () => { + assert.equal(getArtifactName("out/addon.framework/addon"), "addon"); + }); + + it("strips the prefix CMake adds to shared libraries", () => { + // weak-node-api does not clear PREFIX, so it builds a "libweak-node-api.so" + // and has to keep emitting a "weak-node-api.android.node" — the path + // packages/host/android/build.gradle points its jniLibs at. + assert.equal(getArtifactName("libweak-node-api.so"), "weak-node-api"); + }); +}); diff --git a/packages/cmake-rn/src/output-path.ts b/packages/cmake-rn/src/output-path.ts new file mode 100644 index 00000000..33a5845a --- /dev/null +++ b/packages/cmake-rn/src/output-path.ts @@ -0,0 +1,35 @@ +import path from "node:path"; + +/** + * Expand `{placeholder}` occurrences in a template. + * + * Placeholders without a value are left untouched, so a template can be expanded + * in multiple passes as more values become known. + */ +export function expandTemplate( + input: string, + values: Record, +): string { + return input.replaceAll(/{([^}]+)}/g, (match, key: string) => + typeof values[key] === "string" ? values[key] : match, + ); +} + +/** + * The final artifacts are emitted per target, relative to the source directory + * of the target itself. This keeps a target's prebuild next to the sources it + * was built from, even when a single project declares many addons, which is what + * the Babel plugin and auto-linking rely on to resolve a `require`. + */ +export function createOutputPathResolver(outTemplate: string, source: string) { + return function resolveOutputPath(targetSourceDir: string) { + return path.resolve( + process.cwd(), + expandTemplate(outTemplate, { + // `paths.source` is relative to the top-level source directory, unless + // the target lives outside of it, in which case it is already absolute. + targetSourceDir: path.resolve(source, targetSourceDir), + }), + ); + }; +} diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index 5c8b16ae..8364e1ce 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -14,7 +14,7 @@ import { import * as cmakeFileApi from "cmake-file-api"; import type { BaseOpts, Platform } from "./types.js"; -import { toDefineArguments } from "../helpers.js"; +import { getArtifactName, toDefineArguments } from "../helpers.js"; import { getCmakeJSVariables, getWeakNodeApiVariables, @@ -201,7 +201,6 @@ export const platform: Platform = { await Promise.all( triplets.map(async ({ triplet, spawn }) => { const buildPath = getBuildPath(build, triplet, configuration); - const outputPath = path.join(buildPath, "out"); // We want to use the CMake File API to query information later await cmakeFileApi.createSharedStatelessQuery( buildPath, @@ -226,7 +225,6 @@ export const platform: Platform = { ...commonDefinitions, { // "CPACK_SYSTEM_NAME": `Android-${architecture}`, - CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath, ANDROID_ABI: ANDROID_ARCHITECTURES[triplet], }, ]), @@ -247,13 +245,20 @@ export const platform: Platform = { return typeof ANDROID_HOME === "string" && fs.existsSync(ANDROID_HOME); }, async postBuild( - outputPath, + resolveOutputPath, triplets, { autoLink, configuration, target, build, strip, ndkVersion }, ) { + // Keyed by CMake target name, which CMake guarantees to be unique within a + // project. The artifact name is not: every addon of a multi-addon project + // may well build an "addon.node". const prebuilds: Record< string, - { triplet: Triplet; libraryPath: string }[] + { + artifactName: string; + targetSourceDir: string; + libraries: { triplet: Triplet; libraryPath: string }[]; + } > = {}; for (const { triplet, spawn } of triplets) { @@ -269,47 +274,51 @@ export const platform: Platform = { type === "SHARED_LIBRARY" && (target.length === 0 || target.includes(name)), ); - 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] = []; - } - const libraryPath = path.join(buildPath, artifact.path); - assert( - fs.existsSync(libraryPath), - `Expected built library at ${libraryPath}`, - ); + await Promise.all( + sharedLibraries.map(async (sharedLibrary) => { + 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] = { + artifactName: getArtifactName(artifact.path), + targetSourceDir: sharedLibrary.paths.source, + libraries: [], + }; + } + const libraryPath = path.join(buildPath, artifact.path); + assert( + fs.existsSync(libraryPath), + `Expected built library at ${libraryPath}`, + ); - if (strip) { - const llvmBinPath = getNdkLlvmBinPath(getNdkPath(ndkVersion)); - const stripToolPath = path.join(llvmBinPath, `llvm-strip`); - assert( - fs.existsSync(stripToolPath), - `Expected llvm-strip to exist at ${stripToolPath}`, - ); - await spawn(stripToolPath, [libraryPath]); - } - prebuilds[sharedLibrary.name].push({ - triplet, - libraryPath, - }); + if (strip) { + const llvmBinPath = getNdkLlvmBinPath(getNdkPath(ndkVersion)); + const stripToolPath = path.join(llvmBinPath, `llvm-strip`); + assert( + fs.existsSync(stripToolPath), + `Expected llvm-strip to exist at ${stripToolPath}`, + ); + await spawn(stripToolPath, [libraryPath]); + } + prebuilds[sharedLibrary.name].libraries.push({ + triplet, + libraryPath, + }); + }), + ); } - for (const [libraryName, libraries] of Object.entries(prebuilds)) { + for (const { artifactName, targetSourceDir, libraries } of Object.values( + prebuilds, + )) { const prebuildOutputPath = path.resolve( - outputPath, - `${libraryName}.android.node`, + resolveOutputPath(targetSourceDir), + `${artifactName}.android.node`, ); await oraPromise( createAndroidLibsDirectory({ @@ -318,10 +327,10 @@ export const platform: Platform = { autoLink, }), { - text: `Assembling Android libs directory (${libraryName})`, - successText: `Android libs directory (${libraryName}) assembled into ${prettyPath(prebuildOutputPath)}`, + text: `Assembling Android libs directory (${artifactName})`, + successText: `Android libs directory (${artifactName}) assembled into ${prettyPath(prebuildOutputPath)}`, failText: ({ message }) => - `Failed to assemble Android libs directory (${libraryName}): ${message}`, + `Failed to assemble Android libs directory (${artifactName}): ${message}`, }, ); } diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index b2c8b640..2811bcbb 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; import cp from "node:child_process"; +import { promisify } from "node:util"; import { assertFixable, @@ -18,7 +19,7 @@ import { import type { Platform } from "./types.js"; import * as cmakeFileApi from "cmake-file-api"; -import { toDefineArguments } from "../helpers.js"; +import { getArtifactName, toDefineArguments } from "../helpers.js"; import { getCmakeJSVariables, getWeakNodeApiVariables, @@ -35,17 +36,16 @@ const XcodeListOutput = z.object({ }), }); -function listXcodeProject(cwd: string): z.infer { - const result = cp.spawnSync("xcodebuild", ["-list", "-json"], { +const execFile = promisify(cp.execFile); + +async function listXcodeProject( + cwd: string, +): Promise> { + const { stdout } = await execFile("xcodebuild", ["-list", "-json"], { encoding: "utf-8", cwd, }); - assert.equal( - result.status, - 0, - `Failed to run xcodebuild -list: ${result.stderr}`, - ); - const parsed = JSON.parse(result.stdout) as unknown; + const parsed = JSON.parse(stdout) as unknown; return XcodeListOutput.parse(parsed); } @@ -176,7 +176,7 @@ function getBuildPath(baseBuildPath: string, triplet: Triplet) { return path.join(baseBuildPath, triplet.replace(/;/g, "_")); } -async function readCmakeSharedLibraryTarget( +async function readCmakeSharedLibraryTargets( buildPath: string, configuration: string, target: string[], @@ -186,18 +186,11 @@ async function readCmakeSharedLibraryTarget( configuration, "2.0", ); - const sharedLibraries = targets.filter( + return targets.filter( ({ type, name }) => type === "SHARED_LIBRARY" && (target.length === 0 || target.includes(name)), ); - assert.equal( - sharedLibraries.length, - 1, - "Expected exactly one shared library", - ); - const [sharedLibrary] = sharedLibraries; - return sharedLibrary; } const SIMULATOR_TRIPLET_SUFFIXES = [ @@ -363,8 +356,22 @@ export const platform: Platform = { // where an unexpanded variable would emitted in the artifact paths. // This is okay, since we're generating per triplet build directories anyway. // https://gitlab.kitware.com/cmake/cmake/-/issues/24161 - CMAKE_LIBRARY_OUTPUT_DIRECTORY: path.join(buildPath, "out"), - CMAKE_ARCHIVE_OUTPUT_DIRECTORY: path.join(buildPath, "out"), + // + // The directory is per target: a project declaring multiple addons + // gives every target the same OUTPUT_NAME (see gyp-to-cmake's + // --namespaced-targets), so a shared directory would have them + // overwrite each other's framework and every prebuild would end up + // assembled from whichever target happened to build last. + CMAKE_LIBRARY_OUTPUT_DIRECTORY: path.join( + buildPath, + "out", + "$", + ), + CMAKE_ARCHIVE_OUTPUT_DIRECTORY: path.join( + buildPath, + "out", + "$", + ), }, ]), ]); @@ -375,70 +382,58 @@ export const platform: Platform = { { spawn, triplet }, { build, target, configuration, appleBundleIdentifier, codeSigningAllowed }, ) { - // We expect the final application to sign these binaries - if (target.length > 1) { - throw new Error("Building for multiple targets is not supported yet"); - } - const buildPath = getBuildPath(build, triplet); - const sharedLibrary = await readCmakeSharedLibraryTarget( + const sharedLibraries = await readCmakeSharedLibraryTargets( buildPath, configuration, target, ); - const isFramework = sharedLibrary.nameOnDisk?.includes(".framework/"); + const frameworkTargets = sharedLibraries.filter(({ nameOnDisk }) => + nameOnDisk?.includes(".framework/"), + ); + const libraryTargets = sharedLibraries.filter( + ({ nameOnDisk }) => !nameOnDisk?.includes(".framework/"), + ); - if (isFramework) { - const { project } = listXcodeProject(buildPath); + if (frameworkTargets.length > 0) { + const { project } = await listXcodeProject(buildPath); const schemes = project.schemes.filter( (scheme) => scheme !== "ALL_BUILD" && scheme !== "ZERO_CHECK", ); - assert( - schemes.length === 1, - `Expected exactly one buildable scheme, got ${schemes.join(", ")}`, - ); - - const [scheme] = schemes; - - if (target.length === 1) { - assert.equal( - scheme, - target[0], - "Expected the only scheme to match the requested target", + // Note: These run in sequence on purpose. Concurrent invocations of + // xcodebuild against the same Xcode project (and its derived data) are + // not reliable, and every target of a triplet shares a single project. + for (const { name } of frameworkTargets) { + assert( + schemes.includes(name), + `Expected to find a scheme for ${name}, got ${schemes.join(", ")}`, ); + + for (const action of ["archive", "install"] as const) { + await spawn( + "xcodebuild", + [ + action, + "-scheme", + name, + "-configuration", + configuration, + "-destination", + DESTINATION_BY_TRIPLET[triplet], + ], + buildPath, + ); + } } + } - await spawn( - "xcodebuild", - [ - "archive", - "-scheme", - scheme, - "-configuration", - configuration, - "-destination", - DESTINATION_BY_TRIPLET[triplet], - ], - buildPath, - ); - await spawn( - "xcodebuild", - [ - "install", - "-scheme", - scheme, - "-configuration", - configuration, - "-destination", - DESTINATION_BY_TRIPLET[triplet], - ], - buildPath, - ); - } else { + if (libraryTargets.length > 0) { + // A single invocation builds every requested target, so this is hoisted + // out of the per-target loop below. await spawn("cmake", [ "--build", buildPath, @@ -452,115 +447,146 @@ export const platform: Platform = { // --code-signing-allowed. `CODE_SIGNING_ALLOWED=${codeSigningAllowed ? "YES" : "NO"}`, ]); - // Create a framework - const { artifacts } = sharedLibrary; - assert( - artifacts && artifacts.length === 1, - "Expected exactly one artifact", + + // We expect the final application to sign these binaries + await Promise.all( + libraryTargets.map(async ({ artifacts }) => { + assert( + artifacts && artifacts.length === 1, + "Expected exactly one artifact", + ); + const [artifact] = artifacts; + await createAppleFramework({ + libraryPath: path.join(buildPath, artifact.path), + kind: triplet.endsWith("-darwin") ? "versioned" : "flat", + bundleIdentifier: appleBundleIdentifier, + }); + }), ); - const [artifact] = artifacts; - await createAppleFramework({ - libraryPath: path.join(buildPath, artifact.path), - kind: triplet.endsWith("-darwin") ? "versioned" : "flat", - bundleIdentifier: appleBundleIdentifier, - }); } }, isSupportedByHost: function (): boolean | Promise { return process.platform === "darwin"; }, async postBuild( - outputPath, + resolveOutputPath, triplets, { configuration, autoLink, xcframeworkExtension, target, build, strip }, ) { - const libraryNames = new Set(); - const frameworkPaths: string[] = []; + // Keyed by CMake target name, which CMake guarantees to be unique within a + // project. The artifact name is not: every addon of a multi-addon project + // may well build an "addon.node". + const prebuilds: Record< + string, + { + artifactName: string; + targetSourceDir: string; + frameworkPaths: string[]; + } + > = {}; + // TODO: Run this in parallel for (const { spawn, triplet } of triplets) { const buildPath = getBuildPath(build, triplet); assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); - const sharedLibrary = await readCmakeSharedLibraryTarget( + const sharedLibraries = await readCmakeSharedLibraryTargets( buildPath, configuration, target, ); - const { artifacts } = sharedLibrary; - assert( - artifacts && artifacts.length === 1, - "Expected exactly one artifact", - ); - const [artifact] = artifacts; - - const artifactPath = path.join(buildPath, artifact.path); - if (strip) { - // -r: All relocation entries. - // -S: All symbol table entries. - // -T: All text relocation entries. - // -x: All local symbols. - await spawn("strip", ["-rSTx", artifactPath]); - } - - libraryNames.add(sharedLibrary.name); - // Locate the path of the framework, if a free dynamic library was built - if (artifact.path.includes(".framework/")) { - frameworkPaths.push(path.dirname(artifactPath)); - } else { - const libraryName = path.basename( - artifact.path, - path.extname(artifact.path), - ); - const frameworkPath = path.join( - buildPath, - path.dirname(artifact.path), - `${libraryName}.framework`, - ); - assert( - fs.existsSync(frameworkPath), - `Expected to find a framework at: ${frameworkPath}`, - ); - frameworkPaths.push(frameworkPath); - } + await Promise.all( + sharedLibraries.map(async (sharedLibrary) => { + const { name, paths, artifacts } = sharedLibrary; + assert( + artifacts && artifacts.length === 1, + "Expected exactly one artifact", + ); + const [artifact] = artifacts; + const artifactName = getArtifactName(artifact.path); + + const artifactPath = path.join(buildPath, artifact.path); + + if (strip) { + // -r: All relocation entries. + // -S: All symbol table entries. + // -T: All text relocation entries. + // -x: All local symbols. + await spawn("strip", ["-rSTx", artifactPath]); + } + + // Locate the path of the framework, if a free dynamic library was built + let frameworkPath: string; + if (artifact.path.includes(".framework/")) { + frameworkPath = path.dirname(artifactPath); + } else { + // createAppleFramework names the framework after the artifact file, + // keeping any "lib" prefix, so this is derived the same way rather + // than from the (prefix-stripped) name of the prebuild. + frameworkPath = path.join( + buildPath, + path.dirname(artifact.path), + `${path.basename( + artifact.path, + path.extname(artifact.path), + )}.framework`, + ); + assert( + fs.existsSync(frameworkPath), + `Expected to find a framework at: ${frameworkPath}`, + ); + } + + if (name in prebuilds) { + prebuilds[name].frameworkPaths.push(frameworkPath); + } else { + prebuilds[name] = { + artifactName, + targetSourceDir: paths.source, + frameworkPaths: [frameworkPath], + }; + } + }), + ); } - // Make sure none of the frameworks are symlinks - // We do this before creating an xcframework to avoid symlink paths being invalidated - // as the xcframework might be moved to a different location - await Promise.all( - frameworkPaths.map(async (frameworkPath) => { - const stat = await fs.promises.lstat(frameworkPath); - if (stat.isSymbolicLink()) { - await dereferenceDirectory(frameworkPath); - } - }), - ); - - const extension = xcframeworkExtension ? ".xcframework" : ".apple.node"; + for (const { + artifactName, + targetSourceDir, + frameworkPaths, + } of Object.values(prebuilds)) { + // Make sure none of the frameworks are symlinks + // We do this before creating an xcframework to avoid symlink paths being invalidated + // as the xcframework might be moved to a different location + await Promise.all( + frameworkPaths.map(async (frameworkPath) => { + const stat = await fs.promises.lstat(frameworkPath); + if (stat.isSymbolicLink()) { + await dereferenceDirectory(frameworkPath); + } + }), + ); - assert( - libraryNames.size === 1, - "Expected all libraries to have the same name", - ); - const [libraryName] = libraryNames; + const extension = xcframeworkExtension ? ".xcframework" : ".apple.node"; - // Create the xcframework - const xcframeworkOutputPath = path.resolve( - outputPath, - `${libraryName}${extension}`, - ); + // Create the xcframework + const xcframeworkOutputPath = path.resolve( + resolveOutputPath(targetSourceDir), + `${artifactName}${extension}`, + ); - await oraPromise( - createXCframework({ - outputPath: xcframeworkOutputPath, - frameworkPaths, - autoLink, - }), - { - text: `Assembling XCFramework (${libraryName})`, - successText: `XCFramework (${libraryName}) assembled into ${prettyPath(xcframeworkOutputPath)}`, - failText: ({ message }) => - `Failed to assemble XCFramework (${libraryName}): ${message}`, - }, - ); + await oraPromise( + createXCframework({ + outputPath: xcframeworkOutputPath, + frameworkPaths, + autoLink, + }), + { + text: `Assembling XCFramework (${artifactName})`, + successText: `XCFramework (${artifactName}) assembled into ${prettyPath(xcframeworkOutputPath)}`, + failText: ({ message }) => + `Failed to assemble XCFramework (${artifactName}): ${message}`, + }, + ); + } }, }; diff --git a/packages/cmake-rn/src/platforms/types.ts b/packages/cmake-rn/src/platforms/types.ts index d6cd3963..98e68f2a 100644 --- a/packages/cmake-rn/src/platforms/types.ts +++ b/packages/cmake-rn/src/platforms/types.ts @@ -29,6 +29,14 @@ export type Spawn = ( cwd?: string, ) => Promise; +/** + * Resolve the directory a target's final artifact should be emitted into. + * @param targetSourceDir The target's source directory, as reported by the CMake + * File API: relative to the top-level source directory, or absolute if the + * target lives outside of it. + */ +export type ResolveOutputPath = (targetSourceDir: string) => string; + export type Platform< Triplets extends string[] = string[], Opts extends cli.OptionValues = Record, @@ -86,9 +94,9 @@ export type Platform< */ postBuild( /** - * Location of the final prebuilt artefact. + * Resolve the location of the final prebuilt artefact, per target. */ - outputPath: string, + resolveOutputPath: ResolveOutputPath, triplets: TripletContext[], options: BaseOpts & Opts, ): Promise; diff --git a/packages/gyp-to-cmake/src/cli.ts b/packages/gyp-to-cmake/src/cli.ts index 45cefbaa..13ff8480 100644 --- a/packages/gyp-to-cmake/src/cli.ts +++ b/packages/gyp-to-cmake/src/cli.ts @@ -90,6 +90,11 @@ export const program = new Command("gyp-to-cmake") "Disable emitting target properties to produce Apple frameworks", ) .option("--cpp ", "C++ standard version", "17") + .option( + "--namespaced-targets", + "Use namespaced targets, to allow multiple targets with the same name to be referenced from a single parent project", + false, + ) .addOption(projectNameOption) .argument( "[path]", @@ -107,6 +112,7 @@ export const program = new Command("gyp-to-cmake") weakNodeApi, appleFramework, projectName, + namespacedTargets, }, ) => { const options: Omit = { @@ -117,6 +123,7 @@ export const program = new Command("gyp-to-cmake") defineNapiVersion, weakNodeApi, appleFramework, + namespacedTargets, }; const stat = fs.statSync(targetPath); if (stat.isFile()) { diff --git a/packages/gyp-to-cmake/src/transformer.test.ts b/packages/gyp-to-cmake/src/transformer.test.ts index 06d62f8f..a43a794a 100644 --- a/packages/gyp-to-cmake/src/transformer.test.ts +++ b/packages/gyp-to-cmake/src/transformer.test.ts @@ -125,4 +125,110 @@ describe("bindingGypToCmakeLists", () => { ); }); }); + + describe("namespaced targets", () => { + const gyp = { + targets: [{ target_name: "addon", sources: ["addon.cc"] }], + }; + + it("should not namespace or set OUTPUT_NAME by default", () => { + const output = bindingGypToCmakeLists({ + projectName: "some-project", + gyp, + }); + + assert( + output.includes("add_library(addon SHARED addon.cc"), + `Expected an un-namespaced target:\n${output}`, + ); + assert( + !output.includes("OUTPUT_NAME"), + `Expected no OUTPUT_NAME when not namespacing:\n${output}`, + ); + }); + + it("should prefix the target name with the project name", () => { + const output = bindingGypToCmakeLists({ + projectName: "some-project", + gyp, + namespacedTargets: true, + }); + + assert( + output.includes("add_library(some-project-addon SHARED addon.cc"), + `Expected a namespaced target:\n${output}`, + ); + assert( + !output.includes("add_library(addon "), + `Expected no un-namespaced target:\n${output}`, + ); + }); + + it("should reference the namespaced target in target-specific commands", () => { + const output = bindingGypToCmakeLists({ + projectName: "some-project", + gyp: { + targets: [ + { + target_name: "addon", + sources: ["addon.cc"], + include_dirs: ["include"], + defines: ["FOO"], + }, + ], + }, + namespacedTargets: true, + weakNodeApi: true, + compileFeatures: ["cxx_std_17"], + }); + + for (const command of [ + "target_link_libraries(some-project-addon PRIVATE weak-node-api)", + "target_include_directories(some-project-addon PRIVATE include)", + "target_compile_definitions(some-project-addon PRIVATE FOO)", + "target_compile_features(some-project-addon PRIVATE cxx_std_17)", + ]) { + assert( + output.includes(command), + `Expected output to include "${command}":\n${output}`, + ); + } + }); + + it("should keep the artifact name un-namespaced in both Apple branches", () => { + const output = bindingGypToCmakeLists({ + projectName: "some-project", + gyp, + namespacedTargets: true, + }); + + // CMake names the framework bundle after OUTPUT_NAME, so both the + // framework and the plain shared library branch need it. Otherwise the + // prebuild ends up named after the namespaced target. + assert.equal( + output.match(/OUTPUT_NAME addon$/gm)?.length, + 2, + `Expected OUTPUT_NAME in both branches:\n${output}`, + ); + }); + + it("should set OUTPUT_NAME when Apple framework support is disabled", () => { + const output = bindingGypToCmakeLists({ + projectName: "some-project", + gyp, + namespacedTargets: true, + appleFramework: false, + }); + + assert( + output.includes("set_target_properties(some-project-addon PROPERTIES"), + `Expected properties on the namespaced target:\n${output}`, + ); + assert.equal( + output.match(/OUTPUT_NAME addon$/gm)?.length, + 1, + `Expected a single OUTPUT_NAME:\n${output}`, + ); + }); + }); }); diff --git a/packages/gyp-to-cmake/src/transformer.ts b/packages/gyp-to-cmake/src/transformer.ts index 4901df4f..de4c0773 100644 --- a/packages/gyp-to-cmake/src/transformer.ts +++ b/packages/gyp-to-cmake/src/transformer.ts @@ -16,6 +16,7 @@ export type GypToCmakeListsOptions = { defineNapiVersion?: boolean; weakNodeApi?: boolean; appleFramework?: boolean; + namespacedTargets?: boolean; }; function isCmdExpansion(value: string) { @@ -50,6 +51,7 @@ export function bindingGypToCmakeLists({ weakNodeApi = false, appleFramework = true, compileFeatures = [], + namespacedTargets = false, }: GypToCmakeListsOptions): string { function mapExpansion(value: string): string[] { if (!isCmdExpansion(value)) { @@ -123,12 +125,23 @@ export function bindingGypToCmakeLists({ escapedIncludes.push("${CMAKE_JS_INC}"); } + const actualTargetName = namespacedTargets + ? `${projectName}-${targetName}` + : targetName; + + // Namespacing only disambiguates the CMake target name: the artifact on disk + // keeps the name the JS `require` expects, which is what cmake-rn derives the + // final prebuild name from. + const outputNameProperties: Record = namespacedTargets + ? { OUTPUT_NAME: targetName } + : {}; + function setTargetPropertiesLines( properties: Record, indent = "", ): string[] { return [ - `${indent}set_target_properties(${targetName} PROPERTIES`, + `${indent}set_target_properties(${actualTargetName} PROPERTIES`, ...Object.entries(properties).map( ([key, value]) => `${indent} ${key} ${value ? value : '""'}`, ), @@ -136,7 +149,9 @@ export function bindingGypToCmakeLists({ ]; } - lines.push(`add_library(${targetName} SHARED ${escapedSources.join(" ")})`); + lines.push( + `add_library(${actualTargetName} SHARED ${escapedSources.join(" ")})`, + ); if (appleFramework) { lines.push( @@ -153,6 +168,9 @@ export function bindingGypToCmakeLists({ MACOSX_FRAMEWORK_SHORT_VERSION_STRING: "1.0", MACOSX_FRAMEWORK_BUNDLE_VERSION: "1.0", XCODE_ATTRIBUTE_SKIP_INSTALL: "NO", + // CMake names the framework bundle after OUTPUT_NAME, so this has to + // be set here too for the artifact to keep its non-namespaced name. + ...outputNameProperties, }, " ", ), @@ -161,6 +179,7 @@ export function bindingGypToCmakeLists({ { PREFIX: "", SUFFIX: ".node", + ...outputNameProperties, }, " ", ), @@ -172,19 +191,20 @@ export function bindingGypToCmakeLists({ ...setTargetPropertiesLines({ PREFIX: "", SUFFIX: ".node", + ...outputNameProperties, }), ); } if (libraries.length > 0) { lines.push( - `target_link_libraries(${targetName} PRIVATE ${libraries.join(" ")})`, + `target_link_libraries(${actualTargetName} PRIVATE ${libraries.join(" ")})`, ); } if (escapedIncludes.length > 0) { lines.push( - `target_include_directories(${targetName} PRIVATE ${escapedIncludes.join( + `target_include_directories(${actualTargetName} PRIVATE ${escapedIncludes.join( " ", )})`, ); @@ -192,17 +212,17 @@ export function bindingGypToCmakeLists({ if (escapedDefines.length > 0) { lines.push( - `target_compile_definitions(${targetName} PRIVATE ${escapedDefines.join(" ")})`, + `target_compile_definitions(${actualTargetName} PRIVATE ${escapedDefines.join(" ")})`, ); } if (compileFeatures.length > 0) { lines.push( - `target_compile_features(${targetName} PRIVATE ${compileFeatures.join(" ")})`, + `target_compile_features(${actualTargetName} PRIVATE ${compileFeatures.join(" ")})`, ); } - // `set_target_properties(${targetName} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO)`, + // `set_target_properties(${actualTargetName} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO)`, } if (!weakNodeApi) { diff --git a/packages/node-addon-examples/.gitignore b/packages/node-addon-examples/.gitignore index 7470cb91..e7b17a9e 100644 --- a/packages/node-addon-examples/.gitignore +++ b/packages/node-addon-examples/.gitignore @@ -1,2 +1,3 @@ examples/ build/ +/CMakeLists.txt diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index 0d2d129b..acfd70ec 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -21,9 +21,10 @@ }, "scripts": { "copy-examples": "tsx scripts/copy-examples.mts", - "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", + "gyp-to-cmake": "gyp-to-cmake --namespaced-targets --weak-node-api .", + "generate-root-project": "tsx scripts/generate-root-project.mts", + "build": "cmake-rn --configuration RelWithDebInfo", + "copy-and-build": "node --run copy-examples && node --run gyp-to-cmake && node --run generate-root-project && node --run build", "verify": "tsx scripts/verify-prebuilds.mts", "test": "node --run copy-and-build && node --run verify", "bootstrap": "node --run copy-and-build" diff --git a/packages/node-addon-examples/scripts/build-examples.mts b/packages/node-addon-examples/scripts/build-examples.mts deleted file mode 100644 index bc447e71..00000000 --- a/packages/node-addon-examples/scripts/build-examples.mts +++ /dev/null @@ -1,14 +0,0 @@ -import { execSync } from "node:child_process"; - -import { findCMakeProjects } from "./cmake-projects.mjs"; - -const projectDirectories = findCMakeProjects(); - -for (const projectDirectory of projectDirectories) { - console.log(`Running "cmake-rn" in ${projectDirectory}`); - execSync("cmake-rn --configuration RelWithDebInfo", { - cwd: projectDirectory, - stdio: "inherit", - }); - console.log(); -} diff --git a/packages/node-addon-examples/scripts/cmake-projects.mts b/packages/node-addon-examples/scripts/cmake-projects.mts index 56aab0f0..22dcd5e7 100644 --- a/packages/node-addon-examples/scripts/cmake-projects.mts +++ b/packages/node-addon-examples/scripts/cmake-projects.mts @@ -1,26 +1,27 @@ -import { readdirSync, statSync } from "node:fs"; +import fs from "node:fs"; import path from "node:path"; -export const EXAMPLES_DIR = path.resolve(import.meta.dirname, "../examples"); -export const TESTS_DIR = path.resolve(import.meta.dirname, "../tests"); +export const PACKAGE_DIR = path.resolve(import.meta.dirname, ".."); +export const EXAMPLES_DIR = path.resolve(PACKAGE_DIR, "examples"); +export const TESTS_DIR = path.resolve(PACKAGE_DIR, "tests"); export const DIRS = [EXAMPLES_DIR, TESTS_DIR]; -export function findCMakeProjectsRecursively(dir: string): string[] { - let results: string[] = []; - const files = readdirSync(dir); - - for (const file of files) { - const fullPath = path.join(dir, file); - if (statSync(fullPath).isDirectory()) { - results = results.concat(findCMakeProjectsRecursively(fullPath)); - } else if (file === "CMakeLists.txt") { - results.push(dir); - } +/** + * Find the shallowest directories declaring a CMake project. + * + * Recursion stops at the first CMakeLists.txt found on a path: an example + * bringing its own nested CMake project has to be added to the root project + * once, since CMake requires target names to be unique across the project tree. + */ +export function findRootCMakeProjects(dir: string): string[] { + if (!fs.existsSync(dir)) { + return []; } - - return results; -} - -export function findCMakeProjects(): string[] { - return DIRS.flatMap(findCMakeProjectsRecursively); + if (fs.existsSync(path.join(dir, "CMakeLists.txt"))) { + return [dir]; + } + return fs + .readdirSync(dir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .flatMap((entry) => findRootCMakeProjects(path.join(dir, entry.name))); } diff --git a/packages/node-addon-examples/scripts/generate-root-project.mts b/packages/node-addon-examples/scripts/generate-root-project.mts new file mode 100644 index 00000000..8d2b4df9 --- /dev/null +++ b/packages/node-addon-examples/scripts/generate-root-project.mts @@ -0,0 +1,32 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { DIRS, PACKAGE_DIR, findRootCMakeProjects } from "./cmake-projects.mjs"; + +// A single root project lets cmake-rn build every example in one invocation. +// It is generated rather than globbed, because a glob is evaluated once at +// configure time and would miss examples copied in afterwards. +const projectDirectories = DIRS.flatMap(findRootCMakeProjects) + .map((directory) => + path.relative(PACKAGE_DIR, directory).split(path.sep).join(path.posix.sep), + ) + .sort(); + +const outputPath = path.join(PACKAGE_DIR, "CMakeLists.txt"); + +fs.writeFileSync( + outputPath, + [ + "# Generated by scripts/generate-root-project.mts - do not edit.", + "cmake_minimum_required(VERSION 3.15...3.31)", + "project(node-addon-examples)", + "", + ...projectDirectories.map((directory) => `add_subdirectory(${directory})`), + "", + ].join("\n"), + "utf-8", +); + +console.log( + `Generated ${path.relative(process.cwd(), outputPath)} with ${projectDirectories.length} sub-projects`, +); diff --git a/packages/node-addon-examples/scripts/verify-prebuilds.mts b/packages/node-addon-examples/scripts/verify-prebuilds.mts index cdbd312b..94b4e1bb 100644 --- a/packages/node-addon-examples/scripts/verify-prebuilds.mts +++ b/packages/node-addon-examples/scripts/verify-prebuilds.mts @@ -2,7 +2,7 @@ import fs from "node:fs"; import assert from "node:assert/strict"; import path from "node:path"; -import { EXAMPLES_DIR } from "./cmake-projects.mjs"; +import { DIRS } from "./cmake-projects.mjs"; const EXPECTED_ANDROID_ARCHS = ["armeabi-v7a", "arm64-v8a", "x86_64", "x86"]; @@ -82,17 +82,28 @@ async function verifyApplePrebuild(dirent: fs.Dirent) { } } -for await (const dirent of fs.promises.glob("**/*.*.node", { - cwd: EXAMPLES_DIR, - withFileTypes: true, -})) { - if (dirent.name.endsWith(".android.node")) { - await verifyAndroidPrebuild(dirent); - } else if (dirent.name.endsWith(".apple.node")) { - await verifyApplePrebuild(dirent); - } else { - throw new Error( - `Unexpected prebuild file: ${dirent.name} in ${dirent.parentPath}`, - ); +let verified = 0; + +for (const cwd of DIRS) { + for await (const dirent of fs.promises.glob("**/*.*.node", { + cwd, + withFileTypes: true, + })) { + if (dirent.name.endsWith(".android.node")) { + await verifyAndroidPrebuild(dirent); + } else if (dirent.name.endsWith(".apple.node")) { + await verifyApplePrebuild(dirent); + } else { + throw new Error( + `Unexpected prebuild file: ${dirent.name} in ${dirent.parentPath}`, + ); + } + verified++; } } + +// Without this, the script passes by simply not finding any prebuilds, which is +// exactly what happens if they stop being emitted next to the sources they were +// built from. +assert(verified > 0, `Found no prebuilds in ${DIRS.join(", ")}`); +console.log(`Verified ${verified} prebuilds`); diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index 67e5448b..ca9532d7 100644 --- a/packages/node-addon-examples/tests/async/CMakeLists.txt +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -3,24 +3,26 @@ project(async-test) find_package(weak-node-api REQUIRED CONFIG) -add_library(addon SHARED addon.c) +add_library(async-test-addon SHARED addon.c) option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) if(APPLE AND BUILD_APPLE_FRAMEWORK) - set_target_properties(addon PROPERTIES + set_target_properties(async-test-addon PROPERTIES FRAMEWORK TRUE MACOSX_FRAMEWORK_IDENTIFIER async-test.addon MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 XCODE_ATTRIBUTE_SKIP_INSTALL NO + OUTPUT_NAME addon ) else() - set_target_properties(addon PROPERTIES + set_target_properties(async-test-addon PROPERTIES PREFIX "" SUFFIX .node + OUTPUT_NAME addon ) endif() -target_link_libraries(addon PRIVATE weak-node-api) -target_compile_features(addon PRIVATE cxx_std_17) \ No newline at end of file +target_link_libraries(async-test-addon PRIVATE weak-node-api) +target_compile_features(async-test-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 da615db2..d9314224 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -3,24 +3,26 @@ project(buffers-test) find_package(weak-node-api REQUIRED CONFIG) -add_library(addon SHARED addon.c) +add_library(buffers-test-addon SHARED addon.c) option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) if(APPLE AND BUILD_APPLE_FRAMEWORK) - set_target_properties(addon PROPERTIES + set_target_properties(buffers-test-addon PROPERTIES FRAMEWORK TRUE MACOSX_FRAMEWORK_IDENTIFIER buffers-test.addon MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 XCODE_ATTRIBUTE_SKIP_INSTALL NO + OUTPUT_NAME addon ) else() - set_target_properties(addon PROPERTIES + set_target_properties(buffers-test-addon PROPERTIES PREFIX "" SUFFIX .node + OUTPUT_NAME addon ) endif() -target_link_libraries(addon PRIVATE weak-node-api) -target_compile_features(addon PRIVATE cxx_std_17) \ No newline at end of file +target_link_libraries(buffers-test-addon PRIVATE weak-node-api) +target_compile_features(buffers-test-addon PRIVATE cxx_std_17) \ No newline at end of file diff --git a/packages/node-addon-examples/tests/threadsafe-function/CMakeLists.txt b/packages/node-addon-examples/tests/threadsafe-function/CMakeLists.txt index 1be47aff..40ffb7f8 100644 --- a/packages/node-addon-examples/tests/threadsafe-function/CMakeLists.txt +++ b/packages/node-addon-examples/tests/threadsafe-function/CMakeLists.txt @@ -3,24 +3,26 @@ project(threadsafe-function-test) find_package(weak-node-api REQUIRED CONFIG) -add_library(addon SHARED addon.c) +add_library(threadsafe-function-test-addon SHARED addon.c) option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) if(APPLE AND BUILD_APPLE_FRAMEWORK) - set_target_properties(addon PROPERTIES + set_target_properties(threadsafe-function-test-addon PROPERTIES FRAMEWORK TRUE MACOSX_FRAMEWORK_IDENTIFIER threadsafe-function-test.addon MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 XCODE_ATTRIBUTE_SKIP_INSTALL NO + OUTPUT_NAME addon ) else() - set_target_properties(addon PROPERTIES + set_target_properties(threadsafe-function-test-addon PROPERTIES PREFIX "" SUFFIX .node + OUTPUT_NAME addon ) endif() -target_link_libraries(addon PRIVATE weak-node-api) -target_compile_features(addon PRIVATE cxx_std_17) +target_link_libraries(threadsafe-function-test-addon PRIVATE weak-node-api) +target_compile_features(threadsafe-function-test-addon PRIVATE cxx_std_17) \ No newline at end of file