Skip to content

Commit b8df948

Browse files
chrfalchmeta-codesync[bot]
authored andcommitted
Encode line breaks in SwiftPM scheme pre-action scriptText (#58826)
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
1 parent 99d327c commit b8df948

3 files changed

Lines changed: 65 additions & 24 deletions

File tree

‎packages/react-native/scripts/spm/__tests__/generate-spm-xcodeproj-test.js‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const {
2020
generateXcscheme,
2121
readScriptPhasesManifest,
2222
} = require('../generate-spm-xcodeproj');
23+
const {DOMParser} = require('@xmldom/xmldom');
2324
const {execFileSync} = require('node:child_process');
2425
const fs = require('node:fs');
2526
const os = require('node:os');
@@ -137,6 +138,40 @@ describe('scheme pre-action', () => {
137138
expect(result).toContain('<');
138139
});
139140

141+
function parsedSyncScript(xml) {
142+
return new DOMParser()
143+
.parseFromString(xml, 'text/xml')
144+
.getElementsByTagName('ActionContent')[0]
145+
.getAttribute('scriptText');
146+
}
147+
148+
const MULTILINE_SCRIPT = 'set -e\n\tif [ "$A" ]; then\r\n a && b < c\nfi';
149+
150+
it('keeps line breaks and tabs when an XML parser reads the script', () => {
151+
const result = generateXcscheme(
152+
'MyApp',
153+
'TARGET_UUID',
154+
'MyApp',
155+
MULTILINE_SCRIPT,
156+
);
157+
expect(parsedSyncScript(result)).toBe(MULTILINE_SCRIPT);
158+
});
159+
160+
it('encodes line breaks when it refreshes a script written with raw ones', () => {
161+
const legacy = generateXcscheme(
162+
'MyApp',
163+
'TARGET_UUID',
164+
'MyApp',
165+
'OLD',
166+
).replace('scriptText = "OLD"', 'scriptText = "old\n# line 2"');
167+
const updated = addPreActionToScheme(
168+
legacy,
169+
'TARGET_UUID',
170+
MULTILINE_SCRIPT,
171+
);
172+
expect(parsedSyncScript(updated)).toBe(MULTILINE_SCRIPT);
173+
});
174+
140175
it('refreshes stale script text and is idempotent', () => {
141176
const first = generateXcscheme(
142177
'MyApp',

‎packages/react-native/scripts/spm/__tests__/remove-spm-injection-test.js‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,21 @@ describe('deinit — scheme ownership', () => {
499499
expect(deinit().status).toBe('removed');
500500
expect(fs.existsSync(schemePath)).toBe(false);
501501
});
502+
503+
it('deletes its own scheme written with raw line breaks in the script', () => {
504+
const {sync, deinit, schemePath, readScheme} = setUp();
505+
sync();
506+
fs.writeFileSync(
507+
schemePath,
508+
readScheme().replace(/&#(9|10|13);/g, (_, c) =>
509+
String.fromCharCode(Number(c)),
510+
),
511+
'utf8',
512+
);
513+
514+
expect(deinit().status).toBe('removed');
515+
expect(fs.existsSync(schemePath)).toBe(false);
516+
});
502517
});
503518

504519
// ---------------------------------------------------------------------------

‎packages/react-native/scripts/spm/generate-spm-xcodeproj.js‎

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -862,28 +862,21 @@ function buildSyncAutolinkingScript(
862862
${syncStaleCheckAndDispatch()}
863863
`;
864864
}
865-
// XML-attribute escape (the five named entities). The sync script uses `>`
866-
// and `&` for redirection and bg/and chains, plus `<` for heredocs and
867-
// comparisons — all of which break Xcode's scheme parser if left raw.
865+
// XML-attribute escape. The sync script uses `>` and `&` for redirection and
866+
// bg/and chains, plus `<` for heredocs and comparisons — all of which break
867+
// Xcode's scheme parser if left raw. Raw line breaks and tabs are legal but an
868+
// XML parser turns them into spaces (XML 1.0 §3.3.3), collapsing the script
869+
// onto one line, so they are written as character references, as Xcode does.
868870
function escapeXmlAttribute(s /*: string */) /*: string */ {
869871
return s
870872
.replace(/&/g, '&amp;')
871873
.replace(/</g, '&lt;')
872874
.replace(/>/g, '&gt;')
873875
.replace(/"/g, '&quot;')
874-
.replace(/'/g, '&apos;');
875-
}
876-
877-
// The inverse of escapeXmlAttribute. `&amp;` is expanded LAST so an entity that
878-
// was itself escaped (`&lt;` → `&amp;lt;`) round-trips back to its own text
879-
// rather than to `<`.
880-
function unescapeXmlAttribute(s /*: string */) /*: string */ {
881-
return s
882-
.replace(/&lt;/g, '<')
883-
.replace(/&gt;/g, '>')
884-
.replace(/&quot;/g, '"')
885-
.replace(/&apos;/g, "'")
886-
.replace(/&amp;/g, '&');
876+
.replace(/'/g, '&apos;')
877+
.replace(/\r/g, '&#13;')
878+
.replace(/\n/g, '&#10;')
879+
.replace(/\t/g, '&#9;');
887880
}
888881

889882
function generateXcscheme(
@@ -2525,8 +2518,9 @@ function injectSpmIntoExistingXcodeproj(
25252518
return {status: 'injected', target: plan.target.name};
25262519
}
25272520

2528-
/** The sync pre-action's script, unescaped, or null when the scheme has none. */
2529-
function schemePreActionScript(xml /*: string */) /*: ?string */ {
2521+
// The scheme with the sync pre-action's scriptText emptied, so the compare
2522+
// ignores how the script was encoded; null when there is no pre-action.
2523+
function withoutPreActionScript(xml /*: string */) /*: ?string */ {
25302524
const titleIdx = xml.indexOf('title = "Sync SPM Autolinking"');
25312525
if (titleIdx < 0) {
25322526
return null;
@@ -2540,9 +2534,7 @@ function schemePreActionScript(xml /*: string */) /*: ?string */ {
25402534
// escapeXmlAttribute maps a literal `"` to `&quot;`, so the next `"` is always
25412535
// the closing delimiter.
25422536
const valueEnd = xml.indexOf('"', valueStart);
2543-
return valueEnd < 0
2544-
? null
2545-
: unescapeXmlAttribute(xml.slice(valueStart, valueEnd));
2537+
return valueEnd < 0 ? null : xml.slice(0, valueStart) + xml.slice(valueEnd);
25462538
}
25472539

25482540
/**
@@ -2564,10 +2556,9 @@ function isGeneratedScheme(
25642556
targetUuid /*: string */,
25652557
projName /*: string */,
25662558
) /*: boolean */ {
2567-
const script = schemePreActionScript(xml);
25682559
return (
2569-
script != null &&
2570-
xml === generateXcscheme(appName, targetUuid, projName, script)
2560+
withoutPreActionScript(xml) ===
2561+
generateXcscheme(appName, targetUuid, projName, '')
25712562
);
25722563
}
25732564

0 commit comments

Comments
 (0)