Skip to content

Add Swift Package Manager opt-ins for RN_STRICT_API and RN_ALLOW_FRAMEWORKS (#59043) - #59043

Closed
coado wants to merge 1 commit into
react:mainfrom
coado:export-D124985657
Closed

coado wants to merge 1 commit into
react:mainfrom
coado:export-D124985657

Conversation

@coado

@coado coado commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

Let iOS apps that use the Swift Package Manager setup enforce React Native's C++ stable API, matching the CocoaPods and Android opt-ins. Two env vars are read by npx react-native spm add / update:

  • RN_STRICT_API=1 defines the RN_STRICT_API macro, which arms the react/cxxstableapi guards.
  • RN_ALLOW_FRAMEWORKS=1 defines the RN_ALLOW_FRAMEWORKS macro, which silences their "for frameworks" warning.

Each variable is named after the macro it defines, as in the CocoaPods opt-in.

Each opted-in macro is defined for the app's own code:

  • the app target (GCC_PREPROCESSOR_DEFINITIONS, seeded with $(inherited)),
  • the packages the autolinker generates for the app's local spm.modules,
  • the app codegen package (ReactCodegen, ReactAppDependencyProvider).

Generated code never warns: the codegen package always defines RN_ALLOW_FRAMEWORKS, because nobody can act on a warning in generated code and libraries are held to the API in their own builds.

Under SwiftPM every library builds from its own Package.swift, so a library declares RN_ALLOW_FRAMEWORKS there, and the app's opt-ins do not reach libraries. React Native itself ships prebuilt, so none of its code is compiled with these macros.

The Xcode build phase runs spm sync without the shell environment, so add/update record the chosen macros in .spm-injected.json and every later sync regenerates the packages from that record. As with pod install, each add/update re-reads the variables: running update without one turns its macro off, and deinit removes the app-target defines along with the rest of the injection.

Changelog:
[iOS][Added] - Add RN_STRICT_API and RN_ALLOW_FRAMEWORKS opt-ins for the Swift Package Manager setup to enforce React Native's C++ stable API in the app's native code

Reviewed By: cipolleschi

Differential Revision: D124985657

@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 Oct 13, 2026
@facebook-github-tools facebook-github-tools Bot added p: Software Mansion Partner: Software Mansion Partner labels Oct 13, 2026
@meta-codesync

meta-codesync Bot commented Oct 13, 2026

Copy link
Copy Markdown

@coado has exported this pull request. If you are a Meta employee, you can view the originating Diff in D124985657.

@facebook-github-tools facebook-github-tools Bot added the p: Facebook Partner: Facebook label Oct 13, 2026
@meta-codesync meta-codesync Bot changed the title Add Swift Package Manager opt-in for RN_STRICT_API Add Swift Package Manager opt-ins for RN_STRICT_API and RN_ALLOW_FRAMEWORKS (#59043) Oct 14, 2026
@coado
coado force-pushed the export-D124985657 branch from afdfbd7 to bc5bc92 Compare October 14, 2026 10:22
coado added a commit to coado/react-native that referenced this pull request Oct 14, 2026
…EWORKS (react#59043)

Summary:

Let iOS apps that use the Swift Package Manager setup enforce React Native's C++ stable API, and control its "for frameworks" warning, matching the CocoaPods and Android opt-ins. Two env vars are read by `npx react-native spm add` / `update`:

- `RN_STRICT_API=1` defines the `RN_STRICT_API` macro, which arms the `react/cxxstableapi` guards.
- `RN_ALLOW_FRAMEWORKS=1` defines the `RN_ALLOW_FRAMEWORKS` macro, which silences their "for frameworks" warning.

Each variable is named after the macro it defines, as in the CocoaPods opt-in.

Each opted-in macro is defined for the app's own code:
- the app target (`GCC_PREPROCESSOR_DEFINITIONS`, seeded with `$(inherited)`),
- the packages the autolinker generates for the app's local `spm.modules`,
- the app codegen package (`ReactCodegen`, `ReactAppDependencyProvider`).

Generated code never warns: the codegen package always defines `RN_ALLOW_FRAMEWORKS`, because nobody can act on a warning in generated code and libraries are held to the API in their own builds.

Under SwiftPM every library builds from its own `Package.swift` (shipped upstream or created with `spm scaffold`), so a library declares `RN_ALLOW_FRAMEWORKS` there, and the app's opt-ins do not reach libraries. React Native itself ships prebuilt, so none of its code is compiled with these macros.

The Xcode build phase runs `spm sync` without the shell environment, so `add`/`update` record the chosen macros in `.spm-injected.json` and every later sync regenerates the packages from that record. As with `pod install`, each `add`/`update` re-reads the variables: running `update` without one turns its macro off, and `deinit` removes the app-target defines along with the rest of the injection.

The SwiftPM guide (`scripts/spm/__docs__/spm-scripts.md`) documents the opt-ins: what they define, that libraries are only checked when they define the macros in their own `Package.swift`, and how the choice is remembered for Xcode builds.

The opt-ins are one list (`CXX_API_DEFINES` in `spm-utils.js`) passed through as macro names, so adding another opt-in is a one-line change.

Changelog:
[iOS][Added] - Add `RN_STRICT_API` and `RN_ALLOW_FRAMEWORKS` opt-ins for the Swift Package Manager setup to enforce React Native's C++ stable API in the app's native code

Differential Revision: D124985657
coado added a commit to coado/react-native that referenced this pull request Oct 19, 2026
…ap (react#59068)

Summary:
Apps that use React Native through Swift Package Manager and define `RN_STRICT_API` (the C++ stable API opt-in, see react#59043) cannot build `import React`.

The prebuilt `ReactNativeHeaders.xcframework` ships a module map with one module per namespace. The prebuild makes every header that reaches no C++ a member of that module, and the `react/cxxstableapi` guards qualify because they are pure preprocessor code. So `ReactNativeHeaders_react` lists `FrameworksGuard.h`, `PrivateGuard.h` and `UmbrellaGuard.h` themselves, plus `react/debug/flags.h` and the `JSRuntimeFactory` headers that include them.

Clang compiles module members on their own, using the importing target's `-D` macros, with neither `RN_BUILDING` nor `RN_UMBRELLA_CONTEXT` set. With `RN_STRICT_API` defined, `PrivateGuard.h` and `UmbrellaGuard.h` hit their `#error` while building the module. `import React` needs that module (`RCTHermesInstanceFactory.h` imports `<react/runtime/JSRuntimeFactoryCAPI.h>`), so every Swift file importing React, and every Objective-C++ file importing `<React/...>` with modules on, fails.

This change declares the guards, and every module member that reaches one through unguarded includes, as `textual header` (new rule R13 in `headers-spec.js`). They still belong to the module, so `React.framework` may keep including them without a non-modular include error, but they are expanded at each include site, where the guard sees its real includer. On `main` this covers 8 headers across `ReactNativeHeaders_react`, `ReactCommon` and `reactperflogger`.

Other changes:
- `headers-verify.js` runs the Swift `import React` gate a second time with `RN_STRICT_API` defined. Its module import fixture now picks a non-textual member of each module.
- The R5 comment said `react/` gets no module. It does, named `ReactNativeHeaders_react`.

## Changelog:
[Internal]


Test Plan:
Prebuild end to end (Debug, iOS simulator slice), from `packages/react-native`:

```
RN_DEP_VERSION=nightly node scripts/ios-prebuild.js -s -f Debug
node scripts/ios-prebuild.js -b -f Debug -p ios-simulator
node scripts/ios-prebuild.js -c -f Debug -p ios-simulator
node scripts/ios-prebuild/headers-verify.js --flavor Debug
```

- The composed module map has the 8 `textual header` entries, and `headers-verify.js` reports `ALL GATES PASSED`, including the new strict gate.

RNTester through SwiftPM, with react#59043 applied and the artifact above packaged as CI does (`ReactCoreDebug.xcframework.tar.gz`):

```
RN_CORE_TARBALL_PATH=/path/to/ReactCoreDebug.xcframework.tar.gz npx react-native spm download --artifacts /tmp/spm-artifacts
npx react-native spm scaffold --artifacts /tmp/spm-artifacts --download skip || true
RN_STRICT_API=1 npx react-native spm add --deintegrate --artifacts /tmp/spm-artifacts --download skip
xcodebuild -project RNTesterPods.xcodeproj -scheme RNTester -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
```

| Scenario | Before | After |
| --- | --- | --- |
| `RN_STRICT_API` on | Fails: `ReactNativeHeaders_react` won't precompile, Swift target fails | Build succeeds, app links |
| `RN_STRICT_API` on, private include in app code | — | error |
| `RN_STRICT_API` on, direct public include | — | error |
| `RN_STRICT_API` on, "for frameworks" include | — | warning |
| `RN_STRICT_API` on, umbrella include | — | builds |
| `RN_STRICT_API` off (`spm update` without it) | Builds | Builds, no guard diagnostics |

The "Before" row was built against the published nightly. Swift with C++ interop also builds with `RN_STRICT_API` once `main`'s `RCTCallInvoker.h` (which now includes `<React/CallInvoker.h>`) is in the artifact.

Not run: Release flavor, device and Mac Catalyst slices. React Native's dependencies and Hermes came from the nightly rather than being built locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


This was also tested locally on rn-tester.

Differential Revision: D125444181

Pulled By: coado
…EWORKS (react#59043)

Summary:

Let iOS apps that use the Swift Package Manager setup enforce React Native's C++ stable API, matching the CocoaPods and Android opt-ins. Two env vars are read by `npx react-native spm add` / `update`:

- `RN_STRICT_API=1` defines the `RN_STRICT_API` macro, which arms the `react/cxxstableapi` guards.
- `RN_ALLOW_FRAMEWORKS=1` defines the `RN_ALLOW_FRAMEWORKS` macro, which silences their "for frameworks" warning.

Each variable is named after the macro it defines, as in the CocoaPods opt-in.

Each opted-in macro is defined for the app's own code:
- the app target (`GCC_PREPROCESSOR_DEFINITIONS`, seeded with `$(inherited)`),
- the packages the autolinker generates for the app's local `spm.modules`,
- the app codegen package (`ReactCodegen`, `ReactAppDependencyProvider`).

Generated code never warns: the codegen package always defines `RN_ALLOW_FRAMEWORKS`, because nobody can act on a warning in generated code and libraries are held to the API in their own builds.

Under SwiftPM every library builds from its own `Package.swift`, so a library declares `RN_ALLOW_FRAMEWORKS` there, and the app's opt-ins do not reach libraries. React Native itself ships prebuilt, so none of its code is compiled with these macros.

The Xcode build phase runs `spm sync` without the shell environment, so `add`/`update` record the chosen macros in `.spm-injected.json` and every later sync regenerates the packages from that record. As with `pod install`, each `add`/`update` re-reads the variables: running `update` without one turns its macro off, and `deinit` removes the app-target defines along with the rest of the injection.


Changelog:
[iOS][Added] - Add `RN_STRICT_API` and `RN_ALLOW_FRAMEWORKS` opt-ins for the Swift Package Manager setup to enforce React Native's C++ stable API in the app's native code

Reviewed By: cipolleschi

Differential Revision: D124985657
@coado
coado force-pushed the export-D124985657 branch from bc5bc92 to c457fd8 Compare October 19, 2026 15:29
@meta-codesync meta-codesync Bot closed this in 47f744d Oct 19, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Oct 19, 2026
@meta-codesync

meta-codesync Bot commented Oct 19, 2026

Copy link
Copy Markdown

This pull request has been merged in 47f744d.

@react-native-bot

Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @coado in 47f744d

When will my fix make it into a release? | How to file a pick request?

cipolleschi pushed a commit that referenced this pull request Oct 26, 2026
…t.pbxproj (#58834)

Port of 6a2c7c2 (#58834) to 0.88-stable. This is not a plain cherry-pick: the commit on main also moves the C++ stable API opt-ins from #59043 into the generated xcconfig, and #59043 is not on 0.88-stable. This port is the reviewed version of #58834 from before it was rebased onto #59043. Everything else is the same as the commit on main.

Summary:
`npx react-native spm add` and `spm update` edit the app's `project.pbxproj`. Today they write React Native's build settings and the full text of two build scripts into that file. Some of these values change with every React Native version. So a routine `spm update` gives a `project.pbxproj` diff, and teams get merge conflicts in a file that is hard to read.

This PR moves those values out of `project.pbxproj` into files that React Native generates next to it:

- `ReactNativeSPM/ReactNativeSPM.xcconfig` holds the build settings. Each app build configuration uses it as its base configuration.
- `ReactNativeSPM/Scripts/sync-autolinking.sh` and `ReactNativeSPM/Scripts/embed-flavored-frameworks.sh` hold the script bodies. The build phases in `project.pbxproj` only run these files.

The result: when a version bump does not change the set of packages or plugin frameworks, `spm update` gives no `project.pbxproj` diff. The changes go to the generated files.

Commit the `ReactNativeSPM/` directory with the project. Xcode reads these files before the build starts, so they must exist on a fresh clone. They are not in the ignored `build/` directory for that reason.

`project.pbxproj` still holds the package references, the product dependencies, and the build phases. Xcode has no other place for them.

`REACT_NATIVE_PATH` also changes. It is now `$(SRCROOT)/<path>` through `node_modules/react-native`. Before, with pnpm, it could be the resolved `.pnpm/react-native@<version>_<hash>` path. That path is specific to one machine, and it was committed.

### Limits

- The "no diff" result applies only when packages are referenced by local path. In remote mode, the package version in `project.pbxproj` still changes on each bump.
- When you add or remove a plugin framework, the input and output paths of the embed phase in `project.pbxproj` still change. Xcode requires these paths in the project file.
- `spm add` and `spm update` now refuse a target in two new cases, with a message that tells you what to do:
  - A configuration already uses a different xcconfig file as its base, and that file does not `#include` the generated one. React Native does not edit xcconfig files that it does not own. The message gives the exact `#include` line to add to your file. After you add it, `spm` accepts the configuration and leaves its base unchanged.
  - The target sets a list setting that the generated file also sets, without `$(inherited)`. The target value would hide the React Native value. The message names the settings to fix.
- Configuration names that contain spaces or `]` are not escaped in the `[config=<name>]` conditions.
- A configuration that you add in Xcode after `spm update` (for example, a copy of Release named "Staging") gets the release flavor until the next `spm update` adds its own condition.

### Existing apps

No manual migration is necessary. On the next `spm update`, React Native removes the build settings that it wrote before (it records them in its marker file), then writes the xcconfig file.

### Details for reviewers

- `generate-spm-xcodeproj.js`:
  - `generateReactNativeXcconfig()` writes the settings. Flavor-specific settings (`RN_SPM_FLAVOR`, `SWIFT_ACTIVE_COMPILATION_CONDITIONS`) use Xcode's `[config=<name>]` conditions, with the real configuration names from the project. One file serves all configurations.
  - `mergeReactBuildSettings()` is removed.
  - An unconditioned `RN_SPM_FLAVOR = release` comes before the `[config=<name>]` lines, as the fallback for configurations that the file does not list.
  - `generatedScriptWrapper()` writes the stable build-phase text. If the script file is missing, the build fails with a message to run `npx react-native spm update`. The embed script is an input path of its phase, so Xcode runs the phase again when the script changes.
  - `planInjection()` follows the `#include` chain of a configuration's base xcconfig. Each path resolves from the folder of the file that contains it, and a cycle stops the walk. The configurations that include the generated file are recorded in the marker as `xcconfig.includedByConfigUuids`.
  - The CocoaPods template sets `REACT_NATIVE_PATH` relative to `${PODS_ROOT}`. A target value outranks an xcconfig value, so injection removes that one value. Deinit does not put it back, because the value resolves to an empty path when CocoaPods is not used.
  - `linkedReactNativeRoot()` finds the `node_modules/react-native` link that resolves to the real React Native directory, to build the stable path.
  - `injectSpmIntoPbxproj()` now takes `{fromAppRoot, fromSrcRoot}` as its third argument.
  - Deinit removes the base-configuration references, the file reference, and the generated files. If a configuration uses a different xcconfig that may include the generated one, deinit keeps the generated file and logs why.
- `xcconfig-files.js` (new): xcconfig path resolution and `#include` parsing, moved out of `ios-deployment-target.js` so both files share it. `ios-deployment-target.js` behaves as before.
- `spm-pbxproj.js`: `ensureScalarField()` takes an `{after}` option, to put `baseConfigurationReference` at the correct position.

## Changelog:

[IOS] [CHANGED] - SwiftPM: `spm add` and `spm update` write React Native's build settings to `ReactNativeSPM/ReactNativeSPM.xcconfig` and its build scripts to `ReactNativeSPM/Scripts/`, so a version bump no longer changes `project.pbxproj`. A configuration that uses its own xcconfig must now `#include` the generated file, and a target setting that hides React Native's value must add `$(inherited)`. `spm` prints the exact fix in both cases.

Pull Request resolved: #58834

Test Plan:
```
yarn jest --no-cache -i packages/react-native/scripts/spm
```

Result on this 0.88-stable port branch: 22 suites and 1099 tests pass (0.88-stable before the 5 ports: 21 suites, 1005 tests). The new tests failed before the change.

New tests:

- `inject-spm-xcodeproj-test.js`: a version bump (new React Native path and new framework slice data) gives a byte-identical `project.pbxproj`. Other tests check that each configuration uses the generated xcconfig, that a `${PODS_ROOT}`-based `REACT_NATIVE_PATH` is removed, that a user xcconfig is accepted when its `#include` chain reaches the generated file and refused otherwise, that the refusal gives the exact `#include` line, and that the embed script is an input of its phase.
- `remove-spm-injection-test.js`: a list setting without `$(inherited)` is refused. Settings from an older injection move into the xcconfig on the next update. Deinit removes the xcconfig reference and the generated files, keeps a base configuration that the user changed, and keeps the generated xcconfig while a user xcconfig may include it. A user xcconfig that includes the generated one survives add → sync → deinit unchanged. pnpm layouts give `$(SRCROOT)/…/node_modules/react-native`. A nearer `node_modules/react-native` that is a different install is skipped.
- `generate-spm-xcodeproj-test.js` and `spm-pbxproj-test.js`: the xcconfig content and the `{after}` option.

Also run:

- `flow focus-check` (flow-bin 0.331.0, as 0.88-stable pins) on the changed files: no errors in them.
- `prettier --check` and `eslint --max-warnings 0` on the changed files: no problems.
- CI on this PR: `test_ios_spm_helloworld`, `test_ios_spm_newapp` and `test_ios_spm_rntester` (Debug and Release) run `spm add` and a full `xcodebuild` with the generated xcconfig. Not run locally.

Not tested:

- An Xcode project in a subdirectory of the app root (`--xcodeproj`). The generated files are written relative to the app root. This case was also not supported before this change.

Ported from commit 6a2c7c2, without the #59043 changes.

(cherry picked from commit 2715043)
meta-codesync Bot pushed a commit that referenced this pull request Oct 27, 2026
…ap (#59068)

Summary:
Apps that use React Native through Swift Package Manager and define `RN_STRICT_API` (the C++ stable API opt-in, see #59043) cannot build `import React`.

The prebuilt `ReactNativeHeaders.xcframework` ships a module map with one module per namespace. The prebuild makes every header that reaches no C++ a member of that module, and the `react/cxxstableapi` guards qualify because they are pure preprocessor code. So `ReactNativeHeaders_react` lists `FrameworksGuard.h`, `PrivateGuard.h` and `UmbrellaGuard.h` themselves, plus `react/debug/flags.h` and the `JSRuntimeFactory` headers that include them.

Clang compiles module members on their own, using the importing target's `-D` macros, with neither `RN_BUILDING` nor `RN_UMBRELLA_CONTEXT` set. With `RN_STRICT_API` defined, `PrivateGuard.h` and `UmbrellaGuard.h` hit their `#error` while building the module. `import React` needs that module (`RCTHermesInstanceFactory.h` imports `<react/runtime/JSRuntimeFactoryCAPI.h>`), so every Swift file importing React, and every Objective-C++ file importing `<React/...>` with modules on, fails.

This change declares the guards, and every module member that reaches one through unguarded includes, as `textual header` (new rule R13 in `headers-spec.js`). They still belong to the module, so `React.framework` may keep including them without a non-modular include error, but they are expanded at each include site, where the guard sees its real includer. On `main` this covers 8 headers across `ReactNativeHeaders_react`, `ReactCommon` and `reactperflogger`.

Other changes:
- `headers-verify.js` runs the Swift `import React` gate a second time with `RN_STRICT_API` defined. Its module import fixture now picks a non-textual member of each module.
- The R5 comment said `react/` gets no module. It does, named `ReactNativeHeaders_react`.

## Changelog:
[Internal]

Pull Request resolved: #59068

Test Plan:
Prebuild end to end (Debug, iOS simulator slice), from `packages/react-native`:

```
RN_DEP_VERSION=nightly node scripts/ios-prebuild.js -s -f Debug
node scripts/ios-prebuild.js -b -f Debug -p ios-simulator
node scripts/ios-prebuild.js -c -f Debug -p ios-simulator
node scripts/ios-prebuild/headers-verify.js --flavor Debug
```

- The composed module map has the 8 `textual header` entries, and `headers-verify.js` reports `ALL GATES PASSED`, including the new strict gate.

RNTester through SwiftPM, with #59043 applied and the artifact above packaged as CI does (`ReactCoreDebug.xcframework.tar.gz`):

```
RN_CORE_TARBALL_PATH=/path/to/ReactCoreDebug.xcframework.tar.gz npx react-native spm download --artifacts /tmp/spm-artifacts
npx react-native spm scaffold --artifacts /tmp/spm-artifacts --download skip || true
RN_STRICT_API=1 npx react-native spm add --deintegrate --artifacts /tmp/spm-artifacts --download skip
xcodebuild -project RNTesterPods.xcodeproj -scheme RNTester -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
```

| Scenario | Before | After |
| --- | --- | --- |
| `RN_STRICT_API` on | Fails: `ReactNativeHeaders_react` won't precompile, Swift target fails | Build succeeds, app links |
| `RN_STRICT_API` on, private include in app code | — | error |
| `RN_STRICT_API` on, direct public include | — | error |
| `RN_STRICT_API` on, "for frameworks" include | — | warning |
| `RN_STRICT_API` on, umbrella include | — | builds |
| `RN_STRICT_API` off (`spm update` without it) | Builds | Builds, no guard diagnostics |

The "Before" row was built against the published nightly. Swift with C++ interop also builds with `RN_STRICT_API` once `main`'s `RCTCallInvoker.h` (which now includes `<React/CallInvoker.h>`) is in the artifact.

Not run: Release flavor, device and Mac Catalyst slices. React Native's dependencies and Hermes came from the nightly rather than being built locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

This was also tested locally on rn-tester.

Reviewed By: cipolleschi

Differential Revision: D125444181

Pulled By: coado

fbshipit-source-id: 4df7dd94d13a6ed65f6c2fef3474170e13096503
tvinhas pushed a commit to tvinhas/react-native-macos that referenced this pull request Oct 28, 2026
…t.pbxproj (react#58834)

Port of 6a2c7c2 (react#58834) to 0.88-stable. This is not a plain cherry-pick: the commit on main also moves the C++ stable API opt-ins from react#59043 into the generated xcconfig, and react#59043 is not on 0.88-stable. This port is the reviewed version of react#58834 from before it was rebased onto react#59043. Everything else is the same as the commit on main.

Summary:
`npx react-native spm add` and `spm update` edit the app's `project.pbxproj`. Today they write React Native's build settings and the full text of two build scripts into that file. Some of these values change with every React Native version. So a routine `spm update` gives a `project.pbxproj` diff, and teams get merge conflicts in a file that is hard to read.

This PR moves those values out of `project.pbxproj` into files that React Native generates next to it:

- `ReactNativeSPM/ReactNativeSPM.xcconfig` holds the build settings. Each app build configuration uses it as its base configuration.
- `ReactNativeSPM/Scripts/sync-autolinking.sh` and `ReactNativeSPM/Scripts/embed-flavored-frameworks.sh` hold the script bodies. The build phases in `project.pbxproj` only run these files.

The result: when a version bump does not change the set of packages or plugin frameworks, `spm update` gives no `project.pbxproj` diff. The changes go to the generated files.

Commit the `ReactNativeSPM/` directory with the project. Xcode reads these files before the build starts, so they must exist on a fresh clone. They are not in the ignored `build/` directory for that reason.

`project.pbxproj` still holds the package references, the product dependencies, and the build phases. Xcode has no other place for them.

`REACT_NATIVE_PATH` also changes. It is now `$(SRCROOT)/<path>` through `node_modules/react-native`. Before, with pnpm, it could be the resolved `.pnpm/react-native@<version>_<hash>` path. That path is specific to one machine, and it was committed.

### Limits

- The "no diff" result applies only when packages are referenced by local path. In remote mode, the package version in `project.pbxproj` still changes on each bump.
- When you add or remove a plugin framework, the input and output paths of the embed phase in `project.pbxproj` still change. Xcode requires these paths in the project file.
- `spm add` and `spm update` now refuse a target in two new cases, with a message that tells you what to do:
  - A configuration already uses a different xcconfig file as its base, and that file does not `#include` the generated one. React Native does not edit xcconfig files that it does not own. The message gives the exact `#include` line to add to your file. After you add it, `spm` accepts the configuration and leaves its base unchanged.
  - The target sets a list setting that the generated file also sets, without `$(inherited)`. The target value would hide the React Native value. The message names the settings to fix.
- Configuration names that contain spaces or `]` are not escaped in the `[config=<name>]` conditions.
- A configuration that you add in Xcode after `spm update` (for example, a copy of Release named "Staging") gets the release flavor until the next `spm update` adds its own condition.

### Existing apps

No manual migration is necessary. On the next `spm update`, React Native removes the build settings that it wrote before (it records them in its marker file), then writes the xcconfig file.

### Details for reviewers

- `generate-spm-xcodeproj.js`:
  - `generateReactNativeXcconfig()` writes the settings. Flavor-specific settings (`RN_SPM_FLAVOR`, `SWIFT_ACTIVE_COMPILATION_CONDITIONS`) use Xcode's `[config=<name>]` conditions, with the real configuration names from the project. One file serves all configurations.
  - `mergeReactBuildSettings()` is removed.
  - An unconditioned `RN_SPM_FLAVOR = release` comes before the `[config=<name>]` lines, as the fallback for configurations that the file does not list.
  - `generatedScriptWrapper()` writes the stable build-phase text. If the script file is missing, the build fails with a message to run `npx react-native spm update`. The embed script is an input path of its phase, so Xcode runs the phase again when the script changes.
  - `planInjection()` follows the `#include` chain of a configuration's base xcconfig. Each path resolves from the folder of the file that contains it, and a cycle stops the walk. The configurations that include the generated file are recorded in the marker as `xcconfig.includedByConfigUuids`.
  - The CocoaPods template sets `REACT_NATIVE_PATH` relative to `${PODS_ROOT}`. A target value outranks an xcconfig value, so injection removes that one value. Deinit does not put it back, because the value resolves to an empty path when CocoaPods is not used.
  - `linkedReactNativeRoot()` finds the `node_modules/react-native` link that resolves to the real React Native directory, to build the stable path.
  - `injectSpmIntoPbxproj()` now takes `{fromAppRoot, fromSrcRoot}` as its third argument.
  - Deinit removes the base-configuration references, the file reference, and the generated files. If a configuration uses a different xcconfig that may include the generated one, deinit keeps the generated file and logs why.
- `xcconfig-files.js` (new): xcconfig path resolution and `#include` parsing, moved out of `ios-deployment-target.js` so both files share it. `ios-deployment-target.js` behaves as before.
- `spm-pbxproj.js`: `ensureScalarField()` takes an `{after}` option, to put `baseConfigurationReference` at the correct position.

## Changelog:

[IOS] [CHANGED] - SwiftPM: `spm add` and `spm update` write React Native's build settings to `ReactNativeSPM/ReactNativeSPM.xcconfig` and its build scripts to `ReactNativeSPM/Scripts/`, so a version bump no longer changes `project.pbxproj`. A configuration that uses its own xcconfig must now `#include` the generated file, and a target setting that hides React Native's value must add `$(inherited)`. `spm` prints the exact fix in both cases.

Pull Request resolved: react#58834

Test Plan:
```
yarn jest --no-cache -i packages/react-native/scripts/spm
```

Result on this 0.88-stable port branch: 22 suites and 1099 tests pass (0.88-stable before the 5 ports: 21 suites, 1005 tests). The new tests failed before the change.

New tests:

- `inject-spm-xcodeproj-test.js`: a version bump (new React Native path and new framework slice data) gives a byte-identical `project.pbxproj`. Other tests check that each configuration uses the generated xcconfig, that a `${PODS_ROOT}`-based `REACT_NATIVE_PATH` is removed, that a user xcconfig is accepted when its `#include` chain reaches the generated file and refused otherwise, that the refusal gives the exact `#include` line, and that the embed script is an input of its phase.
- `remove-spm-injection-test.js`: a list setting without `$(inherited)` is refused. Settings from an older injection move into the xcconfig on the next update. Deinit removes the xcconfig reference and the generated files, keeps a base configuration that the user changed, and keeps the generated xcconfig while a user xcconfig may include it. A user xcconfig that includes the generated one survives add → sync → deinit unchanged. pnpm layouts give `$(SRCROOT)/…/node_modules/react-native`. A nearer `node_modules/react-native` that is a different install is skipped.
- `generate-spm-xcodeproj-test.js` and `spm-pbxproj-test.js`: the xcconfig content and the `{after}` option.

Also run:

- `flow focus-check` (flow-bin 0.331.0, as 0.88-stable pins) on the changed files: no errors in them.
- `prettier --check` and `eslint --max-warnings 0` on the changed files: no problems.
- CI on this PR: `test_ios_spm_helloworld`, `test_ios_spm_newapp` and `test_ios_spm_rntester` (Debug and Release) run `spm add` and a full `xcodebuild` with the generated xcconfig. Not run locally.

Not tested:

- An Xcode project in a subdirectory of the app root (`--xcodeproj`). The generated files are written relative to the app root. This case was also not supported before this change.

Ported from commit 6a2c7c2, without the react#59043 changes.

(cherry picked from commit 2715043)
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. Merged This PR has been merged. meta-exported p: Facebook Partner: Facebook p: Software Mansion Partner: Software Mansion Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants