Skip to content

[iOS][SPM] Derive the generated manifests' iOS platform floor from the app - #58379

Open
chrfalch wants to merge 5 commits into
mainfrom
chrfalch/spm-ios-deployment-target-floor
Open

[iOS][SPM] Derive the generated manifests' iOS platform floor from the app#58379
chrfalch wants to merge 5 commits into
mainfrom
chrfalch/spm-ios-deployment-target-floor

Conversation

@chrfalch

@chrfalch chrfalch commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Summary:

The SwiftPM manifests React Native generates — the Autolinked aggregate, the per-dependency synth packages, and the scaffolded community packages — hardcoded platforms: [.iOS(.v15)]. SwiftPM refuses to link a product whose minimum platform is above the depending target's, so any self-managed package with a higher floor could not be autolinked. Every Expo package declares iOS 16.4, so a stock Expo app on 0.87.1 fails at build time with:

error: The package product 'Expo' requires minimum platform version 16.4 for the iOS platform,
       but this target supports 15.0 (in target 'AutolinkedAggregate' from project 'Autolinked')

The app itself is correct (IPHONEOS_DEPLOYMENT_TARGET = 16.4); the only disagreement was the generated manifests.

The floor is now the app's own IPHONEOS_DEPLOYMENT_TARGET, read from the injected .xcodeproj:

  • Target selection: the injection marker's targetUuid, else the application target named by --product-name, else every application target.
  • Per configuration: the target's own value, else the project-level configuration of the same name. The lowest value wins, since one manifest floor has to hold for every configuration.
  • Clamped to React Native's minimum (15.1, min_ios_version_supported) and emitted in string form (.iOS("16.4")), because the .vNN enum form cannot express a minor version.

setup-apple-spm.js resolves the value once per add/update/sync/scaffold run, logs it (iOS deployment target: 16.4 (from MyApp.xcodeproj)), and passes it to the generator, the sync script, and the scaffolder via a new --ios-deployment-target flag. The flag value is sanitized at the generator boundary as well. SCAFFOLDER_VERSION is bumped to 20 so existing scaffolds regenerate with the new floor.

A floor set in an .xcconfig the configuration is based on is honored, including #include chains (target literal → target xcconfig → project literal → project xcconfig). A floor supplied through a build-setting variable ($(MY_FLOOR)) is not resolved and falls back to the minimum; the log line says so. spm add and spm update also refresh the platforms: element of existing scaffolded manifests (those carrying the scaffolder marker, including the pre-v20 .iOS(.v15) form, over the same expanded dependency set the scaffolder uses, transitive SwiftPM dependencies included) without creating new scaffolds or touching anything else in them.

Known limitation, documented in scripts/spm/__docs__/spm-scripts.md: the project file is not one of the auto-sync build phase's staleness inputs, so a deployment target changed after spm add takes effect on the next spm update.

The default floor moves from 15.0 to 15.1 for apps without a readable project setting; React Native already requires 15.1 for the app target, so this is not observable.

Changelog:

[IOS] [FIXED] - SwiftPM: generated manifests derive their iOS platform floor from the app's IPHONEOS_DEPLOYMENT_TARGET instead of hardcoding iOS 15, so dependencies with a higher minimum (e.g. Expo, 16.4) can be autolinked

Test Plan:

Unit tests were written first and seen failing, then made green:

yarn jest --no-cache -i packages/react-native/scripts/spm
Test Suites: 37 passed, 37 total
Tests:       1733 passed, 1733 total

New/updated coverage: ios-deployment-target-test.js (pbxproj reading: target-level, project-level fallback, xcconfig chain with #include and <group> anchoring, minimum across configurations, marker/--product-name/all-targets selection, invalid values, clamping, unreadable project), scaffold-package-swift-test.js (emitted floor, refresh of existing scaffolded manifests), generate-spm-autolinking-test.js (aggregate and synth templates, --ios-deployment-target through main()), scaffold-package-swift-test.js, sync-spm-autolinking-test.js (flag forwarding), setup-apple-spm-test.js (resolution and log line).

ESLint (--max-warnings 0), Prettier, and Flow are clean on the touched files.

Not run here: an end-to-end Xcode build of an Expo app on this branch. The failing scenario and the fix were verified by the Expo team against 0.87.1 with the same manifest change patched in locally (build proceeds past SwiftPM resolution into compiling Expo).

🤖 Generated with Claude Code

@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 Sep 7, 2026
@chrfalch
chrfalch marked this pull request as ready for review September 9, 2026 07:46
@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 Sep 9, 2026

@cipolleschi cipolleschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of questions about deployment-target propagation:

Comment thread packages/react-native/scripts/spm/ios-deployment-target.js
Comment thread packages/react-native/scripts/setup-apple-spm.js
@chrfalch
chrfalch requested a review from cipolleschi September 9, 2026 10:20
Comment thread packages/react-native/scripts/spm/scaffold-package-swift.js Outdated
@cipolleschi

Copy link
Copy Markdown
Contributor

Thanks for addressing the earlier feedback! Could you please also rebase this PR onto the latest main? It is currently showing merge conflicts.

Copilot stopped work on behalf of rachel11026725 due to an error September 10, 2026 10:35
@chrfalch
chrfalch force-pushed the chrfalch/spm-ios-deployment-target-floor branch from 54d0f61 to 19dc7a9 Compare September 10, 2026 10:37
chrfalch and others added 5 commits September 10, 2026 13:39
…e app

The Autolinked aggregate, the per-dependency synth packages and the
scaffolded community packages hardcoded `platforms: [.iOS(.v15)]`. SwiftPM
refuses to link a product whose minimum platform is above the depending
target's, so any self-managed package with a higher floor (every Expo
package: iOS 16.4) failed to resolve:

  The package product 'Expo' requires minimum platform version 16.4 for the
  iOS platform, but this target supports 15.0 (in target 'AutolinkedAggregate')

The floor is now the app's own IPHONEOS_DEPLOYMENT_TARGET, read from the
injected .xcodeproj (the marker's target, else the `--product-name` target,
else every application target; target configuration first, then the
project-level configuration of the same name; the lowest wins), clamped to
React Native's minimum (15.1) and emitted in string form (`.iOS("16.4")`).
setup-apple-spm.js resolves it once per run and passes it to the generator,
the sync script and the scaffolder via `--ios-deployment-target`.
SCAFFOLDER_VERSION is bumped so existing scaffolds regenerate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Drop table rows and assertions that exercised the same branch twice or
pinned internal layering, share the pbxproj fixture builders between the
two suites that need a two-target project, and turn the helpers.rb parity
test into a real read of min_ios_version_supported. Two matching code
simplifications: the reader no longer normalizes (the comparison is
numeric and the resolver sanitizes), and the marker uuid is used without
an `isa` guard (a stale uuid still degrades to the default).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A configuration without a literal deployment target now falls back to the
xcconfig its baseConfigurationReference points at, per Xcode's precedence
(target literal, target xcconfig, project literal, project xcconfig).
`<group>` references are anchored through the PBXGroup chain, `#include`
lines are followed in place so a later assignment in the includer wins,
and only the unconditional key with a plain version counts. A floor set
through a build-setting variable is still not resolved and falls back to
React Native's minimum.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…update

`spm add` and `spm update` now rewrite the `platforms: [.iOS(...)]` element
of every scaffolded manifest carrying the scaffolder marker to the app's
deployment target, covering the `.iOS(.v15)` form older scaffolds carry.
Nothing else in the file is touched and no scaffold is created; the full
re-render stays with `spm scaffold`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The floor refresh visited only the direct autolinking.json entries, while
the scaffolder covers the expanded set including transitive SwiftPM
dependencies, so a previously scaffolded transitive dep kept its old floor
after `spm update`. Both now share one dependency-collection helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chrfalch
chrfalch force-pushed the chrfalch/spm-ios-deployment-target-floor branch from 19dc7a9 to 167dc22 Compare September 10, 2026 11:39
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.

2 participants