Multi addon projects - #413
Merged
Merged
Conversation
kraenhansen
force-pushed
the
claude/pr-363-feedback-qqu312
branch
from
August 13, 2026 04:49
2ed7ed3 to
01a37f9
Compare
Closed
kraenhansen
marked this pull request as ready for review
August 13, 2026 04:59
kraenhansen
force-pushed
the
claude/pr-363-feedback-qqu312
branch
from
August 13, 2026 05:03
01a37f9 to
f48956e
Compare
kraenhansen
force-pushed
the
claude/pr-363-feedback-qqu312
branch
3 times, most recently
from
August 13, 2026 11:49
5c75b42 to
938a205
Compare
A project declaring multiple addons wrote every prebuild into a single
output directory, named after the CMake target. Both the location and the
name are now derived per target from the CMake File API:
- The output directory defaults to {targetSourceDir}/build/{configuration},
where the new {targetSourceDir} placeholder expands to the target's own
source directory. A single-addon project reports "." and so resolves to
the same path as before.
- The prebuild is named after the artifact on disk (the target's
OUTPUT_NAME) rather than the target name, so a target renamed to avoid a
clash within the project still produces the name the JS require expects.
Together this keeps a prebuild where the Babel plugin and auto-linking
resolve it from, and reduces --namespaced-targets to an internal concern.
Also fixes, in the same area:
- gyp-to-cmake emitted OUTPUT_NAME regardless of --namespaced-targets, due
to an always-truthy condition, and never emitted it for Apple framework
targets, which CMake names after it.
- The Apple build ran a full "cmake --build" once per shared library,
concurrently against one build tree, and called "xcodebuild -list" (a
synchronous spawn) once per library per triplet.
- xcodebuild invocations now run in sequence per build directory, as
concurrent invocations against a single Xcode project are not reliable.
- postBuild looked for "<target name>.framework" while createAppleFramework
names it after the artifact, so the two diverged under namespacing.
- --concurrency accepted any value, and did not implement the documented
fallback to 1 under --verbose. Max listeners is now derived from it.
- verify-prebuilds globbed a directory the prebuilds had moved out of, so
it passed by finding nothing. It now covers tests/ too and requires a
non-zero count.
- The root example project globbed recursively, which both missed examples
copied in after configure and would add a nested project twice. It is
now generated from the same script pipeline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfKQDvEkxNtkSE4aaF9yG8
kraenhansen
force-pushed
the
claude/pr-363-feedback-qqu312
branch
from
August 13, 2026 12:34
938a205 to
d3dc08f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an ability for
cmake-rnto build projects declaring multiple addons in a single invocation.Supersedes #363, which this started as a follow-up to. It carries that PR's commits, rebased onto
next, plus the work needed to resolve the question it was drafted on — where the final output should go.Building many addons at once
gyp-to-cmakegains--namespaced-targets. CMake requires target names to be unique across a project tree, so sub-projects that each declare anaddontarget cannot be added to one root project. The flag prefixes the target name with the project name.cmake-rnthen handles a project with more than one shared library target, rather than asserting there is exactly one.packages/node-addon-examplesuses this: instead of runningcmake-rnonce per example, a generated root project adds every example as a sub-directory and a single invocation builds them all.Where the output goes
Naming the artifacts after the (now namespaced) CMake target and writing them all into one directory breaks the consumer side in two ways:
plugin.ts, andfindNodeAddonForBindingsinpath-utils.ts), so a prebuild collected into one shared directory is invisible to it.getLibraryNamederives the library name from the prebuild's path within its package, and the auto-linker renames the.sotolib${libraryName}.so. The plugin derives the same name from the require path. Move or rename the prebuild and the two disagree.So both are derived per target from the CMake File API instead:
{targetSourceDir}/build/{configuration}. The new{targetSourceDir}placeholder expands totarget.paths.source, resolved against--source. A project declaring a single addon at the top level reports., so it resolves to the same path as before.OUTPUT_NAME, which--namespaced-targetssets back to the original addon name — rather than the CMake target name. Anylibprefix CMake adds is stripped, so a target that does not clearPREFIX(weak-node-apibuilds alibweak-node-api.so) keeps emitting the prebuild name it does today — the onepackages/host/android/build.gradlepoints itsjniLibsat.The net effect is that each addon lands exactly where building it on its own would have put it, and
--namespaced-targetsstays an internal CMake concern that nothing downstream can observe.Grouping is keyed by CMake target name, which CMake guarantees unique within a project. The artifact name is deliberately not used as the key, since every addon may well build an
addon.node.The Apple build directory has to be per target
Because every target now shares one
OUTPUT_NAME, theCMAKE_LIBRARY_OUTPUT_DIRECTORYthe Apple path sets (to work around CMake #24161) can no longer be shared. With one directory, all 14 example targets resolve to just 3 artifact paths:Every target then overwrites the previous one's framework, and each prebuild gets assembled from whichever target built last.
Test app (iOS)caught this on an earlier push: the app archived fine butsimctl installrefused it, because two addons carried the same framework identity —tests/threadsafe-functionsorts last among the sub-projects, so its framework was the one left behind for all of them. Appending$<TARGET_PROPERTY:NAME>to the output directory keeps the #24161 workaround while giving each target its own: 14 targets, 14 distinct artifact paths, verified against real CMake.Verified against real CMake
Configuring the generated root project and running the resolver over the File API output:
./build/RelWithDebInfois on thebindingslookup path and the basenames are the original addon names, sorequire('bindings')('hello')resolves. Separately confirmed against real CMake: a single-addon project reportspaths.source === "."and emits to exactly the path it does today, and a target that does not clearPREFIXreports alibweak-node-api.soartifact.Also in here
cmake-rngains--concurrency, bounding how many build tasks run at once. It defaults to the available parallelism, or 1 under--verbose, since interleaved output from concurrent builds is unreadable.EventEmitter.defaultMaxListenersis derived from it — each spawned child attaches threeprocesslisteners and removes them on exit, so the live count tracks concurrent children.xcodebuildinvocations run in sequence per build directory, since concurrent invocations against a single Xcode project and its derived data are not reliable.xcodebuild -listruns once per build directory and asynchronously, rather than once per library as a synchronous spawn.cmake --buildbuilds all requested targets in one invocation.scripts/generate-root-project.mtsrather than globbed. Afile(GLOB_RECURSE ...)is evaluated once at configure time, so it would miss examples copied in afterwards, and being recursive it wouldadd_subdirectory()both a parent and a nested project — three allow-list entries incopy-examples.mtsare commented out as "Brings its own CMake project 👀". The generator stops recursing at the firstCMakeLists.txton a path. Being generated fromexamples/(gitignored), the file is gitignored too.verify-prebuildsnow coverstests/as well asexamples/, and asserts it found a non-zero number of prebuilds, so it cannot pass by simply finding none.findCMakeProjects/findCMakeProjectsRecursivelyare gone along withbuild-examples.mts.Changesets
--concurrency. Notes the two things an existing user can observe:--outno longer defaults inside--build(same path unless--buildpoints outside the source directory), and the artifact is named afterOUTPUT_NAME(the same thing unless it is set explicitly).--namespaced-targets.node-addon-examplesis private, so it gets none.Testing
Run on Linux, against
nextrebased ontomain:pnpm run build,pnpm exec eslint .,pnpm exec prettier --check .— all clean.pnpm --filter gyp-to-cmake run test— 30 pass, including new--namespaced-targetscoverage, of which there was none.pnpm --filter cmake-rn run test— 12 pass, including new coverage for the output-path resolution and artifact naming.pnpm --filter react-native-node-api run test— 50 pass, 4 fail. The 4 arepath-utilspermission tests that cannot pass as root (chmod 000does not stop root); nothing inpackages/hostis touched here.weak-node-apiconfig: it configures without duplicate targets, and all 14 targets resolve to distinct prebuild paths next to their own sources.The native builds could not be run locally — this is a Linux worker, and per
AGENTS.mdnative builds are not bootstrapped there — so Test app (Android) and Test app (iOS), plus the Weak Node-API jobs, are the real end-to-end check. The last of those covers thePREFIXcase described above, and the iOS job is what surfaced the shared-output-directory collision.Test app (macOS)is deliberately not labelled: it stays blocked on react-native-macos shipping a release based on React Native ≥ 0.82, per #392. Enabling it earlier reproduced exactly thePlease set CMAKE_BUILD_TYPEHermesc failure that issue documents, which is unrelated to this change.🤖 Generated with Claude Code
https://claude.ai/code/session_01KfKQDvEkxNtkSE4aaF9yG8