Skip to content

SPM: fix two ways array build settings were mishandled - #57744

Open
chrfalch wants to merge 3 commits into
mainfrom
chrfalch/spm-promoted-array-scalar-restore
Open

SPM: fix two ways array build settings were mishandled#57744
chrfalch wants to merge 3 commits into
mainfrom
chrfalch/spm-promoted-array-scalar-restore

Conversation

@chrfalch

@chrfalch chrfalch commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

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, so deinit stripped 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 sets LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; as a target-level scalar.

Fixed by pinning the pre-injection value in .spm-injected.json and 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 (otherwise deinit would overwrite a setting we never touched); and restoration is skipped if the field is gone, so a setting deleted after add isn'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:

{
		"/new",                             ← bare entry in the dict body: invalid pbxproj
	HEADER_SEARCH_PATHS = ("/vendor", );    ← member never added
}

One spm add produced a project Xcode can't open, and deinit couldn'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 shapes add now 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 deinit restores a promoted scalar build setting, and spm add no longer corrupts a one-line array

Test Plan:

yarn jest packages/react-native/scripts722 tests, all green.

Written red first. Byte-identical adddeinit round-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"). Plus addupdatedeinit, a user-added member surviving deinit, 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.

`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>
@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 Jul 28, 2026
@chrfalch
chrfalch requested a review from cipolleschi July 28, 2026 17:47
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jul 28, 2026
…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>
@chrfalch chrfalch changed the title SPM: restore a promoted scalar build setting on deinit fix(swiftpm): restore a promoted scalar build setting on deinit Jul 30, 2026
@chrfalch chrfalch changed the title fix(swiftpm): restore a promoted scalar build setting on deinit SPM: fix two ways array build settings were mishandled (promoted scalars, one-line arrays) Jul 30, 2026
@chrfalch chrfalch changed the title SPM: fix two ways array build settings were mishandled (promoted scalars, one-line arrays) fix(swiftpm): fix two ways array build settings were mishandled (promoted scalars, one-line arrays) Jul 30, 2026
@chrfalch chrfalch changed the title fix(swiftpm): fix two ways array build settings were mishandled (promoted scalars, one-line arrays) SPM: fix two ways array build settings were mishandled Jul 30, 2026
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 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.

1 participant