Skip to content

SPM: persist --config-command so the in-build sync keeps using it - #57756

Draft
chrfalch wants to merge 1 commit into
mainfrom
chrfalch/spm-persist-config-command
Draft

SPM: persist --config-command so the in-build sync keeps using it#57756
chrfalch wants to merge 1 commit into
mainfrom
chrfalch/spm-persist-config-command

Conversation

@chrfalch

Copy link
Copy Markdown
Collaborator

Summary:

spm add --config-command worked once, then broke every build.

An app that replaces @react-native-community/cli autolinking — an Expo app, for instance — has to override the autolinking config command, which --config-command (and RCT_SPM_AUTOLINKING_CONFIG_COMMAND) exists to do. But the flag was never stored anywhere. So:

  1. npx react-native spm add --config-command '[...]' → succeeds, writes a valid project ✅
  2. Build in Xcode → the "Sync SPM Autolinking" phase re-derives autolinking.json, doesn't know about the flag, falls back to the default command, and fails ❌
PhaseScriptExecution failed with a nonzero exit code
  → Sync SPM Autolinking
  → 'npx --no-install @react-native-community/cli config' exited with status 1

The only real workaround was exporting RCT_SPM_AUTOLINKING_CONFIG_COMMAND into 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.json marker at add/update time, and read it back on later runs — the same set-or-preserve pin the neighbouring artifactsVersionOverride already uses.

Resolution order, unchanged at the front and only extended at the back:

--config-command  →  RCT_SPM_AUTOLINKING_CONFIG_COMMAND  →  pinned value  →  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 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:

  • 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 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.
  • 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 instead of injecting a bogus argv into a build.

Because this is now persistent state, add/update logs 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 of deinit, 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

export RCT_SPM_AUTOLINKING_CONFIG_COMMAND='["node","…/expo-modules-autolinking.js","react-native-config","--json","--platform","ios"]'

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.env only when NODE_BINARY is unset (nodeAndRnDirPreamble). An app that sets NODE_BINARY as 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-command so the in-build autolinking sync keeps using it

Test Plan:

yarn jest packages/react-native/scripts31 suites, 703 tests (25 new).

Each new test written red first, covering:

  • the command round-trips through .spm-injected.json (marker content asserted)
  • addupdate without the flag keeps the pin; a later flag overwrites it
  • add with only the env var set pins the env-derived command, and a later run with neither flag nor env resolves it back
  • all four resolution-order cases, including that the env var beats the pin, and that a whitespace-only env var pins nothing and falls through
  • an invalid non-blank env var still fails loud rather than pinning garbage
  • a corrupt or hand-edited pin (bare string, [], non-string member, empty-string member, object) degrades to the default rather than throwing
  • deinit drops it with the marker

Not covered: no test drives a real sync end to end, since that needs artifacts, codegen and a real pbxproj. The two halves are tested separately against the same marker field — the injector writes configCommand, 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.

`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>
@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 29, 2026
@chrfalch
chrfalch marked this pull request as draft July 29, 2026 17:26
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant