fix(swiftpm): fix two ways array build settings were mishandled - #57744
Closed
chrfalch wants to merge 5 commits into
Closed
fix(swiftpm): fix two ways array build settings were mishandled#57744chrfalch wants to merge 5 commits into
chrfalch wants to merge 5 commits into
Conversation
This was referenced Jul 29, 2026
cipolleschi
approved these changes
Jul 31, 2026
|
@cipolleschi has imported this pull request. If you are a Meta employee, you can view this in D114317839. |
Contributor
|
A comment left by @fabriziocucci: Two tiny things, both non-blocking and maybe I'm missing context:
|
chrfalch
force-pushed
the
chrfalch/spm-promoted-array-scalar-restore
branch
from
August 1, 2026 07:21
9379eb3 to
c105182
Compare
meta-codesync Bot
pushed a commit
that referenced
this pull request
Aug 10, 2026
Summary:
**`spm add --version <ver>` pinned a value that nothing ever read back.**
The marker write has been there all along, and so has the reader — `readArtifactsVersionOverride()` in `spm/generate-spm-xcodeproj.js`, exported and unit-tested. It just had **zero production callers**. Every reference to it was a test.
`determineVersion` — the only resolver — went straight from the flag to `package.json`:
```js
let version = args.version; // --version
if (version == null) {
version = pkgJson.version; // react-native/package.json; marker never consulted
}
```
That value picks which artifact slots the project gets wired to. So:
1. `spm add --version 0.88.0-nightly-…` → wires the nightly's slots, pins the label ✅
2. `spm update` (no flag) → silently resolves `package.json`'s version instead, re-pointing the project at different slots, while the marker still claims the nightly ❌
`--version` was effectively single-use. In this monorepo `package.json` is `1000.0.0`, which has no published artifacts, so a flagless run after a pinned `add` fails outright — which is why the standing advice has been to pass `--version` on *every* invocation. That advice was working around this bug.
**Fix:** insert the pin between the two existing sources.
```
--version → pinned override → react-native/package.json
```
15 lines of logic. `spm download` and the scaffold path pick it up for free, since both consume the same resolved value. Because this is persistent state, one line is logged when the pin is the source, so a stale pin is diagnosable instead of silent; there is still no way to clear it short of `deinit`.
**Three comments were actively wrong** and are corrected here: the reader's doc block, the marker-field comment, and `findInjectedXcodeproj`'s comment all asserted that the build-time sync calls this via `readArtifactsVersionOverride`. It does not and never did — the `sync` action returns before artifacts are resolved at all. Those comments are what made the gap invisible; they misled me while investigating.
## Changelog:
[Internal] [Fixed] - SwiftPM: `spm --version` now sticks, so a later run without the flag keeps using the pinned artifact version
Pull Request resolved: #57762
Test Plan:
`yarn jest packages/react-native/scripts` → **31 suites, 684 tests**, all green.
New tests, written red first — the load-bearing one failed with `Expected: "0.80.0" / Received: "1000.0.0"`, i.e. exactly the reported bug:
- `--version` given → wins, even with a different value pinned
- no flag, pin present → the pinned version is used
- no flag, no pin → `package.json`'s version (unchanged behaviour)
- no flag, corrupt or absent marker → `package.json`'s version, no throw
- the log line fires only when the pin is the source
The three fallback cases passed *before* the fix too, which is the point — they pin today's behaviour so this change can't regress it.
## Note on landing order
Touches `setup-apple-spm.js` and `spm/generate-spm-xcodeproj.js`, which #57744, #57756 and #57757 also touch. All are cut independently from `main`; whichever lands first, I'll rebase the rest. #57756 is the closest relative — it does the same wiring for `--config-command`, which had the identical write-only-pin shape.
Reviewed By: fabriziocucci
Differential Revision: D114318107
Pulled By: cipolleschi
fbshipit-source-id: 157da5b2eaaef9adee924eaa1832b7a38b008f4c
meta-codesync Bot
pushed a commit
that referenced
this pull request
Aug 10, 2026
…es (#57757) Summary: **SwiftPM has no equivalent of CocoaPods' `script_phase`, so a framework that generates content at build time can't get one.** The first casualty is expo-constants: nothing writes `EXConstants.bundle/app.config`, which shows up at runtime as *"Unable to find the embedded app config"*. This adds a 6th field to the SwiftPM autolinking plugin contract: ```js scriptPhases: [{ id: 'expo-constants.app-config', // stable: ledger key + deterministic UUID seed name: 'Generate Expo app.config', // Xcode's display name script: '…', position: 'end', // 'end' (default) | 'beforeCompile' inputPaths: ['$(SRCROOT)/../app.json'], outputPaths: ['$(TARGET_BUILD_DIR)/…/EXConstants.bundle/app.config'], alwaysOutOfDate: true, }] ``` The plugin returns data; RN validates it, records it to a `.spm-plugin-script-phases.json` sidecar (written even when empty, so removing a plugin clears stale entries), and `spm add`/`update` emits one `PBXShellScriptBuildPhase` per entry — tracked in `.spm-injected.json` by `id`, so `update` reconciles and `deinit` reverts. ### Verified end to end by the Expo team On a real Expo app, against a local cut of this branch. A `position: 'end'` phase lands last, after the JS bundle phase: ``` 5. Resources 6. Bundle React Native code and images 7. [Expo Dev Launcher] Strip Local Network Keys for Release 8. Generate Expo app.config ← last ``` `BUILD SUCCEEDED`, and `EXConstants.bundle/app.config` is written with `sdkVersion: 56.0.0` — precisely the value whose absence caused the original bug. Red baseline confirmed first: before the declaration, the same app built with `0 script phase(s)`, an empty sidecar, and no `EXConstants.bundle` at all. They also independently confirmed `deinit` leaves zero residue, `add` is idempotent (same sha1 twice), and no phase duplicates. Their side is expo/expo#47647. ### Design decisions worth a reviewer's attention - **`end` appends at the true end of `buildPhases`**, which is *after* the JS bundle phase — where expo-constants must write, since it targets `$TARGET_BUILD_DIR`. Anchoring relative to the Frameworks phase (the obvious-looking choice) lands it *before* the bundle phase, because real template order is `Sources, Frameworks, Resources, Bundle React Native code and images`. - **`beforeCompile` anchors after RN's own "Sync SPM Autolinking" phase**, which must stay first since it regenerates autolinking — a plugin phase ahead of it would run against stale generated content. The anchor chains forward so declared order survives. Position and relative order are re-derived every sync, so a phase dragged by hand in Xcode returns to its declared slot. - **Validation is fatal**, matching `flavoredFrameworks` rather than the lenient `watchPaths`. A silently dropped phase means the content is never written and the app fails at runtime with no build-time signal — which is the bug being fixed. - **`id` is the ledger key and the UUID seed.** The charset allows a scoped npm name (`expo/log-box`) but excludes `:`, which separates the `plugin:<id>` seed. `__proto__`/`constructor`/`prototype` are rejected because `plainObject['__proto__'] = v` vanishes through `JSON.stringify`, which would record a phase that `deinit` could never remove. - **A plugin-supplied `name` reaches pbxproj comments**, and those are scanned by single-line regexes. What lands in a comment is therefore normalized: a name containing `*/`, `{` or `,` otherwise produced a brace-unbalanced project Xcode couldn't open, or an orphan phase `deinit` reported removing but didn't. The full name still goes verbatim into the `name` field Xcode displays. The same normalization now covers generated-source filenames, which had the identical hole. ### Also fixed in passing `add → update → deinit` did **not** restore `project.pbxproj` byte-for-byte, even with zero script phases: the second run's marker forgot what the first had created, leaving an empty `packageReferences` / `packageProductDependencies` and the generated `.xcscheme` behind. The created-record now carries forward and `scheme.created` is sticky. Two guards came with that, both tested: a created array field is removed only when it is **empty** after RN's own members come out (so a package a user added to it survives `deinit`), and the scheme is deleted only if it is still RN's own (so a scheme the user has taken over is left alone). ## Changelog: [Internal] [Added] - SwiftPM: autolinking plugins can declare build-time script phases via `scriptPhases` Pull Request resolved: #57757 Test Plan: `yarn jest packages/react-native/scripts` → **853 tests**, all green. The SwiftPM suite specifically went from **462 → 637** tests. Written red first throughout. Coverage includes: - one declared phase → exactly one `PBXShellScriptBuildPhase`, correct `name`/`shellScript`/serialized paths; `alwaysOutOfDate` emitted as unquoted `1` only when set - `end` lands last; `beforeCompile` lands after the sync phase and before Sources; declared order preserved for multiple phases of each position and for a mix; a changed `position` is re-seated on the next sync - add / update-in-place / remove keyed on `id`; unchanged re-sync byte-identical; `deinit` byte-identical with no orphan object or section - a 17-row hostile-`name` matrix (`{ } ( ) , ; = */ /* * /`, unbalanced quote, tab, unicode, 300 chars, a name that normalizes to empty) × {balanced after add, byte-identical re-inject, clean deinit} - scripts containing quotes, backslashes, newlines and `$(VAR)` round-trip through emission, refresh and deinit - contract validation: 16 malformed-entry cases, duplicate `id` across plugins, reserved ids, scoped ids accepted, `:` rejected - `add → update → deinit` byte-identity with zero phases and with two **Not covered by unit tests, deliberately:** that `end` runs after the JS bundle phase. The `plain-app.pbxproj` fixture has no bundle phase, so it is unassertable here — this is disclosed in a comment at the test rather than papered over, and is exactly what the Expo verification above establishes. **Known limitation, not addressed here:** against a project Xcode has previously saved, `add → deinit → add` is structurally identical (same UUIDs, same reference counts) but not byte-identical — Xcode writes multi-line dicts in sorted order, the injector writes single-line dicts in insertion order, which shows up as formatting churn in a committed `project.pbxproj`. Pre-existing for every object the injector emits, not specific to script phases, and filed separately. ## Note on landing order This touches `generate-spm-xcodeproj.js` and `spm-pbxproj.js`, which #57744 and #57756 also touch — including the same marker-write block. All three are cut independently from `main`; whichever lands first, I'll rebase the others. Happy to restack in whatever order is easiest to review. Reviewed By: fabriziocucci Differential Revision: D114318236 Pulled By: cipolleschi fbshipit-source-id: 4aa7958c302323299eda9db17adcec0d86edeebe
`spm add` merges its array build settings (`HEADER_SEARCH_PATHS`, `OTHER_LDFLAGS`, `FRAMEWORK_SEARCH_PATHS`, `LD_RUNPATH_SEARCH_PATHS`) via `addArrayStringValues`, which has three add paths: create the field, append to an existing array, or promote an existing SCALAR into a `( … )` array. Only the first two were reversible. A promotion was recorded as `appendedArrayValues`, whose reversal only strips the injected members — so `spm deinit` left the promoted array and its `"$(inherited)"` seed behind, and the original scalar was never recorded anywhere to restore from. A scalar is the ordinary shape in a real project (`HEADER_SEARCH_PATHS = "$(inherited)";` in the Debug config), so this broke the byte-identical restore `deinit` promises on a common input. Record the pre-injection value in a new `promotedArrayScalars` field on the marker's `BuildSettingChange` and restore it in place. Notes on the details: - The recorded value is kept RAW. `findField`'s token for a bare scalar runs to the `;`, so it carries any whitespace before it, and deinit has to write those bytes back. The value emitted as an array MEMBER is still trimmed — a member with trailing whitespace would be malformed. The two differ deliberately. - The record is gated on the merge having actually changed the text. `addArrayStringValues` no-ops when its value list is empty (as `FRAMEWORK_SEARCH_PATHS` is with no flavored frameworks) or when every value is already present, and a recorded-but-untouched field would have deinit clobber whatever the user has there by then. - Restoration is skipped when the field is absent at deinit time: it was deleted after injection, and re-adding it would resurrect it at the top of the dictionary, matching neither the original nor the user's intent. - Promotion no longer re-emits a prior value that is itself `"$(inherited)"` (the seed) or empty (a bare `,` is not a valid plist element). Reversing a promotion rewrites the whole field, because the injected members and the seed are indistinguishable from the user's own once folded together. Members hand-added to a promoted array afterwards are therefore lost; the removal-helper banner in spm-pbxproj.js now names that exception. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tradeoff Review feedback on the promoted-scalar restore: - The seed guard compared the prior scalar against the quoted `"$(inherited)"` only, so an unquoted `$(inherited)` — equally valid, and present in the suite's own untrimmed-scalar fixture — was re-emitted alongside the seed. Deinit still restored it byte-identically (the record is raw), so this was duplication rather than breakage, but it defeated the guard. Compare unquoted, and parametrize the guard's unit test over both spellings so the injected shape is asserted, not just the post-deinit bytes. - The reversal tradeoff is not deinit-only: every re-sync reverts from the recorded baseline before re-injecting, so an `spm update` discards hand-added members just the same. Say so in the banner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
addArrayStringValues' "already an array" branch assumed the array was
multi-line: it anchored on `lastIndexOf('\n', tokenEnd - 1)`. For a single-line
array there is no newline inside the value, so that lands at the end of the
PREVIOUS line and the new members were spliced above the field — outside the
array, as a bare entry in the dict body:
{
"/new", <- invalid pbxproj
HEADER_SEARCH_PATHS = ("/vendor", ); <- member never added
OTHER = 1;
}
One `spm add` against such a project produced a file Xcode cannot open, and
because removeArrayStringValues only searches inside the field's value region,
`deinit` could never remove the stray line. Xcode writes multi-line arrays, but
hand-edited projects and other generators (XcodeGen, Tuist) emit compact ones.
Splice the members inline ahead of the `)` instead, matching the separator style
already present and honouring an existing trailing comma. Reformatting to
multi-line would change the user's formatting and would need the old shape
recorded to stay reversible, for no benefit. removeArrayStringValues gains
delimiter-anchored patterns for the shapes `add` can now produce, so the span
removed is exactly the span inserted and every shape round-trips byte-for-byte.
The dedupe parse was also quote-blind: it split members on every `,`, so a
member holding a quoted comma (`"$(FOO(x)),weird"`) parsed as two tokens and
defeated the exact-token short-circuit. It now uses a quote-aware splitter.
The multi-line path is unchanged byte-for-byte, verified by a differential
harness against the previous implementation over 48 add/remove cases, plus a
test pinning its exact output bytes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`spm add` promotes an array build setting that is already present as a scalar (`HEADER_SEARCH_PATHS`, `OTHER_LDFLAGS`, `FRAMEWORK_SEARCH_PATHS`, `LD_RUNPATH_SEARCH_PATHS`), and reversing that promotion rewrites the whole field, because the injected members and the user's own are indistinguishable once folded together. Members hand-added to such an array afterwards are lost on `deinit`, and on `update`, which reverts to the recorded baseline before re-injecting. The docs claimed the pre-`add` restore was byte-identical with no qualification. - Follow a stock Xcode app target being an instance of this (the template writes `LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";` as a target-level scalar), so the caveat reads as concrete rather than theoretical, and note that an existing array keeps the shape it was written in — including the one-line form hand edits and other generators emit. - The `.spm-injected.json` row of "What to commit" described the marker as a record of injected edits only; it also pins the pre-injection value of a build setting `add` rewrote. Docs only; no behavior change. [Internal] - SwiftPM: document that `deinit` cannot preserve members added by hand to a promoted array build setting Prose review of `packages/react-native/scripts/spm/__doc__/spm-scripts.md`. Every claim traced to this PR's code: the recorded marker key (`promotedArrayScalars` on `BuildSettingChange`), the settings merged as arrays (`INJECTED_ARRAY_SETTINGS` + `frameworkArrayBuildSettings`), the whole-field rewrite in `removeRecordedBuildSettings`, and `update` re-applying from `removeRecordedBuildSettings(original, prevMarker…)`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`deinit` removes `appendedArrayValues` before it restores `promotedArrayScalars`, so recording a key under both currently comes out right — the scalar restore rewrites the whole value last. That makes the exclusivity invisible to a round-trip test, and reversing those two loops would silently start removing array members from an already-restored scalar. Assert it on the marker instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chrfalch
force-pushed
the
chrfalch/spm-promoted-array-scalar-restore
branch
from
August 18, 2026 08:54
c105182 to
52cb9ff
Compare
|
@cipolleschi merged this pull request in 8ac3e2f. |
This was referenced Aug 19, 2026
meta-codesync Bot
pushed a commit
that referenced
this pull request
Aug 19, 2026
…correct the rest (#58006) Summary: Rebased onto `main` after #57757, #57756, #57762 and #57744 landed. Because this PR renames `__doc__` and Prettier-formats every file, a hunk-level rebase was the wrong tool — it stopped on the second of nine commits and the reformat commit then conflicted with everything after it. So the history is **replayed** on top of the new `main` instead, and reordered to put Prettier *before* the content edits, which makes the substantive diff reviewable rather than tangled with reflow. Verified nothing those four PRs added was lost: every heading on `main` survives except the two this PR deliberately renames, and their facts — the `artifactsVersionOverride` pin, the autolinking-config-command pin, the pre-injection build-setting values, and the promoted-scalar caveat — are carried into the new "Files the tool touches" table rather than sitting in a row this PR deletes. Housekeeping and accuracy pass over `packages/react-native/scripts/spm/__docs__`. Docs only — no code or test changes. **Rename.** `scripts/spm/__doc__` was the only `__doc__` directory in the repo; every other subsystem uses `__docs__` (27+ directories, and the convention documented in `__docs__/GUIDELINES.md`). The misspelling also meant these files were **published to npm**: `package.json` includes `scripts/spm` in `files` and excludes `!**/__docs__/**`, which the misspelled directory dodged. **Removed two superseded documents.** `rfc-spm-xcframework.md` (707 lines) was a stale ancestor of RFC0994, still describing `spm init`, xcodeproj *generation*, the `.xcodeproj.legacy` rename migration, a `spm clean` command that does not exist, stub `Package.swift` files and VFS overlays. `spm-plugins-assessment.md` evaluated SwiftPM plugins as a replacement for the injected build phases — conclusion valid, detail stale (it listed a "Prepare VFS Overlay" phase). Both have had their durable content folded into RFC0994. **Corrected the remaining docs against the implementation.** The substantive one: auto-sync is **two** hooks, not one — a scheme pre-action in the app's *shared* scheme plus the build phase. The docs described only the phase. Two claims here are corrections of my own earlier drafts, made after testing them on `private/helloworld` rather than reading the code: - Neither hook can bootstrap a clean checkout. With `build/` deleted, `xcodebuild -scheme … build` fails in nine lines of log, `Resolve Package Graph` first, the pre-action never running. The one-time setup run really is required, and the ordering table now shows the measured sequence. - A sync failure is not unconditionally non-fatal. Exit 2 — a dependency with no `Package.swift` — maps to `exit 1` and fails the build deliberately; only other non-zero codes warn. As written, the doc also contradicted the exit-2 behaviour documented elsewhere in the same file. Smaller fixes: `hermesvm` → `hermes-engine` (that name is in no code), plugin `watchPaths` added to the staleness inputs, and the `#auto-sync-build-phase` anchors left dangling by the heading rename. **Added a `__docs__/README.md` index**, per `GUIDELINES.md`, plus a **"Files the tool touches"** table — there was no single answer to what the tool creates or modifies. Writing it surfaced that `add` creates or appends to `ios/.gitignore` (documented nowhere), and that `deinit` does not revert that block, anything `--deintegrate` changed, or a promoted array setting — so the "exact inverse of `add`" claim needed qualifying in three places. **Documented `spm.dependencies`**, the SwiftPM analog of a podspec's `s.dependency`, which was implemented but absent from every doc, and corrected the scaffold prerequisite: it is `add`/`update` that stops, with a distinct exit code 2, not the build. **Prettier-formatted the docs, in its own commit.** These were the only unformatted markdown files in the repository, so `prettier --list-different "./**/*.md"` goes from three failures to clean. ## Changelog: [Internal] - SwiftPM docs: rename `__doc__` to `__docs__`, remove two superseded documents, and correct the rest against the implementation Pull Request resolved: #58006 Test Plan: - `npx prettier --check "packages/react-native/scripts/spm/__docs__/*.md"` passes; `npx prettier --list-different "./**/*.md"` is empty for the whole repo. - All 19 intra-doc anchor links verified against GitHub's slug rules; none dead. Every relative path out of the new README resolves, including the root-index hop. - `npm pack --dry-run --ignore-scripts` lists no `scripts/spm/__docs__/` entries while every `scripts/spm/*.js` still ships. - The Prettier commit is content-neutral: comparing `[A-Za-z0-9]+` token streams across it, all three files are identical word for word. - Behavioural claims traced to source: `VALID_ACTIONS` in `setup-apple-spm.js`; `injectOrCreateScheme` / `addPreActionToScheme` and the `RC -eq 2` branch of `buildSyncAutolinkingScript` in `generate-spm-xcodeproj.js`; `BUILTIN_FRAMEWORKS` in `flavored-frameworks.js`; `generateXCFrameworksPackageSwift` in `generate-spm-package.js`; `ensureGitignoreSpmEntries` and its `action =--sanitized-- Reviewed By: fabriziocucci Differential Revision: D116598871 Pulled By: cipolleschi fbshipit-source-id: b1822dcc885b9c882ec3a2626addc42553244b51
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:
Two bugs in
addArrayStringValues, which adds members to an array build setting (HEADER_SEARCH_PATHS,OTHER_LDFLAGS,FRAMEWORK_SEARCH_PATHS,LD_RUNPATH_SEARCH_PATHS). Both hit real projects; neither was visible from the existing fixture.1. A promoted scalar was never restored. A setting that already exists as a scalar gets promoted to a
( … )array, but that was recorded as a plain member-append — sodeinitstripped the members and left the array shell plus its injected"$(inherited)"behind. Stock Xcode projects hit this: the app template setsLD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";as a target-level scalar.Fixed by pinning the pre-injection value in
.spm-injected.jsonand restoring it in place. It is stored raw (a bare scalar's token runs to the;, carrying whitespace that must come back), recorded only if the merge actually changed the field, and not restored if the field is gone.2. A one-line array was corrupted. The append anchored on
lastIndexOf('\n', tokenEnd - 1), which assumes multi-line. With no newline in the value that lands on the previous line, so members were spliced above the field, outside the array:The result is a project Xcode cannot open, and
deinitcould not remove the stray line. Xcode writes multi-line arrays, but hand-edited projects and other generators (XcodeGen, Tuist) emit compact ones. Fixed by splicing inline ahead of the); removal gained matching delimiter-anchored patterns, so the span removed is the span inserted. The dedupe parse was also quote-blind — a member holding a quoted comma parsed as two tokens — and is now quote-aware.Tradeoff: reversing a promotion rewrites the whole field, so members hand-added to a promoted array afterwards are lost.
Rebase note:
mainhas since grown an overlapping guard (buildSettingValueTokens) that skips the append when every value is already present, avoiding the no-op promotion. It is kept and complements this change: main still promotes irreversibly when there is a fresh value to add to a scalar, which is what the restore here covers. The two records stay mutually exclusive per key, pinned by a test.Changelog:
[Internal] [Fixed] - SwiftPM:
spm deinitrestores a promoted scalar build setting, andspm addno longer corrupts a one-line arrayTest Plan:
yarn jest packages/react-native/scripts→ 963 tests, 32 suites green; eslint, prettier and flow clean.Written red first: byte-identical
add→deinitround-trips for each pre-existing shape (absent, multi-line, bare and quoted scalars, and the one-line forms), plusadd→update→deinit. The multi-line path is unchanged byte-for-byte, verified by a differential harness over 48 add/remove cases against the previous implementation.Re-verified after the rebase on the committed
HelloWorld.xcodeproj, driving the realinjectSpmIntoExistingXcodeproj/removeSpmInjection. Onmaina one-lineHEADER_SEARCH_PATHSgains a bare"…/autolinking/headers",entry above the field and never receives the member; with this change it lands inside the array, and a pre-existing scalar comes back exactly.