Skip to content

Commit ab6e590

Browse files
committed
fix(spm): alias self-managed lib refs via libs/<Name> to avoid SPM identity collision
A community library that ships its `Package.swift` inside an `ios/` subdir (common convention — e.g. @chrfalch/react-native-calculator/ios/Package.swift) collides with our codegen package at build/generated/ios/Package.swift: both have path basename "ios", which is what SPM uses to derive package identity. SPM rejects with "Conflicting identity for ios". Worse, two libs that both ship Package.swift in ios/ collide with each other regardless of where our codegen lives. The fix routes every self-managed dep through a uniquely-named symlink: build/generated/autolinking/ Package.swift libs/ # NEW Calculator → <abs>/node_modules/@chrfalch/.../ios ReactNativeSafeAreaContext → <abs>/node_modules/react-native-safe-area-context packages/ # unchanged (wrapper synths, already unique) The aggregator emits `.package(name: ..., path: "libs/<SwiftName>")` instead of an absolute path. SPM derives identity from the symlink basename — the Swift module name — which is guaranteed unique per autolinker construction. `libs/` is wiped + repopulated on every autolinker run, so stale entries for uninstalled deps don't linger. Also reverts an aborted exploration that moved the codegen Package.swift to build/generated/codegen/ — the lib-aliases approach is the better answer because it solves lib-vs-lib collisions, not just codegen-vs-lib.
1 parent 79ee3bc commit ab6e590

3 files changed

Lines changed: 62 additions & 5 deletions

File tree

packages/react-native/scripts/spm/__doc__/rfc-spm-xcframework.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,11 @@ needed.
117117
│ │ ├── autolinking/ (generated) │
118118
│ │ │ ├── Package.swift │
119119
│ │ │ ├── autolinking.json │
120-
│ │ │ └── packages/ (synth wrappers) │
120+
│ │ │ ├── packages/ (synth wrappers) │
121+
│ │ │ └── libs/ (alias symlinks │
122+
│ │ │ for self-managed│
123+
│ │ │ deps; basename │
124+
│ │ │ = SwiftName) │
121125
│ │ └── ios/ (codegen) │
122126
│ └── xcframeworks/ (symlinks) │
123127
│ ├── Package.swift │
@@ -310,6 +314,28 @@ Each entry becomes a target in `autolinked/Package.swift`. Sources outside the
310314
autolinked directory are mirrored with **file-level symlinks** (SPM rejects
311315
directory symlinks that resolve outside the package root).
312316

317+
### Self-managed deps and package identity
318+
319+
A community library that ships its own `Package.swift` (instead of being
320+
wrapped by the autolinker) is referenced directly. SPM derives the package
321+
identity for a `.package(path:)` dependency from the path's basename — and
322+
a common convention is to ship the manifest inside an `ios/` subdir
323+
(`<dep>/ios/Package.swift`). Two libs following that convention would both
324+
have identity `"ios"`, and SPM rejects with `Conflicting identity for ios`.
325+
326+
To make every reference globally unique by construction, the autolinker
327+
materializes each self-managed dep as a symlink at
328+
`build/generated/autolinking/libs/<SwiftName>/` pointing at the dep's real
329+
manifest dir. The aggregator `Package.swift` then references the symlink
330+
(`path: "libs/<SwiftName>"`), and SPM uses the symlink basename — the
331+
library's Swift module name — as the package identity. Swift module names
332+
are already unique per dep (deriving from the npm package name), so this
333+
sidesteps the collision in all cases, including against the codegen
334+
package at `build/generated/ios/`.
335+
336+
The `libs/` directory is wiped and recreated on every autolinker run, so
337+
stale aliases for uninstalled deps disappear automatically.
338+
313339
### Third-party library support
314340

315341
The current implementation handles React Native's own frameworks and app-local

packages/react-native/scripts/spm/__doc__/spm-scripts.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ my-app/ios/
7676
autolinking/ <-- gitignored (regenerated at build time)
7777
Package.swift
7878
autolinking.json
79-
packages/ <-- package wrappers for native modules
79+
packages/ <-- synth wrappers for autolinker-managed deps
80+
libs/ <-- symlinks to self-managed deps' Package.swift
81+
dirs, named by Swift module so SPM
82+
package identity stays unique
8083
headers/ <-- generated header symlinks
8184
ios/ <-- gitignored, codegen output
8285
xcframeworks/ <-- gitignored, symlinks to cached artifacts
@@ -177,6 +180,21 @@ Each entry becomes a target in `build/generated/autolinking/Package.swift`.
177180
Sources outside `build/generated/autolinking/` are automatically mirrored with
178181
file-level symlinks.
179182

183+
## Self-managed community packages
184+
185+
A community library that ships its own `Package.swift` is referenced
186+
directly by the autolinker instead of being wrapped. To keep SPM's
187+
package identity (which it derives from the path basename) unique across
188+
deps — even when several libs put their manifest inside an `ios/` subdir
189+
— each self-managed dep is exposed through a uniquely-named symlink at
190+
`build/generated/autolinking/libs/<SwiftName>/`. The aggregator
191+
`Package.swift` references that path, so two libs both shipping
192+
`<dep>/ios/Package.swift` never collide on identity `"ios"`.
193+
194+
The `libs/` directory is wiped and recreated on every autolinker run,
195+
so deleting a dep via `npm uninstall` cleans up the alias automatically
196+
on the next build.
197+
180198
## Header Resolution
181199

182200
React Native uses CocoaPods-style imports (`#import <React/RCTBridge.h>`) that

packages/react-native/scripts/spm/generate-spm-autolinking.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,8 +1180,16 @@ function main(argv /*:: ?: Array<string> */) /*: void */ {
11801180

11811181
const packagesDir = path.join(outputDir, 'packages');
11821182
const headersDir = path.join(outputDir, 'headers');
1183+
// libs/<SwiftName>/ symlinks for self-managed deps. The symlink basename
1184+
// is the Swift module name (guaranteed unique per dep), so SPM's
1185+
// path-basename-based package identity never collides — even when two
1186+
// libs ship their own Package.swift inside `ios/` (a common convention).
1187+
// Wiped on every run; populated below as self-managed deps are visited.
1188+
const libsDir = path.join(outputDir, 'libs');
11831189
fs.mkdirSync(packagesDir, {recursive: true});
11841190
fs.mkdirSync(headersDir, {recursive: true});
1191+
fs.rmSync(libsDir, {recursive: true, force: true});
1192+
fs.mkdirSync(libsDir, {recursive: true});
11851193

11861194
const wrapperDirs /*: Map<string, string> */ = new Map();
11871195
const selfManagedDirs /*: Map<string, string> */ = new Map();
@@ -1254,11 +1262,16 @@ function main(argv /*:: ?: Array<string> */) /*: void */ {
12541262
// outside of ios/), and cross-package consumers should still resolve
12551263
// them via the centralized -I path.
12561264
linkHeaderTree(absSource, path.join(headersDir, target.name));
1257-
// packagePath is the directory containing Package.swift — for the
1258-
// nested layout this is <dep>/ios, not the dep root.
1265+
// Route the manifest reference through a uniquely-named symlink at
1266+
// libs/<SwiftName>/ so SPM derives the package identity from the
1267+
// alias basename. Two libs that both ship Package.swift inside their
1268+
// own `ios/` subdir would otherwise collide with identity "ios".
1269+
const realPackageDir = selfManagedDirs.get(target.name) ?? absSource;
1270+
const aliasPath = path.join(libsDir, target.name);
1271+
ensureSymlink(aliasPath, realPackageDir);
12591272
aggregatorPackageDeps.push({
12601273
swiftName: target.name,
1261-
packagePath: selfManagedDirs.get(target.name) ?? absSource,
1274+
packagePath: `libs/${target.name}`,
12621275
});
12631276
continue;
12641277
}

0 commit comments

Comments
 (0)