Skip to content

Re-sync SwiftPM autolinking when a watched path appears - #58828

Closed
chrfalch wants to merge 4 commits into
mainfrom
chrfalch/spm-sync-watch-absent-paths
Closed

chrfalch wants to merge 4 commits into
mainfrom
chrfalch/spm-sync-watch-absent-paths

Conversation

@chrfalch

@chrfalch chrfalch commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary:

The SwiftPM build-time sync runs only when its stamp is older than one of the files it watches. A watched path that did not exist at the last sync was dropped from the watch list. So when that path appeared later, the sync did not run.

Examples:

  • A plugin watches its precompiled-module folder. On a fresh clone, that folder does not exist yet, so later precompiles never trigger a sync.
  • A dependency has no root Package.swift or .react-native/ folder. When one appears later, for example after npx react-native spm scaffold, the dependency is detected differently, but the sync does not run.

This PR writes missing watch paths to a new file, .spm-sync-watch-absent. The stamp is stale when one of those paths now exists. The missing paths do not go to .spm-sync-watch-paths, because projects from older versions treat a missing path in that file as stale and would sync on every build. Those projects get this fix after running npx react-native spm update once.

The paths that can go to .spm-sync-watch-absent:

  • a plugin watchPaths entry that does not exist yet
  • the root Package.swift and .react-native/ of each dependency, when they do not exist

The docs now tell plugins to list their precompiled-module folder in watchPaths, but only a folder that the Xcode build does not write to.

This PR does not watch project.pbxproj. Xcode rewrites that file on many edits in the IDE, so watching it would run a full sync after each edit. #58827 makes .spm-injected.json a watched file instead. Only spm add and spm update change that file.

This PR is independent of #58827. They can land in either order. Both change the generated sync check, so the second one to land needs a rebase.

Changelog:

[IOS] [FIXED] - SwiftPM: the build-time sync now runs again when a watched path that did not exist at the last sync appears.

Test Plan:

yarn jest --no-cache -i packages/react-native/scripts/spm/__tests__/

Result: 21 suites and 1010 tests pass.

  • generate-spm-autolinking-test.js:
    • A missing watch path goes to .spm-sync-watch-absent only. This test failed before the change.
    • A dependency without a root Package.swift or .react-native/ lists both paths as absent. The first version of this PR already did this, so the test passed at once. Removing the two paths from the code makes it fail.
    • project.pbxproj is not watched. This test failed before the change.
  • generate-spm-xcodeproj-test.js: runs the shipped bash check under /bin/bash. The stamp is stale only when a path listed as absent now exists.

The build-time SwiftPM sync check only watched paths that existed at the last sync. A watched path created later, such as a plugin's precompiled output folder on a fresh clone, never triggered a re-sync. The sync now lists those absent paths in .spm-sync-watch-absent, and the build phase re-syncs when one appears. Existing projects get the new check after running `npx react-native spm update` once.
@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 4, 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 4, 2026
@chrfalch
chrfalch requested a review from cipolleschi October 4, 2026 14:57
const watchPaths = Array.from(new Set(watchCandidates))
.filter(p => p.length > 0 && fs.existsSync(p))
.sort();
for (const name of fs.readdirSync(appRoot)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happens to iteration speed in Xcode? Xcode rewrites project.pbxproj whenever a file is added or a setting changes in the IDE, so each of those edits would now trigger a full sync (codegen + autolinking) on the next build. Have we considered watching something narrower, like a hash of only the sections spm owns (e.g. the embed phase), so unrelated project edits don't re-sync?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You're right. Xcode rewrites project.pbxproj on many IDE edits. I removed the project-file watch from this PR in abf4196. #58827 now records the linked frameworks in .spm-injected.json and makes that file a sync input instead. The marker is committed next to the project, and only spm add and spm update change it. So a teammate's pull of spm changes still starts a sync, and IDE edits do not. I updated the title and description.

.filter(p => p.length > 0 && fs.existsSync(p))
.sort();
for (const name of fs.readdirSync(appRoot)) {
if (name.endsWith('.xcodeproj')) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: should we also check that the entry is a directory, and limit this to the injected project (e.g. via findInjectedXcodeproj)? Right now any top-level *.xcodeproj gets watched, including projects spm doesn't manage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This code is gone in abf4196, together with the project-file watch, so no top-level *.xcodeproj is watched now.

const absentWatchPaths /*: Array<string> */ = [];
for (const p of Array.from(new Set(watchCandidates)).sort()) {
if (p.length > 0) {
(fs.existsSync(p) ? watchPaths : absentWatchPaths).push(p);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it intended that every dependency's missing root Package.swift and .react-native/ ends up in .spm-sync-watch-absent, not just plugin paths? The check is cheap, but it means a dependency gaining either one now triggers a re-sync. That might be desirable (e.g. after spm scaffold), but it might be worth calling out in the summary or the comment above.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this is intended. A root Package.swift or .react-native/ that appears later, for example after spm scaffold, changes how the dependency is detected, so it needs a re-sync. I added a why-comment, a test, and a line in spm-scripts.md in abf4196, and the PR description now mentions it.

@chrfalch chrfalch changed the title Re-sync SwiftPM autolinking when a watched path appears or the project file changes Re-sync SwiftPM autolinking when a watched path appears Oct 6, 2026
@meta-codesync

meta-codesync Bot commented Oct 14, 2026

Copy link
Copy Markdown

@cipolleschi has imported this pull request. If you are a Meta employee, you can view this in D125206241.

@chrfalch
chrfalch requested a review from cipolleschi October 14, 2026 12:37
@meta-codesync meta-codesync Bot closed this in c89f412 Oct 14, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Oct 14, 2026
@meta-codesync

meta-codesync Bot commented Oct 14, 2026

Copy link
Copy Markdown

@cipolleschi merged this pull request in c89f412.

meta-codesync Bot pushed a commit that referenced this pull request Oct 15, 2026
…ee (#58827)

Summary:
An autolinking plugin, such as Expo, can provide precompiled frameworks. The build-time `sync` reads the list of these frameworks from the plugin. `npx react-native spm add` and `spm update` write a second list: the frameworks that the Xcode project links. When the two lists differ, the build fails at the link step with "no such file … plugins/<pkg>.xcframework" or "no such module". That error does not say what is wrong or what to run.

This happens, for example, when a module is precompiled after the last `spm update`, or when a teammate commits a project that links a module this machine does not provide.

This PR makes `sync` compare the two lists before the build continues. For each framework that differs, it prints an Xcode `error:` line that names the framework and what to do, and it exits with code 2. Existing projects already stop the build on exit code 2.

How it works:

- `spm add` and `spm update` record the linked frameworks in `.spm-injected.json`, next to the project. The check reads them there. For a marker from an older version, the check reads the embed phase of `project.pbxproj`, as before.
- `.spm-injected.json` is now a sync input. A pull that changes what the project links starts a sync on the next build. `project.pbxproj` is not a sync input, because Xcode rewrites it on many IDE edits.
- On a mismatch, `sync` saves the error lines in `build/generated/autolinking/.spm-plugin-mismatch` and refreshes its stamp. Later builds print the saved errors and stop without running the full sync again. When any sync input changes, the next build runs the sync and the check again. When the check passes, `sync` deletes the file.
- The team must precompile the same set of frameworks on every machine, because the committed project is the contract. So the messages say:
  - Project links a framework that no plugin provides: precompile it, then run `npx react-native spm sync`. Running `spm update` instead removes it from the committed project for the whole team.
  - A plugin provides a framework that the project does not link: run `npx react-native spm update` to add it for the whole team.
- The help text of `npx react-native spm` now lists `sync`, because the error tells users to run it.
- `sync` still never writes `project.pbxproj`. Only `spm add` and `spm update` change what the project links.
- React Native's own frameworks are not compared.
- The check uses Xcode's `PROJECT_FILE_PATH` to find the project being built.

This PR is independent of #58828. They can land in either order. Both change the generated sync check, so the second one to land needs a rebase.

## Changelog:

[IOS] [ADDED] - SwiftPM: the build stops with a clear error when precompiled plugin frameworks and the Xcode project disagree, and the error says what to run.

Pull Request resolved: #58827

Test Plan:
```
yarn jest --no-cache -i packages/react-native/scripts/spm/__tests__/
```

Result: 22 suites and 1046 tests pass. The new tests failed before the change. One exception: the test that a newer `project.pbxproj` alone does not start a sync passed at once, because it guards against a regression.

- `plugin-framework-mismatch-test.js` uses a real injected project. It runs each case twice: with the list recorded in the marker, and with an older marker that has no list. It covers a framework that is missing in either list, a renamed framework, matching lists, a name with a space, a project that is not injected or has no embed phase, a missing manifest, and two injected projects. It also checks that running `spm add` or `spm update` twice gives the same `project.pbxproj`, and the warning when an older marker and a missing manifest make the check skip one direction.
- `sync-spm-autolinking-test.js` checks that a mismatch saves the errors and refreshes the stamp, that a passing check deletes the saved errors, the recovery after a precompile, and the exit codes of the script's direct entry point.
- `generate-spm-xcodeproj-test.js` runs the generated sync check under `/bin/bash`. It checks that a later build prints the saved errors without a sync, that a changed sync input runs the sync again, that a newer `.spm-injected.json` starts a sync, and that a newer `project.pbxproj` alone does not.

Not tested:

- The exit-code-2 mapping in `setup-apple-spm.js`. `main()` has no test seam. The change is one `instanceof` check.
- A full Xcode build with a precompiled plugin. The order (the sync phase runs before the link step) comes from the code and from `spm-scripts.md`.

Reviewed By: cortinico

Differential Revision: D125208952

Pulled By: cipolleschi

fbshipit-source-id: c7727e0e77932c5260237183bbd14ae7195819e3
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. Merged This PR has been merged. p: Expo Partner: Expo Partner 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