Repository navigation
Conversation
Autolinking writes one symlink per self-managed library into build/generated/autolinking/libs/<Name>, and Xcode holds each as a loaded local package root. The generator deleted and re-created that whole tree on every run, even when the generated output was byte-identical, so an Xcode build failed with "Missing package product" for every such library. The sync ran on every IDE build because its staleness check walked each library's directory with `find -newer`, and Xcode writes its own per-user scheme state under <lib>/.swiftpm. That write marked the next build stale, which triggered the destructive re-sync, which broke that build. Keep unchanged entries in place and prune only the ones that are no longer autolinked, and skip .swiftpm when probing for changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@shwanton has imported this pull request. If you are a Meta employee, you can view this in D120740029. |
fabriziocucci
pushed a commit
that referenced
this pull request
Sep 24, 2026
Summary: Building an app in Xcode fails when it autolinks a library that ships its own `Package.swift`, with one error per such library: ``` Missing package product 'reactnativeskia_ReactNativeSkia.ReactNativeSkia' Missing package product 'reactnativesafeareacontext_ReactNativeSafeAreaContext.ReactNativeSafeAreaContext' ``` The same project builds fine from the command line. Two problems combine. **The sync destroys package roots Xcode has already loaded.** Autolinking writes one symlink per self-managed library into `build/generated/autolinking/libs/<Name>`, and Xcode treats each as a local Swift package root. The generator deleted that whole directory and recreated it on every run, even when the generated output was byte-for-byte identical. Measured on a real app, across one no-op sync: the `libs/` inode changed from 1913513750 to 1913930645, `libs/ReactNativeSkia` from 1913513937 to 1913930649, while the generated `Package.swift` kept the same MD5. Recreating a package root that Xcode has already resolved is what produces the error above. **Xcode's own bookkeeping made the sync run every time.** The "Sync SPM Autolinking" build phase re-syncs when a watched input looks newer than its stamp, and it checked each library's whole directory with `find <dir> -newer <stamp>`. Xcode writes its per-user scheme state inside that directory, at `<lib>/.swiftpm/xcode/xcuserdata/<user>.xcuserdatad/xcschemes/xcschememanagement.plist`. So Xcode's own write marked the next build stale, which triggered the destructive re-sync, which broke that build. `xcodebuild` does not write that file, which is why command-line builds were never affected. This change makes the `libs/` tree idempotent — unchanged entries keep their inode, and entries that are no longer autolinked are pruned instead of wiped — and makes the staleness check skip `.swiftpm`. ### How to verify In an app that autolinks a library shipping its own `Package.swift`, build in Xcode twice in a row. Both builds should succeed. Before this change the second build fails with `Missing package product`. ## Changelog: [IOS][FIXED] - Stop SwiftPM autolinking from recreating library package roots on every sync, which broke Xcode builds of apps using libraries that ship their own Package.swift Pull Request resolved: #58597 Test Plan: **Unit tests.** `yarn test packages/react-native/scripts/spm` — 21 suites, 1009 tests pass. New tests, written and seen failing before the fix: - Two consecutive generation runs over an unchanged self-managed dependency keep both the `libs/` inode and each entry's inode. Failed before the fix with the same inode churn measured on the real app. - A dependency removed between two runs leaves no symlink under `libs/`. - The emitted staleness snippet is extracted from the generated script and executed under `/bin/bash -c` with `set -euo pipefail` against a temporary tree. A write under `.swiftpm/` is ignored; a real source change is still detected. **Real app.** A React Native 0.87.1 app on Xcode 27 autolinking `shopify/react-native-skia` and `react-native-safe-area-context`, both self-managed. Before: `xcodebuild` succeeded, Xcode failed in about 5 seconds with the two errors above, on nearly every build. **Not run:** the full CI matrix, and no Android-side check — this touches iOS SwiftPM tooling only. ## Scope Deliberately minimal. One case still re-syncs: the first time Xcode creates `.swiftpm` inside a library, that bumps the library directory's own mtime, so the build after a fresh checkout reports stale once per library. That is harmless now that the sync is idempotent. One related item is left alone: the aggregate `Package.swift` is still rewritten on every sync even when its content is unchanged, which bumps its mtime and can make Xcode re-resolve. That is wasteful but not destructive, and it is no longer reached on an ordinary IDE build now that `.swiftpm` writes do not mark the sync stale. Reviewed By: cipolleschi Differential Revision: D120740029 Pulled By: shwanton fbshipit-source-id: 3783d4cee4791ab13517d8f0dcac71f752cb7c1b (cherry picked from commit 97cc934)
tvinhas
pushed a commit
to tvinhas/react-native-macos
that referenced
this pull request
Oct 21, 2026
Summary: Building an app in Xcode fails when it autolinks a library that ships its own `Package.swift`, with one error per such library: ``` Missing package product 'reactnativeskia_ReactNativeSkia.ReactNativeSkia' Missing package product 'reactnativesafeareacontext_ReactNativeSafeAreaContext.ReactNativeSafeAreaContext' ``` The same project builds fine from the command line. Two problems combine. **The sync destroys package roots Xcode has already loaded.** Autolinking writes one symlink per self-managed library into `build/generated/autolinking/libs/<Name>`, and Xcode treats each as a local Swift package root. The generator deleted that whole directory and recreated it on every run, even when the generated output was byte-for-byte identical. Measured on a real app, across one no-op sync: the `libs/` inode changed from 1913513750 to 1913930645, `libs/ReactNativeSkia` from 1913513937 to 1913930649, while the generated `Package.swift` kept the same MD5. Recreating a package root that Xcode has already resolved is what produces the error above. **Xcode's own bookkeeping made the sync run every time.** The "Sync SPM Autolinking" build phase re-syncs when a watched input looks newer than its stamp, and it checked each library's whole directory with `find <dir> -newer <stamp>`. Xcode writes its per-user scheme state inside that directory, at `<lib>/.swiftpm/xcode/xcuserdata/<user>.xcuserdatad/xcschemes/xcschememanagement.plist`. So Xcode's own write marked the next build stale, which triggered the destructive re-sync, which broke that build. `xcodebuild` does not write that file, which is why command-line builds were never affected. This change makes the `libs/` tree idempotent — unchanged entries keep their inode, and entries that are no longer autolinked are pruned instead of wiped — and makes the staleness check skip `.swiftpm`. ### How to verify In an app that autolinks a library shipping its own `Package.swift`, build in Xcode twice in a row. Both builds should succeed. Before this change the second build fails with `Missing package product`. ## Changelog: [IOS][FIXED] - Stop SwiftPM autolinking from recreating library package roots on every sync, which broke Xcode builds of apps using libraries that ship their own Package.swift Pull Request resolved: react#58597 Test Plan: **Unit tests.** `yarn test packages/react-native/scripts/spm` — 21 suites, 1009 tests pass. New tests, written and seen failing before the fix: - Two consecutive generation runs over an unchanged self-managed dependency keep both the `libs/` inode and each entry's inode. Failed before the fix with the same inode churn measured on the real app. - A dependency removed between two runs leaves no symlink under `libs/`. - The emitted staleness snippet is extracted from the generated script and executed under `/bin/bash -c` with `set -euo pipefail` against a temporary tree. A write under `.swiftpm/` is ignored; a real source change is still detected. **Real app.** A React Native 0.87.1 app on Xcode 27 autolinking `shopify/react-native-skia` and `react-native-safe-area-context`, both self-managed. Before: `xcodebuild` succeeded, Xcode failed in about 5 seconds with the two errors above, on nearly every build. **Not run:** the full CI matrix, and no Android-side check — this touches iOS SwiftPM tooling only. ## Scope Deliberately minimal. One case still re-syncs: the first time Xcode creates `.swiftpm` inside a library, that bumps the library directory's own mtime, so the build after a fresh checkout reports stale once per library. That is harmless now that the sync is idempotent. One related item is left alone: the aggregate `Package.swift` is still rewritten on every sync even when its content is unchanged, which bumps its mtime and can make Xcode re-resolve. That is wasteful but not destructive, and it is no longer reached on an ordinary IDE build now that `.swiftpm` writes do not mark the sync stale. Reviewed By: cipolleschi Differential Revision: D120740029 Pulled By: shwanton fbshipit-source-id: 3783d4cee4791ab13517d8f0dcac71f752cb7c1b (cherry picked from commit 97cc934)
meta-codesync Bot
pushed a commit
that referenced
this pull request
Oct 23, 2026
Summary: Recent fixes to the iOS SwiftPM tooling (`scripts/spm`) all fixed bugs that CI did not catch. Adopters found them first. Each fix added a test for its own bug only. The bugs fall into a few classes: output that changes when nothing changed, generated names that collide or do not resolve, values that are hard-coded instead of read from the app, machine-specific paths in files that users commit, and malformed files that Xcode tolerates. This PR adds two test-only tools that look for each class in general. They run in the existing Linux Jest job (`test_js`). No production code changes. 1. **Invariant checker** (`spm-invariants.js`, not a test file itself). It reads the files that the tooling generates and returns a list of violations: - no machine paths (temp dir, `$HOME`, absolute build-setting values) in `project.pbxproj`, `.xcscheme`, or `.spm-injected.json`; - the scheme parses as XML, and the pre-action script keeps its line breaks; - in every generated `Package.swift`, target and product names are unique (also case-insensitive) and valid C99 identifiers, and every dependency resolves, including products of local `.package(path:)` dependencies; - every generated or scaffolded manifest uses the app's iOS deployment target; the codegen manifest, whose template fixes `.iOS(.v15)`, must not be above it; - `project.pbxproj` is balanced, every referenced object ID is defined, and the frameworks in `.spm-injected.json` match the project. Each rule also fails when the file or field it checks is missing, so a check cannot pass by finding nothing. 2. **Lifecycle test** (`spm-lifecycle-test.js` + `spm-fixture-app.js`). It builds a fake app on disk (deployment target 16.4), fake prebuilt artifacts for two React Native versions, and four fake libraries: one with its own `Package.swift`, one podspec-only library with `module_name` different from its npm name, one scoped `acme/…` package, and one autolinking plugin with a flavored framework. It then runs `scaffold → add → sync → sync → update (same version) → update (next version) → deinit` through `setup-apple-spm.js`, and runs the checker after each step. It also asserts: - a second sync changes no byte and replaces no `libs/` entry; - the build-time stale check reacts to the right changes and ignores files that Xcode writes; - a plugin framework mismatch makes sync exit with code 2; - `update` to the same version and `deinit` restore the project and scheme byte for byte. Network access, `plutil`, `otool`, codegen and `pod` are mocked, so the test runs offline on Linux. ## Changelog: [INTERNAL] [ADDED] - Add regression tests for the iOS SwiftPM tooling Pull Request resolved: #59217 Test Plan: ``` yarn jest --no-cache -i packages/react-native/scripts/spm/__tests__ ``` Result: 24 suites, 1124 tests, all passed, about 23 s (on main at dd9b6c5). The two new suites take about 4 s. Each earlier fix was undone one at a time with `git show <sha> -- ':!**/__tests__/**' | git apply -R`, and the new tests failed each time: | Fix reverted | New tests failing | |---|---| | #58826 (scheme pre-action newlines) | 9 | | #58292 (absolute `HERMES_CLI_PATH` in pbxproj) | 9 | | #58597 (`libs/` rewritten on every sync) | 2 | | #58828 (watched path absent at add time) | 1 | `yarn eslint` and `prettier --check` on the new files: clean. Not run: Linux locally. The `test_js` job on this PR is the first Linux run. Reviewed By: cortinico Differential Revision: D126304581 Pulled By: cipolleschi fbshipit-source-id: c1bacf9ef2c6955a262cac0053491d69535375e9
cipolleschi
pushed a commit
that referenced
this pull request
Oct 26, 2026
Summary: Recent fixes to the iOS SwiftPM tooling (`scripts/spm`) all fixed bugs that CI did not catch. Adopters found them first. Each fix added a test for its own bug only. The bugs fall into a few classes: output that changes when nothing changed, generated names that collide or do not resolve, values that are hard-coded instead of read from the app, machine-specific paths in files that users commit, and malformed files that Xcode tolerates. This PR adds two test-only tools that look for each class in general. They run in the existing Linux Jest job (`test_js`). No production code changes. 1. **Invariant checker** (`spm-invariants.js`, not a test file itself). It reads the files that the tooling generates and returns a list of violations: - no machine paths (temp dir, `$HOME`, absolute build-setting values) in `project.pbxproj`, `.xcscheme`, or `.spm-injected.json`; - the scheme parses as XML, and the pre-action script keeps its line breaks; - in every generated `Package.swift`, target and product names are unique (also case-insensitive) and valid C99 identifiers, and every dependency resolves, including products of local `.package(path:)` dependencies; - every generated or scaffolded manifest uses the app's iOS deployment target; the codegen manifest, whose template fixes `.iOS(.v15)`, must not be above it; - `project.pbxproj` is balanced, every referenced object ID is defined, and the frameworks in `.spm-injected.json` match the project. Each rule also fails when the file or field it checks is missing, so a check cannot pass by finding nothing. 2. **Lifecycle test** (`spm-lifecycle-test.js` + `spm-fixture-app.js`). It builds a fake app on disk (deployment target 16.4), fake prebuilt artifacts for two React Native versions, and four fake libraries: one with its own `Package.swift`, one podspec-only library with `module_name` different from its npm name, one scoped `acme/…` package, and one autolinking plugin with a flavored framework. It then runs `scaffold → add → sync → sync → update (same version) → update (next version) → deinit` through `setup-apple-spm.js`, and runs the checker after each step. It also asserts: - a second sync changes no byte and replaces no `libs/` entry; - the build-time stale check reacts to the right changes and ignores files that Xcode writes; - a plugin framework mismatch makes sync exit with code 2; - `update` to the same version and `deinit` restore the project and scheme byte for byte. Network access, `plutil`, `otool`, codegen and `pod` are mocked, so the test runs offline on Linux. ## Changelog: [INTERNAL] [ADDED] - Add regression tests for the iOS SwiftPM tooling Pull Request resolved: #59217 Test Plan: ``` yarn jest --no-cache -i packages/react-native/scripts/spm/__tests__ ``` Result: 24 suites, 1124 tests, all passed, about 23 s (on main at dd9b6c5). The two new suites take about 4 s. Each earlier fix was undone one at a time with `git show <sha> -- ':!**/__tests__/**' | git apply -R`, and the new tests failed each time: | Fix reverted | New tests failing | |---|---| | #58826 (scheme pre-action newlines) | 9 | | #58292 (absolute `HERMES_CLI_PATH` in pbxproj) | 9 | | #58597 (`libs/` rewritten on every sync) | 2 | | #58828 (watched path absent at add time) | 1 | `yarn eslint` and `prettier --check` on the new files: clean. Not run: Linux locally. The `test_js` job on this PR is the first Linux run. Reviewed By: cortinico Differential Revision: D126304581 Pulled By: cipolleschi fbshipit-source-id: c1bacf9ef2c6955a262cac0053491d69535375e9 (cherry picked from commit fba55e2)
tvinhas
pushed a commit
to tvinhas/react-native-macos
that referenced
this pull request
Oct 28, 2026
Summary: Recent fixes to the iOS SwiftPM tooling (`scripts/spm`) all fixed bugs that CI did not catch. Adopters found them first. Each fix added a test for its own bug only. The bugs fall into a few classes: output that changes when nothing changed, generated names that collide or do not resolve, values that are hard-coded instead of read from the app, machine-specific paths in files that users commit, and malformed files that Xcode tolerates. This PR adds two test-only tools that look for each class in general. They run in the existing Linux Jest job (`test_js`). No production code changes. 1. **Invariant checker** (`spm-invariants.js`, not a test file itself). It reads the files that the tooling generates and returns a list of violations: - no machine paths (temp dir, `$HOME`, absolute build-setting values) in `project.pbxproj`, `.xcscheme`, or `.spm-injected.json`; - the scheme parses as XML, and the pre-action script keeps its line breaks; - in every generated `Package.swift`, target and product names are unique (also case-insensitive) and valid C99 identifiers, and every dependency resolves, including products of local `.package(path:)` dependencies; - every generated or scaffolded manifest uses the app's iOS deployment target; the codegen manifest, whose template fixes `.iOS(.v15)`, must not be above it; - `project.pbxproj` is balanced, every referenced object ID is defined, and the frameworks in `.spm-injected.json` match the project. Each rule also fails when the file or field it checks is missing, so a check cannot pass by finding nothing. 2. **Lifecycle test** (`spm-lifecycle-test.js` + `spm-fixture-app.js`). It builds a fake app on disk (deployment target 16.4), fake prebuilt artifacts for two React Native versions, and four fake libraries: one with its own `Package.swift`, one podspec-only library with `module_name` different from its npm name, one scoped `acme/…` package, and one autolinking plugin with a flavored framework. It then runs `scaffold → add → sync → sync → update (same version) → update (next version) → deinit` through `setup-apple-spm.js`, and runs the checker after each step. It also asserts: - a second sync changes no byte and replaces no `libs/` entry; - the build-time stale check reacts to the right changes and ignores files that Xcode writes; - a plugin framework mismatch makes sync exit with code 2; - `update` to the same version and `deinit` restore the project and scheme byte for byte. Network access, `plutil`, `otool`, codegen and `pod` are mocked, so the test runs offline on Linux. ## Changelog: [INTERNAL] [ADDED] - Add regression tests for the iOS SwiftPM tooling Pull Request resolved: react#59217 Test Plan: ``` yarn jest --no-cache -i packages/react-native/scripts/spm/__tests__ ``` Result: 24 suites, 1124 tests, all passed, about 23 s (on main at dd9b6c5). The two new suites take about 4 s. Each earlier fix was undone one at a time with `git show <sha> -- ':!**/__tests__/**' | git apply -R`, and the new tests failed each time: | Fix reverted | New tests failing | |---|---| | react#58826 (scheme pre-action newlines) | 9 | | react#58292 (absolute `HERMES_CLI_PATH` in pbxproj) | 9 | | react#58597 (`libs/` rewritten on every sync) | 2 | | react#58828 (watched path absent at add time) | 1 | `yarn eslint` and `prettier --check` on the new files: clean. Not run: Linux locally. The `test_js` job on this PR is the first Linux run. Reviewed By: cortinico Differential Revision: D126304581 Pulled By: cipolleschi fbshipit-source-id: c1bacf9ef2c6955a262cac0053491d69535375e9 (cherry picked from commit fba55e2)
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.
Summary
Building an app in Xcode fails when it autolinks a library that ships its own
Package.swift, with one error per such library:The same project builds fine from the command line. Two problems combine.
The sync destroys package roots Xcode has already loaded. Autolinking writes one symlink per self-managed library into
build/generated/autolinking/libs/<Name>, and Xcode treats each as a local Swift package root. The generator deleted that whole directory and recreated it on every run, even when the generated output was byte-for-byte identical. Measured on a real app, across one no-op sync: thelibs/inode changed from 1913513750 to 1913930645,libs/ReactNativeSkiafrom 1913513937 to 1913930649, while the generatedPackage.swiftkept the same MD5. Recreating a package root that Xcode has already resolved is what produces the error above.Xcode's own bookkeeping made the sync run every time. The "Sync SPM Autolinking" build phase re-syncs when a watched input looks newer than its stamp, and it checked each library's whole directory with
find <dir> -newer <stamp>. Xcode writes its per-user scheme state inside that directory, at<lib>/.swiftpm/xcode/xcuserdata/<user>.xcuserdatad/xcschemes/xcschememanagement.plist. So Xcode's own write marked the next build stale, which triggered the destructive re-sync, which broke that build.xcodebuilddoes not write that file, which is why command-line builds were never affected.This change makes the
libs/tree idempotent — unchanged entries keep their inode, and entries that are no longer autolinked are pruned instead of wiped — and makes the staleness check skip.swiftpm.How to verify
In an app that autolinks a library shipping its own
Package.swift, build in Xcode twice in a row. Both builds should succeed. Before this change the second build fails withMissing package product.Changelog:
[IOS][FIXED] - Stop SwiftPM autolinking from recreating library package roots on every sync, which broke Xcode builds of apps using libraries that ship their own Package.swift
Test Plan
Unit tests.
yarn test packages/react-native/scripts/spm— 21 suites, 1009 tests pass.New tests, written and seen failing before the fix:
libs/inode and each entry's inode. Failed before the fix with the same inode churn measured on the real app.libs/./bin/bash -cwithset -euo pipefailagainst a temporary tree. A write under.swiftpm/is ignored; a real source change is still detected.Real app. A React Native 0.87.1 app on Xcode 27 autolinking
@shopify/react-native-skiaandreact-native-safe-area-context, both self-managed. Before:xcodebuildsucceeded, Xcode failed in about 5 seconds with the two errors above, on nearly every build.Not run: the full CI matrix, and no Android-side check — this touches iOS SwiftPM tooling only.
Scope
Deliberately minimal.
One case still re-syncs: the first time Xcode creates
.swiftpminside a library, that bumps the library directory's own mtime, so the build after a fresh checkout reports stale once per library. That is harmless now that the sync is idempotent.One related item is left alone: the aggregate
Package.swiftis still rewritten on every sync even when its content is unchanged, which bumps its mtime and can make Xcode re-resolve. That is wasteful but not destructive, and it is no longer reached on an ordinary IDE build now that.swiftpmwrites do not mark the sync stale.