Skip to content

Encode line breaks in SwiftPM scheme pre-action scriptText - #58826

Closed
chrfalch wants to merge 2 commits into
mainfrom
chrfalch/spm-scheme-preaction-newlines
Closed

chrfalch wants to merge 2 commits into
mainfrom
chrfalch/spm-scheme-preaction-newlines

Conversation

@chrfalch

@chrfalch chrfalch commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

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

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 
.
  • 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 
 by hand, the pre-action ran.

The generated scheme wrote the multi-line sync script raw into the scriptText
XML attribute. An XML parser turns raw line breaks in attribute values into
spaces, so Xcode ran the script as one line that starts a comment and does
nothing. Line breaks and tabs are now written as character references, as
Xcode does, and deinit recognizes RN's scheme however its script was encoded.
Run `npx react-native spm update` once to rewrite an existing scheme.
@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 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. labels Oct 4, 2026
@chrfalch
chrfalch requested a review from cipolleschi October 4, 2026 14:53
@meta-codesync

meta-codesync Bot commented Oct 5, 2026

Copy link
Copy Markdown

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

@meta-codesync meta-codesync Bot closed this in b8df948 Oct 6, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Oct 6, 2026
@meta-codesync

meta-codesync Bot commented Oct 6, 2026

Copy link
Copy Markdown

@cipolleschi merged this pull request in b8df948.

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:
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)
(cherry picked from commit 1d69b90)
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)
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