Skip to content

[iOS][SPM] Stop re-creating library package roots on every sync - #58597

Open
chrfalch wants to merge 1 commit into
mainfrom
chrfalch/spm-idempotent-libs-sync
Open

chrfalch wants to merge 1 commit into
mainfrom
chrfalch/spm-idempotent-libs-sync

Conversation

@chrfalch

@chrfalch chrfalch commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

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

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.

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>
@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 Sep 18, 2026
@github-actions

Copy link
Copy Markdown

Caution

Missing Changelog

Please add a Changelog to your PR description. See Changelog format

@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 Sep 18, 2026
@meta-codesync

meta-codesync Bot commented Sep 18, 2026

Copy link
Copy Markdown

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

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. 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.

1 participant