Skip to content

[iOS] SwiftPM: find a self-managed library's Package.swift in apple/ - #59361

Open
mrousavy wants to merge 2 commits into
react:mainfrom
mrousavy:feat/spm-apple-package-swift
Open

mrousavy wants to merge 2 commits into
react:mainfrom
mrousavy:feat/spm-apple-package-swift

Conversation

@mrousavy

@mrousavy mrousavy commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

SwiftPM autolinking uses a library's hand-written Package.swift when it finds one at the package root or in ios/. This PR adds apple/ as a third location.

Libraries that share sources across iOS, macOS, tvOS and visionOS often keep them in apple/ (react-native-reanimated, react-native-svg and react-native-gesture-handler do). Such a library may want its manifest there for the same reason ios/ is supported: SwiftPM writes .build/, .swiftpm/ and Package.resolved next to the manifest, and those belong outside the npm package root. Today a manifest in apple/ is ignored, and autolinking reports that the library has no Package.swift.

The lookup order is the root, then ios/, then apple/. The first manifest without the autolinker's AUTOGEN marker wins, so every library that resolves today resolves the same way. If a library has hand-written manifests in both ios/ and apple/, ios/ is used without a warning, just as a root manifest already shadows ios/.

Changes:

  • spm-utils.js: NESTED_PACKAGE_SWIFT_DIRS (['ios', 'apple']) holds the nested lookup order, shared by the autolinker and the scaffolder.
  • generate-spm-autolinking.js: findSelfManagedPackageDir checks the root, then each nested directory.
  • scaffold-package-swift.js: spm scaffold skips a library with a hand-written apple/Package.swift, as it already does for ios/Package.swift. Otherwise it would write a root manifest that shadows it.
  • __docs__/spm-scripts.md: documents the lookup order, and that swiftpmConfig.name must equal the product name in a library's own Package.swift, wherever the manifest lives.

The Xcode sync phase needs no change. It scans each library's source directory recursively, so adding or editing apple/Package.swift already triggers a re-sync.

Changelog:

[IOS] [ADDED] - SwiftPM autolinking finds a library's hand-written Package.swift in its apple/ folder

Test Plan:

yarn jest packages/react-native/scripts/spm/__tests__: 23 suites and 1135 tests pass.

New tests:

  • findSelfManagedPackageDir: a manifest only in apple/, an apple/ manifest with the AUTOGEN marker, root over apple/, ios/ over apple/, and a fallback to apple/ when the root and ios/ manifests are autolinker-generated.
  • main(): a dependency whose only manifest is apple/Package.swift gets a libs/<SwiftName> alias pointing at apple/, no synthesized wrapper, and a reference in the aggregator. Before this change, main() threw MissingManifestError for it.
  • scaffoldPackageSwiftForDep: skips a dependency with a hand-written apple/Package.swift and writes no root manifest.

Four of the new tests fail without the source change: the apple/-only lookup, the fallback, the main() case and the scaffolder case. The other three cover precedence and pass either way.

ESLint and Prettier are clean on the changed files. flow check (0.336.0) reports no errors in them.

Not tested: an Xcode build with a real library.

🤖 Generated with Claude Code

SwiftPM autolinking used a library's hand-written Package.swift only at
the package root or in ios/. Libraries that share sources across Apple
platforms often keep them in apple/, and want their manifest there for
the same reason ios/ is supported: it keeps .build/, .swiftpm/ and
Package.resolved out of the npm package root.

Look in the root, then ios/, then apple/. The first hand-written
manifest wins, so existing libraries resolve as before. The scaffolder
skips a library with a hand-written apple/Package.swift, as it already
does for ios/, instead of writing a root manifest that shadows it.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 22, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 22, 2026
@chrfalch

Copy link
Copy Markdown
Collaborator

We closed #59348. React Native now takes a library's SwiftPM name only from swiftpmConfig.name in its package.json, or from the deprecated spm block in react-native.config.js. It does not read the name from Package.swift.

The name therefore no longer depends on which Package.swift the lookup finds. This PR does not need to change anything for #59348. You can remove the paragraph about #59348 from the description.

@chrfalch

Copy link
Copy Markdown
Collaborator

One rule for the docs section you edit: for a library that ships its own Package.swift, the name in swiftpmConfig.name must equal the product name in that manifest, because the generated package references the library by .product(name: <that name>). A manifest in apple/ follows the same rule.

@mrousavy

Copy link
Copy Markdown
Contributor Author

gotcha thanks!

@mrousavy mrousavy closed this Oct 23, 2026
@mrousavy

Copy link
Copy Markdown
Contributor Author

Ah sorry I will probably edit this PR then instead of creating a new one

@mrousavy mrousavy reopened this Oct 23, 2026
The aggregator depends on a self-managed library as
.product(name: "<SwiftName>", package: "<SwiftName>"), so the library's
Package.swift has to declare a product with that name. The rule holds
for a manifest at the root, in ios/ or in apple/.

@chrfalch chrfalch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. I reviewed it, and it looks good. The SwiftPM tests pass (23 suites, 1135 tests). The 4 new tests that you name fail without the source change. The Xcode sync step does pick up a new or edited apple/Package.swift, because it scans each library folder recursively. I found no other place that looks up a library's manifest by hand.

Two behavior changes need a decision or a note:

  1. An apple/Package.swift that was not written for React Native now wins. Today, a library without a usable manifest gets the "Package.swift is missing" error. The user runs spm scaffold, which writes a root manifest. After this PR, if that library ships an apple/Package.swift for another reason, such as a pure Swift SDK, spm scaffold skips it (scaffold-package-swift.js:922-941). The autolinker then links apple/ directly. The build then fails later with a SwiftPM resolve or link error, not with the clear "missing" error. I do not know of a real library with this layout, so documenting it in __docs__/spm-scripts.md is probably enough. The other option is to count apple/ only when the library sets swiftpmConfig in its package.json.
  2. App-local spm.modules change too. A module folder with a hand-written apple/Package.swift used to get a generated wrapper. It is now linked directly, and the old wrapper is removed (generate-spm-autolinking.js:1580, :1597-1601). This is rare. Please mention it in the description, or apply the nested lookup only to npm dependencies.

Smaller suggestions:

  • The scaffold message can name the wrong file. The scaffolder treats a scaffolded ios/Package.swift as "not hand-written" and continues to apple/. The autolinker skips only AUTOGEN manifests, so it uses ios/. With a scaffolded ios/ and a hand-written apple/, the message says "Existing apple/Package.swift … Leaving it alone", but ios/ is linked. This happens only after a manual move. Stopping the loop at the first nested manifest that exists would make the two agree.
  • Tests: consider a test that moves a manifest between folders, for example ios/ to apple/, and checks that the libs/<SwiftName> link is retargeted. The code handles this through ensureSymlink. Also consider a scaffolder test for a scaffolded ios/ plus a hand-written apple/.
  • Docs: in __docs__/spm-scripts.md:620, the identity sentence names only <dep>/ios/Package.swift. It could say "ios/ or apple/".

@github-actions

Copy link
Copy Markdown

Visual regression

No visual differences at 87990d757f2b.

android: no capture

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants