SPM: persist --config-command so the in-build sync keeps using it - #57756
Draft
chrfalch wants to merge 1 commit into
Draft
SPM: persist --config-command so the in-build sync keeps using it#57756chrfalch wants to merge 1 commit into
chrfalch wants to merge 1 commit into
Conversation
`spm add --config-command '<argv>'` succeeded and wrote a valid project, then
every subsequent Xcode build failed: the injected "Sync SPM Autolinking" phase
re-derives autolinking.json on each build, had no knowledge of the flag, and
fell back to `npx --no-install @react-native-community/cli config` — which an
app that replaces CLI autolinking (an Expo app, say) does not have. A
successful `spm add` therefore produced an unbuildable project. Reported by the
Expo team while verifying SwiftPM.
Pin the command into the `.spm-injected.json` marker at add/update time and read
it back on later runs, mirroring the neighbouring `artifactsVersionOverride`
set-or-preserve pin. Resolution order is unchanged at the front and only
extended at the back:
--config-command -> RCT_SPM_AUTOLINKING_CONFIG_COMMAND -> pin -> default
Both input routes persist. The help text advertises the env var as an
equivalent way to supply the command, so pinning only the flag would have left
half the documented interface broken the same way; the pin stores the resolved
command from either route.
Two details worth knowing:
- generateAutolinkingConfig resolves the env var internally when no explicit
command is passed, so handing it the pin would silently outrank a developer's
env override. The read path withholds the pin while the env var is set and
lets the existing precedence apply. A whitespace-only env var pins nothing and
falls through, using the same blankness predicate as the resolver so the two
cannot drift.
- A pinned value is re-validated through the same parseConfigCommandJson the
flag goes through, so a hand-edited or corrupt marker degrades to the
env/default command rather than injecting a bogus argv into a build.
Because this is persistent state, add/update logs one line when the command
comes from the pin, naming .spm-injected.json, so a stale pin is diagnosable
from build output instead of invisible. There is no "clear" verb short of
`deinit`, as with the version pin.
Also corrects two comments asserting that the build-time sync reads
`readArtifactsVersionOverride`. It does not — that reader has no production
caller, so only the write half of the version pin is wired. Wiring it up is a
separate change; the comments are fixed here because they are actively
misleading.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chrfalch
marked this pull request as draft
July 29, 2026 17:26
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:
spm add --config-commandworked once, then broke every build.An app that replaces
@react-native-community/cliautolinking — an Expo app, for instance — has to override the autolinking config command, which--config-command(andRCT_SPM_AUTOLINKING_CONFIG_COMMAND) exists to do. But the flag was never stored anywhere. So:npx react-native spm add --config-command '[...]'→ succeeds, writes a valid project ✅autolinking.json, doesn't know about the flag, falls back to the default command, and fails ❌The only real workaround was exporting
RCT_SPM_AUTOLINKING_CONFIG_COMMANDinto Xcode's environment — i.e. committing it to.xcode.env— which shouldn't be necessary when you already passed a flag. Reported by the Expo team while testing SwiftPM.Fix: pin the command into the
.spm-injected.jsonmarker atadd/updatetime, and read it back on later runs — the same set-or-preserve pin the neighbouringartifactsVersionOverridealready uses.Resolution order, unchanged at the front and only extended at the back:
Both input routes persist. The help text advertises the env var as an equivalent way to supply the command, so pinning only the flag would have left half the documented interface broken in exactly the same way — export the env var, run
spm add, and the build phase (which does not inherit your shell) still fails. The pin therefore stores the resolved command from either route.Two subtleties worth a reviewer's eye:
generateAutolinkingConfigresolves the env var internally when no explicit command is passed, so handing it the pin would silently outrank a developer's env override. The read path therefore withholds the pin while the env var is set, letting the existing precedence do its job. The four order cases are tested, including that a whitespace-only env var falls through to the pin rather than stranding it.parseConfigCommandJsonthe flag goes through, so a hand-edited or corrupt marker degrades to the env/default command instead of injecting a bogus argv into a build.Because this is now persistent state,
add/updatelogs one line when the command comes from the pin, naming.spm-injected.json— a stale pin should be diagnosable from build output rather than invisible. There is no "clear" verb short ofdeinit, same as the version pin; that is noted in the marker comment.122 added lines across three source files. No new mechanism, no changes to the sync scripts, and nothing baked into the generated build phase.
Noticed while here, filed separately, deliberately not fixed:
readArtifactsVersionOverride— the version pin this is modelled on — has no production caller. Only its write half is wired, and two comments claim the build-time sync reads it. Those comments are corrected here (they misled me while writing this); wiring the version pin up is its own change.This isn't blocking anyone
Expo's SwiftPM verification is not blocked on this, so it needn't be rushed. The env-var half of the override already works at build time: adding
to the app's
.xcode.env(or.xcode.env.local) gets the command into the sync phase, because the generated phase sources both files before dispatching. That is what unblocks Expo today, and it is exactly the "commit an env var to.xcode.env" step this PR removes the need for.One caveat that argues for fixing it properly rather than documenting the workaround: the phase sources
.xcode.envonly whenNODE_BINARYis unset (nodeAndRnDirPreamble). An app that setsNODE_BINARYas an Xcode build setting — a documented RN practice — never sources those files, so the workaround silently does nothing there and the build fails with no hint as to why. Persisting the flag doesn't depend on any of that plumbing.Changelog:
[Internal] [Fixed] - SwiftPM: persist
spm --config-commandso the in-build autolinking sync keeps using itTest Plan:
yarn jest packages/react-native/scripts→ 31 suites, 703 tests (25 new).Each new test written red first, covering:
.spm-injected.json(marker content asserted)add→updatewithout the flag keeps the pin; a later flag overwrites itaddwith only the env var set pins the env-derived command, and a later run with neither flag nor env resolves it back[], non-string member, empty-string member, object) degrades to the default rather than throwingdeinitdrops it with the markerNot covered: no test drives a real
syncend to end, since that needs artifacts, codegen and a real pbxproj. The two halves are tested separately against the same marker field — the injector writesconfigCommand, and the resolver reads it. The original failure was reported from a real Expo app build; confirmation that this fixes that build is still pending on the Expo side.