SPM: fix two ways array build settings were mishandled - #57744
Open
chrfalch wants to merge 3 commits into
Open
Conversation
`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>
This was referenced Jul 29, 2026
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>
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, the helper that 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. When the setting already exists as a scalar, it 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. The original scalar was never recorded, so it couldn't be put back. 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. Three details: the value is stored raw (a bare scalar's token runs to the;, so it carries whitespace that must come back); it's recorded only if the merge actually changed the field (otherwisedeinitwould overwrite a setting we never touched); and restoration is skipped if the field is gone, so a setting deleted afteraddisn't resurrected.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:One
spm addproduced a project Xcode can't open, anddeinitcouldn't 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
), matching the separator style already there. Reformatting to multi-line would change the user's formatting and need the old shape recorded to stay reversible. Removal gained delimiter-anchored patterns for the shapesaddnow produces, so the span removed is the span inserted.Also: the dedupe parse split on every
,, so a member holding a quoted comma ("$(FOO(x)),weird") parsed as two tokens and defeated the exact-token check. Now quote-aware.Known tradeoff: reversing a promotion rewrites the whole field, since 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 lost; the removal-helper banner says so.
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→ 722 tests, all green.Written red first. Byte-identical
add→deinitround-trip for every pre-existing shape: absent, multi-line array, bare and quoted scalars (including trailing whitespace and an empty value), and the one-line forms(),("/a"),("/a", ),("/a","/b"),("/a", "/b"). Plusadd→update→deinit, a user-added member survivingdeinit, and an injector-level test that a project with a one-line setting stays delimiter-balanced.The multi-line path is unchanged byte-for-byte — verified with a differential harness loading the previous implementation alongside the new one over 48 add/remove cases, plus a test pinning its exact output bytes.