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/gentle-pandas-learn.md b/.changeset/gentle-pandas-learn.md new file mode 100644 index 00000000..f291a8cd --- /dev/null +++ b/.changeset/gentle-pandas-learn.md @@ -0,0 +1,15 @@ +--- +"cmake-rn": minor +--- + +Let a consumer override the Android `ANDROID_STL` CMake cache variable via +the existing `-D`/`--define` option (e.g. `--define ANDROID_STL=c++_static`). +It still defaults to `c++_shared`, matching what React Native itself uses, +but an addon that must match a prebuilt third-party dependency's STL, or one +that's genuinely self-contained, can now ask for a different value. + +This also fixes an ordering bug where a `--define` targeting any of the +Android platform's own default CMake variables (including `ANDROID_STL`) was +silently discarded: our hardcoded defaults were appended to the CMake +command line _after_ the user-provided `-D` arguments, and CMake resolves a +cache variable set multiple times via `-D` to its last occurrence. 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/.changeset/release-debug-logging.md b/.changeset/release-debug-logging.md new file mode 100644 index 00000000..218065a6 --- /dev/null +++ b/.changeset/release-debug-logging.md @@ -0,0 +1,10 @@ +--- +"react-native-node-api": patch +--- + +Stop emitting `log_debug`'s per-addon diagnostic chatter (library +found/loaded, symbol resolution, ...) in release builds. It is now compiled +out in `NDEBUG` builds (CMake's `Release`/`MinSizeRel`/`RelWithDebInfo` +configurations, and Xcode's default `Release` configuration), mirroring React +Native's own dev/release logging split. `log_warning` and `log_error` are +unaffected and keep firing in every build type. diff --git a/.changeset/silly-cobras-invite.md b/.changeset/silly-cobras-invite.md new file mode 100644 index 00000000..6c79c0e3 --- /dev/null +++ b/.changeset/silly-cobras-invite.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": minor +--- + +Add a `--code-signing-allowed` flag to `cmake-rn`. `CODE_SIGNING_ALLOWED=NO` remains the default (needed for the free-standing dynamic libraries we produce), but a consumer who needs signed binaries in the XCFramework can now pass `--code-signing-allowed` to opt in. diff --git a/.changeset/wet-carrots-relax.md b/.changeset/wet-carrots-relax.md new file mode 100644 index 00000000..6050c6cb --- /dev/null +++ b/.changeset/wet-carrots-relax.md @@ -0,0 +1,5 @@ +--- +"ferric-cli": patch +--- + +Add `--dts-only` flag to `ferric build`, generating just the TypeScript declaration file and JS entrypoint without cross-compiling any Android/Apple binaries. It still runs a real host `cargo build` (napi-rs has no lighter typegen-only mode), so it's meant for regenerating a checked-in declarations fixture rather than for environments without a Rust toolchain. diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 173babe3..31fda3c4 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -4,6 +4,33 @@ Guidance specific to Claude Code sessions that open pull requests against this repository (including automated/scheduled sessions). See the root `CLAUDE.md` and `AGENTS.md` for everything else. +## Keep inline comments very brief — put the color in the PR description + +Default to **no comment at all**. Write one only when it carries knowledge a +reader cannot get from the code itself plus a `git blame` pointing at the PR +that introduced it. When you do write one, keep it to a line or two. + +Rationale, rejected alternatives, benchmark numbers, "we tried X and it +didn't work", links to upstream issues, and anything that reads as a history +lesson belong in the **PR description** (and, where user-facing, the +changeset) — not in the source. Those places are where a reader who has +already found the line via `git blame` will end up anyway, and they don't +have to be maintained as the code around them changes. + +Concretely, do not write comments that: + +- restate what the next line already says; +- explain why an alternative implementation was _not_ chosen; +- narrate the change (`// now compiled out in release builds`) — that is a + commit message, and it goes stale the moment the code moves; +- document a well-known toolchain fact (e.g. what `NDEBUG` means) that a + reader can look up. + +Comments that _do_ earn their place: a non-obvious constraint the compiler or +platform imposes, a workaround with the exact condition that makes it +removable (see the upstream-fix guidance in `AGENTS.md`), or a subtle +invariant a future edit could silently break. + ## Attach CI labels when you open a PR `.github/workflows/check.yml`'s `pull_request` trigger only fires on diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d72877f9..478d8634 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -62,24 +62,18 @@ jobs: uses: hendrikmuhs/ccache-action@v1.2.24 with: key: ${{ github.job }}-${{ runner.os }} - # Set up JDK and Android SDK only because we need weak-node-api, to build ferric-example and to run the linting - # TODO: Remove this once we have a way to run linting without building the native code - - name: Set up JDK 17 - uses: actions/setup-java@v6 - with: - java-version: "17" - distribution: "temurin" - - name: Setup Android SDK - uses: android-actions/setup-android@v4 - with: - packages: tools platform-tools ndk;${{ env.NDK_VERSION }} - - run: rustup target add x86_64-linux-android - run: pnpm install - run: pnpm run build - # Bootstrap weak-node-api and ferric-example to get types - # TODO: Solve this by adding an option to ferric to build only types or by committing the types into the repo as a fixture for an "init" command - - run: pnpm --filter weak-node-api run bootstrap - - run: pnpm --filter @react-native-node-api/ferric-example run bootstrap + # Generate the TypeScript/C++ declarations that other packages' type-checking + # depends on, without building any native binaries: weak-node-api's + # "prebuild:prepare" only copies headers and runs codegen (needs clang-format, + # set up above, but no JDK/Android SDK/NDK). ferric-example's declarations are + # committed as a fixture instead (see packages/ferric-example/.gitignore) — + # napi-rs's dts generation has no way to run without a real `cargo build` + # (confirmed: it leaves a populated target/ directory), so unlike + # weak-node-api's codegen it can't be reproduced here without reintroducing a + # native build into the fastest-feedback job. See #414. + - run: pnpm --filter weak-node-api run prebuild:prepare - run: pnpm run lint env: DEBUG: eslint:eslint @@ -372,6 +366,9 @@ jobs: - name: Build ferric-example for all architectures run: pnpm run build --android working-directory: packages/ferric-example + - name: Verify committed ferric-example TypeScript declarations are up to date + run: git diff --exit-code -- ferric_example.d.ts ferric_example.js + working-directory: packages/ferric-example - name: Run tests (Android) timeout-minutes: 75 uses: reactivecircus/android-emulator-runner@v2 @@ -439,18 +436,28 @@ jobs: # Build Ferric example for all Apple architectures - run: pnpm exec ferric --apple working-directory: packages/ferric-example + - name: Verify committed ferric-example TypeScript declarations are up to date + run: git diff --exit-code -- ferric_example.d.ts ferric_example.js + working-directory: packages/ferric-example - name: Inspect the structure of the prebuilt binary - run: lipo -info ferric_example.apple.node/*/libferric_example.framework/libferric_example > lipo-info.txt + run: | + lipo -info ferric_example.apple.node/*/libferric_example.framework/libferric_example > lipo-output.txt + otool -L ferric_example.apple.node/*/libferric_example.framework/libferric_example > otool-output.txt working-directory: packages/ferric-example - - name: Upload lipo info + - name: Upload lipo output + uses: actions/upload-artifact@v7 + with: + name: lipo-output + path: packages/ferric-example/lipo-output.txt + - name: Upload otool output uses: actions/upload-artifact@v7 with: - name: lipo-info - path: packages/ferric-example/lipo-info.txt + name: otool-output + path: packages/ferric-example/otool-output.txt - name: Verify Apple triplet builds run: | # Create expected fixture content - cat > expected-lipo-info.txt << 'EOF' + cat > expected-lipo-output.txt << 'EOF' Architectures in the fat file: ferric_example.apple.node/ios-arm64_x86_64-simulator/libferric_example.framework/libferric_example are: x86_64 arm64 Architectures in the fat file: ferric_example.apple.node/macos-arm64_x86_64/libferric_example.framework/libferric_example are: x86_64 arm64 Architectures in the fat file: ferric_example.apple.node/tvos-arm64_x86_64-simulator/libferric_example.framework/libferric_example are: x86_64 arm64 @@ -460,5 +467,24 @@ jobs: Non-fat file: ferric_example.apple.node/xros-arm64/libferric_example.framework/libferric_example is architecture: arm64 EOF # Compare with expected fixture (will fail if files differ) - diff expected-lipo-info.txt lipo-info.txt + diff expected-lipo-output.txt lipo-output.txt + # Verify every binary depends on the weak-node-api framework. + # otool -L prints one header line per file, or one per architecture + # when the file is fat, so the number of headers is exactly the number + # of weak-node-api dependencies we expect. Deriving it beats + # hard-coding a count, which silently rots whenever a triplet is added + # or dropped. + # macOS frameworks use the versioned bundle layout, so their install + # name is .../weak-node-api.framework/Versions//weak-node-api + # where the embedded platforms get the flat + # .../weak-node-api.framework/weak-node-api — hence the optional + # "Versions//" in the pattern. + SLICE_COUNT=$(grep -c "^ferric_example\.apple\.node/.*:$" otool-output.txt || true) + WEAK_NODE_API_COUNT=$(grep -cE "@rpath/weak-node-api\.framework/(Versions/[^/]+/)?weak-node-api" otool-output.txt || true) + echo "Found $WEAK_NODE_API_COUNT weak-node-api dependencies across $SLICE_COUNT binaries" + if [ "$SLICE_COUNT" -eq 0 ] || [ "$WEAK_NODE_API_COUNT" -ne "$SLICE_COUNT" ]; then + echo "Expected $SLICE_COUNT dependencies on the weak-node-api framework (one per binary), found $WEAK_NODE_API_COUNT" + cat otool-output.txt + exit 1 + fi working-directory: packages/ferric-example diff --git a/.prettierignore b/.prettierignore index 23bef2b3..c8aaf613 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,3 +14,10 @@ packages/node-addon-examples/examples packages/node-tests/node packages/node-tests/tests packages/node-tests/*.generated.js + +# Committed napi-rs codegen fixture (see packages/ferric-example/.gitignore) — left +# in napi-rs's own output formatting so `pnpm run build:types` reproduces it exactly +# and the CI drift check (see .github/workflows/check.yml) doesn't false-positive on +# formatting alone. +packages/ferric-example/ferric_example.d.ts +packages/ferric-example/ferric_example.js 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.test.ts b/packages/cmake-rn/src/platforms/android.test.ts new file mode 100644 index 00000000..88a4011e --- /dev/null +++ b/packages/cmake-rn/src/platforms/android.test.ts @@ -0,0 +1,60 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { toDefineArguments } from "../helpers.js"; +import { buildCommonDefinitions } from "./android.js"; + +function baseArgs( + overrides: Partial[0]> = {}, +) { + return { + configuration: "Release" as const, + ndkPath: "/opt/ndk", + androidSdkVersion: "24", + ccachePath: null, + define: [], + ...overrides, + }; +} + +describe("buildCommonDefinitions", () => { + it("defaults ANDROID_STL to c++_shared", () => { + const args = toDefineArguments(buildCommonDefinitions(baseArgs())); + const index = args.indexOf("-D"); + assert(index >= 0); + assert(args.includes("ANDROID_STL=c++_shared")); + }); + + it("lets a consumer override ANDROID_STL via --define", () => { + // CMake resolves a cache variable passed multiple times via `-D` to its + // *last* occurrence on the command line, so what matters is which + // ANDROID_STL entry comes last - not merely that c++_static is present. + const args = toDefineArguments( + buildCommonDefinitions( + baseArgs({ define: [{ ANDROID_STL: "c++_static" }] }), + ), + ); + const stlEntries = args.filter((arg) => arg.startsWith("ANDROID_STL=")); + assert.deepEqual(stlEntries, [ + "ANDROID_STL=c++_shared", + "ANDROID_STL=c++_static", + ]); + }); + + it("applies the user's --define after (so it wins over) every default", () => { + const definitions = buildCommonDefinitions( + baseArgs({ define: [{ ANDROID_STL: "c++_static" }] }), + ); + // The user-provided define must be the last entry, since CMake resolves + // a -D variable passed multiple times to its last occurrence. + assert.deepEqual(definitions.at(-1), { ANDROID_STL: "c++_static" }); + }); + + it("includes ccache launcher variables when a ccache path is given", () => { + const args = toDefineArguments( + buildCommonDefinitions(baseArgs({ ccachePath: "/usr/bin/ccache" })), + ); + assert(args.includes("CMAKE_C_COMPILER_LAUNCHER=/usr/bin/ccache")); + assert(args.includes("CMAKE_CXX_COMPILER_LAUNCHER=/usr/bin/ccache")); + }); +}); diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index da5f19c2..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, @@ -91,6 +91,56 @@ function getNdkLlvmBinPath(ndkPath: string) { return path.join(prebuiltPath, platforms[0], "bin"); } +const DEFAULT_ANDROID_STL = "c++_shared"; + +/** + * Builds the list of CMake cache variable definitions common to every + * triplet's configure step. + * + * `define` (populated from the repeatable `-D`/`--define` CLI option) is + * spread last, so a consumer's explicit `-D ANDROID_STL=c++_static` (or any + * other variable set here by default) takes precedence over our own + * defaults: CMake resolves a cache variable passed multiple times via `-D` + * to its last occurrence on the command line. + */ +export function buildCommonDefinitions({ + configuration, + ndkPath, + androidSdkVersion, + ccachePath, + define, +}: { + configuration: BaseOpts["configuration"]; + ndkPath: string; + androidSdkVersion: string; + ccachePath: BaseOpts["ccachePath"]; + define: BaseOpts["define"]; +}) { + return [ + { + CMAKE_BUILD_TYPE: configuration, + CMAKE_SYSTEM_NAME: "Android", + // "CMAKE_INSTALL_PREFIX": installPath, + CMAKE_MAKE_PROGRAM: "ninja", + ANDROID_NDK: ndkPath, + ANDROID_TOOLCHAIN: "clang", + ANDROID_PLATFORM: androidSdkVersion, + // Defaults to c++_shared, matching what React Native itself uses. + // Override with -D/--define ANDROID_STL=c++_static (or another value + // accepted by the NDK's CMake toolchain) when an addon must match a + // prebuilt third-party dependency's STL. + ANDROID_STL: DEFAULT_ANDROID_STL, + }, + ccachePath + ? { + CMAKE_C_COMPILER_LAUNCHER: ccachePath, + CMAKE_CXX_COMPILER_LAUNCHER: ccachePath, + } + : {}, + ...define, + ]; +} + export const platform: Platform = { id: "android", name: "Android", @@ -140,31 +190,17 @@ export const platform: Platform = { const ndkPath = getNdkPath(ndkVersion); const toolchainPath = getNdkToolchainPath(ndkPath); - const commonDefinitions = [ - ...define, - { - CMAKE_BUILD_TYPE: configuration, - CMAKE_SYSTEM_NAME: "Android", - // "CMAKE_INSTALL_PREFIX": installPath, - CMAKE_MAKE_PROGRAM: "ninja", - ANDROID_NDK: ndkPath, - ANDROID_TOOLCHAIN: "clang", - ANDROID_PLATFORM: androidSdkVersion, - // TODO: Make this configurable - ANDROID_STL: "c++_shared", - }, - ccachePath - ? { - CMAKE_C_COMPILER_LAUNCHER: ccachePath, - CMAKE_CXX_COMPILER_LAUNCHER: ccachePath, - } - : {}, - ]; + const commonDefinitions = buildCommonDefinitions({ + configuration, + ndkPath, + androidSdkVersion, + ccachePath, + define, + }); 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, @@ -189,7 +225,6 @@ export const platform: Platform = { ...commonDefinitions, { // "CPACK_SYSTEM_NAME": `Android-${architecture}`, - CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath, ANDROID_ABI: ANDROID_ARCHITECTURES[triplet], }, ]), @@ -210,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) { @@ -232,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({ @@ -281,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 36c595b9..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); } @@ -161,16 +161,22 @@ const appleBundleIdentifierOption = new Option( "Unique CFBundleIdentifier used for Apple framework artifacts", ).default(undefined, "com.callstackincubator.node-api.{libraryName}"); +const codeSigningAllowedOption = new Option( + "--code-signing-allowed", + "Allow code signing when building free dynamic libraries (passed as CODE_SIGNING_ALLOWED to xcodebuild)", +).default(false); + type AppleOpts = { xcframeworkExtension: boolean; appleBundleIdentifier?: string; + codeSigningAllowed: boolean; }; 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[], @@ -180,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 = [ @@ -259,7 +258,8 @@ export const platform: Platform = { amendCommand(command) { return command .addOption(xcframeworkExtensionOption) - .addOption(appleBundleIdentifierOption); + .addOption(appleBundleIdentifierOption) + .addOption(codeSigningAllowedOption); }, assertValidTriplets(triplets) { for (const suffix of SIMULATOR_TRIPLET_SUFFIXES) { @@ -356,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", + "$", + ), }, ]), ]); @@ -366,72 +380,60 @@ export const platform: Platform = { }, async build( { spawn, triplet }, - { build, target, configuration, appleBundleIdentifier }, + { 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, @@ -440,119 +442,151 @@ export const platform: Platform = { ...(target.length > 0 ? ["--target", ...target] : []), "--", - // Skip code-signing (needed when building free dynamic libraries) - // TODO: Make this configurable - "CODE_SIGNING_ALLOWED=NO", + // Skip code-signing by default (needed when building free dynamic + // libraries), but let a consumer opt into signed binaries via + // --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/ferric-example/.gitignore b/packages/ferric-example/.gitignore index f1d36c32..a225cf72 100644 --- a/packages/ferric-example/.gitignore +++ b/packages/ferric-example/.gitignore @@ -3,7 +3,3 @@ /*.xcframework/ /*.apple.node/ /*.android.node/ - -# Generated files -/ferric_example.d.ts -/ferric_example.js diff --git a/packages/ferric-example/ferric_example.d.ts b/packages/ferric-example/ferric_example.d.ts new file mode 100644 index 00000000..3b38df96 --- /dev/null +++ b/packages/ferric-example/ferric_example.d.ts @@ -0,0 +1,11 @@ +/** + * This file was generated by + * ╭─────────────────────────╮ + * │░█▀▀░█▀▀░█▀▄░█▀▄░▀█▀░█▀▀░│ + * │░█▀▀░█▀▀░█▀▄░█▀▄░░█░░█░░░│ + * │░▀░░░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░│ + * ╰─────────────────────────╯ + * Powered by napi.rs + */ +/* eslint-disable */ +export declare function sum(a: number, b: number): number diff --git a/packages/ferric-example/ferric_example.js b/packages/ferric-example/ferric_example.js new file mode 100644 index 00000000..69fff2e3 --- /dev/null +++ b/packages/ferric-example/ferric_example.js @@ -0,0 +1,13 @@ +/* eslint-disable */ + +/** + * This file was generated by + * ╭─────────────────────────╮ + * │░█▀▀░█▀▀░█▀▄░█▀▄░▀█▀░█▀▀░│ + * │░█▀▀░█▀▀░█▀▄░█▀▄░░█░░█░░░│ + * │░▀░░░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░│ + * ╰─────────────────────────╯ + * Powered by napi.rs + */ + +module.exports = require('./ferric_example.node'); diff --git a/packages/ferric-example/package.json b/packages/ferric-example/package.json index 28735907..3e9fd232 100644 --- a/packages/ferric-example/package.json +++ b/packages/ferric-example/package.json @@ -19,7 +19,8 @@ ], "scripts": { "build": "ferric build", - "bootstrap": "node --run build" + "bootstrap": "node --run build", + "build:types": "ferric build --dts-only" }, "dependencies": { "react-native-node-api": "workspace:*" diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 6cda0582..5a72ec03 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -25,7 +25,7 @@ import { determineLibraryBasename, } from "react-native-node-api"; -import { ensureCargo, build } from "./cargo.js"; +import { ensureCargo, build, determineCargoLibraryName } from "./cargo.js"; import { ALL_TARGETS, ANDROID_TARGETS, @@ -104,6 +104,10 @@ const xcframeworkExtensionOption = new Option( "--xcframework-extension", "Don't rename the xcframework to .apple.node", ).default(false); +const dtsOnlyOption = new Option( + "--dts-only", + "Only generate the TypeScript declarations and entrypoint, skipping Android/Apple cross-compilation. Still runs a real `cargo build` for the host target (napi-rs has no lighter typegen-only mode), so this is not a no-op — it's meant for regenerating a checked-in declarations fixture, not for toolchain-free environments.", +).default(false); const outputPathOption = new Option( "--output ", @@ -153,6 +157,7 @@ export const buildCommand = new Command("build") .addOption(appleBundleIdentifierOption) .addOption(concurrencyOption) .addOption(verboseOption) + .addOption(dtsOnlyOption) .action( wrapAction( async ({ @@ -167,7 +172,53 @@ export const buildCommand = new Command("build") appleBundleIdentifier, concurrency, verbose, + dtsOnly, }) => { + if (dtsOnly) { + assertFixable( + targetArg.length === 0 && !apple && !android && !clean, + "The --dts-only flag cannot be combined with --target, --apple, --android or --clean", + { + instructions: + "Drop --dts-only to build native binaries, or remove the other flags to only generate TypeScript declarations", + }, + ); + ensureCargo(); + const libraryName = determineCargoLibraryName(process.cwd()); + const declarationsFilename = `${libraryName}.d.ts`; + const declarationsPath = path.join(outputPath, declarationsFilename); + await oraPromise( + generateTypeScriptDeclarations({ + outputFilename: declarationsFilename, + createPath: process.cwd(), + outputPath, + }), + { + text: "Generating TypeScript declarations", + successText: `Generated TypeScript declarations ${prettyPath( + declarationsPath, + )}`, + failText: (error) => + `Failed to generate TypeScript declarations: ${error.message}`, + }, + ); + const entrypointPath = path.join(outputPath, `${libraryName}.js`); + await oraPromise( + generateEntrypoint({ + libraryName, + outputPath: entrypointPath, + }), + { + text: `Generating entrypoint`, + successText: `Generated entrypoint into ${prettyPath( + entrypointPath, + )}`, + failText: (error) => + `Failed to generate entrypoint: ${error.message}`, + }, + ); + return; + } if (clean) { await oraPromise( () => spawn("cargo", ["clean"], { outputMode: "buffered" }), diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index fc4fb2ac..31c8eaa3 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -93,6 +93,36 @@ export function ensureCargo() { } } +type CargoMetadata = { + packages: { targets: { name: string; kind: string[] }[] }[]; +}; + +/** + * Determine the name of the crate's "cdylib" target, without building anything, + * by asking cargo for its metadata. This matches the basename a full build would + * produce (e.g. "ferric_example" for a crate named "ferric-example"), since cargo + * normalizes the crate name (dashes to underscores) for the compiled artifact. + */ +export function determineCargoLibraryName(cwd: string): string { + const output = cp.execFileSync( + "cargo", + ["metadata", "--no-deps", "--format-version", "1"], + { cwd, encoding: "utf-8" }, + ); + const { packages } = JSON.parse(output) as CargoMetadata; + const cdylibNames = packages + .flatMap((pkg) => pkg.targets) + .filter((target) => target.kind.includes("cdylib")) + .map((target) => target.name); + const candidates = new Set(cdylibNames); + assert( + candidates.size === 1, + `Expected exactly one cdylib target, got: ${[...candidates].join(", ")}`, + ); + const [name] = candidates; + return name; +} + type BuildOptions = { configuration: "debug" | "release"; verbose: boolean; 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/host/cpp/Logger.cpp b/packages/host/cpp/Logger.cpp index b863fcdf..ce9d7c0c 100644 --- a/packages/host/cpp/Logger.cpp +++ b/packages/host/cpp/Logger.cpp @@ -63,13 +63,15 @@ void log_message_internal(LogLevel level, const char *format, va_list args) { namespace callstack::react_native_node_api { +#ifndef NDEBUG void log_debug(const char *format, ...) { - // TODO: Disable logging in release builds va_list args; va_start(args, format); log_message_internal(LogLevel::Debug, format, args); va_end(args); } +#endif + void log_warning(const char *format, ...) { va_list args; va_start(args, format); diff --git a/packages/host/cpp/Logger.hpp b/packages/host/cpp/Logger.hpp index c064e7da..7bec0047 100644 --- a/packages/host/cpp/Logger.hpp +++ b/packages/host/cpp/Logger.hpp @@ -3,7 +3,16 @@ #include namespace callstack::react_native_node_api { + +// Inline (rather than a no-op in Logger.cpp) to let the optimizer drop the +// argument evaluation at every call site. +#ifdef NDEBUG +inline void log_debug(const char *, ...) {} +#else void log_debug(const char *format, ...); +#endif + void log_warning(const char *format, ...); void log_error(const char *format, ...); + } // namespace callstack::react_native_node_api diff --git a/packages/host/src/node/cli/hermes.ts b/packages/host/src/node/cli/hermes.ts index 4b41692c..8af05fd4 100644 --- a/packages/host/src/node/cli/hermes.ts +++ b/packages/host/src/node/cli/hermes.ts @@ -44,7 +44,7 @@ async function patchJsiHeaders({ successText: "Copied JSI from patched Hermes to React Native", failText: (err) => `Failed to copy JSI from Hermes to React Native: ${err.message}`, - isEnabled: !silent, + isSilent: silent, }, ); } @@ -99,7 +99,7 @@ export const command = new Command("vendor-hermes") successText: "Removed existing Hermes clone", failText: (error) => `Failed to remove existing Hermes clone: ${error.message}`, - isEnabled: !silent, + isSilent: silent, }, ); } @@ -128,7 +128,7 @@ export const command = new Command("vendor-hermes") successText: "Cloned custom Hermes", failText: (err) => `Failed to clone custom Hermes: ${err.message}`, - isEnabled: !silent, + isSilent: silent, }, ); } catch (error) { 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