Skip to content

[0.87] Encode line breaks in SwiftPM scheme pre-action scriptText - #59501

Open
chrfalch wants to merge 2 commits into
0.87-stablefrom
chrfalch/0.87-port-58826
Open

chrfalch wants to merge 2 commits into
0.87-stablefrom
chrfalch/0.87-port-58826

Conversation

@chrfalch

@chrfalch chrfalch commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Port of b8df948 (#58826, "Encode line breaks in SwiftPM scheme pre-action scriptText") to 0.87-stable. That commit already landed in main. A plain cherry-pick conflicts, so this PR resolves the conflict.

The scheme pre-action script lost its line breaks, so the "Sync SPM Autolinking" pre-action did not run. This change encodes the line breaks in the scheme file.

The only conflict is in the import block of the test file generate-spm-xcodeproj-test.js. Main uses node: imports from #57611, which is not on 0.87-stable. This port keeps 0.87's imports and adds only the @xmldom/xmldom import that the new tests need. That package is already a root dev dependency on 0.87-stable. The production code is the same as on main.

This is PR 2 of 3 in a stack. Each PR contains the commits of the PRs before it. Merge them in order:

  1. [0.87] Stop re-creating library package roots on every sync #59500 (port of [iOS][SPM] Stop re-creating library package roots on every sync #58597) Stop re-creating library package roots on every sync
  2. [0.87] Encode line breaks in SwiftPM scheme pre-action scriptText #59501 (port of Encode line breaks in SwiftPM scheme pre-action scriptText #58826) Encode line breaks in SwiftPM scheme pre-action scriptText ← this PR
  3. [0.87] Re-sync SwiftPM autolinking when a watched path appears #59502 (port of Re-sync SwiftPM autolinking when a watched path appears #58828) Re-sync SwiftPM autolinking when a watched path appears

Pick request: reactwg/react-native-releases#1474

Changelog:

[IOS] [FIXED] - SwiftPM: keep line breaks in the scheme pre-action script so that the "Sync SPM Autolinking" pre-action runs. Run npx react-native spm update once to rewrite an existing scheme.

Test Plan:

  • git range-diff against b8df948: the only difference is the test file's import block.
  • yarn jest --no-cache -i packages/react-native/scripts/spm on this branch: 822 of 822 tests pass.
  • prettier --check and eslint --max-warnings 0 on the changed files: no problems.
  • CI on this PR.

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)

Conflict resolution for 0.87-stable: the new libs/ symlink tests inline their temp-dir and console-spy setup, because the useTempApps() helper from #58290 does not exist on 0.87.
Summary:
The SwiftPM setup adds a "Sync SPM Autolinking" pre-action to the app's Xcode scheme. The pre-action never ran its sync step.

The script is many lines long. `escapeXmlAttribute` wrote it into the `scriptText` XML attribute with raw line breaks. An XML parser replaces each raw line break in an attribute value with a space (XML 1.0 §3.3.3). So Xcode read the script as one line. The second line of the script is a `#` comment, so everything after `set -euo pipefail` became part of that comment.

This PR makes two changes in `packages/react-native/scripts/spm/generate-spm-xcodeproj.js`:

- `escapeXmlAttribute` now also encodes `\r`, `\n` and `\t` as `&https://github.com/react/react-native/issues/13;`, `&https://github.com/react/react-native/issues/10;` and `&https://github.com/react/react-native/issues/9;`. Xcode itself writes the script this way.
- `deinit` deletes a scheme that RN created only when the scheme is still unchanged. This check now ignores the value of the script. Without this change, a scheme that an older version wrote with raw line breaks would no longer match, and `deinit` would leave it on disk. The XML decoder had no callers left after this change, so this PR removes it.

Existing projects get the fixed script when the user runs `npx react-native spm update` once. The build-time `sync` does not rewrite the scheme.

## Changelog:

[IOS] [FIXED] - SwiftPM: keep line breaks in the scheme pre-action script so that the "Sync SPM Autolinking" pre-action runs. Run `npx react-native spm update` once to rewrite an existing scheme.

Pull Request resolved: #58826

Test Plan:
New tests:

- `generate-spm-xcodeproj-test.js`: parses the generated scheme with `xmldom/xmldom` and checks that the script keeps its line breaks and tabs. A second test checks that a refresh of an older scheme with raw line breaks writes `&https://github.com/react/react-native/issues/10;`.
- `remove-spm-injection-test.js`: checks that `deinit` still deletes an RN-created scheme whose script has raw line breaks.

```
yarn jest --no-cache -i packages/react-native/scripts/spm/__tests__/generate-spm-xcodeproj-test.js packages/react-native/scripts/spm/__tests__/remove-spm-injection-test.js
```

- Before the fix: 2 failed, 124 passed (the two `generate-spm-xcodeproj` tests).
- With only the encoding change: the new `deinit` test and 4 existing `deinit` tests failed.
- With both changes: 2 suites passed, 126 tests passed.

ESLint and Prettier pass on the changed files. `flow check-contents` reports no errors for `generate-spm-xcodeproj.js`.

I did not build an app in Xcode for this PR. An Expo app had the problem: when the line breaks in its scheme were changed to `&https://github.com/react/react-native/issues/10;` by hand, the pre-action ran.

Reviewed By: christophpurrer

Differential Revision: D123382914

Pulled By: cipolleschi

fbshipit-source-id: 0c51cb7c6fe7a66567602d813a13381f6bf3704f

(cherry picked from commit b8df948)

Conflict resolution for 0.87-stable: the test file keeps 0.87's bare 'fs'/'os'/'path'/'child_process' requires and adds only the new @xmldom/xmldom import, since the node: prefix change on main is from another PR.

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. p: Expo Partner: Expo Partner Pick Request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant