Skip to content

Let spm deinit take an app back to CocoaPods - #59368

Open
chrfalch wants to merge 3 commits into
mainfrom
chrfalch/spm-cocoapods-opt-out
Open

chrfalch wants to merge 3 commits into
mainfrom
chrfalch/spm-cocoapods-opt-out

Conversation

@chrfalch

@chrfalch chrfalch commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary:

SwiftPM is planned to become the default iOS setup, with CocoaPods as the opt-out. An app that wants CocoaPods then runs npx react-native spm deinit. This PR does not change the templates or the CLI. It makes spm deinit able to take an app fully back to CocoaPods, which the plan needs.

Today an app that was converted to SwiftPM cannot fully go back to CocoaPods:

  • The conversion (spm add --deintegrate, also run by plain npx react-native spm on a new app) deletes each Podfile line that names a React Native call. A call that spans several lines, such as use_react_native!(…), loses only its first line. The Podfile is then invalid Ruby.
  • spm deinit removes SwiftPM from the Xcode project, but it does not touch the Podfile.

This PR keeps the Podfile unchanged, so npx react-native spm deinit and then pod install go back to CocoaPods:

  1. The conversion keeps the Podfile. spm add --deintegrate runs pod deintegrate and does not change the Podfile. The line editing is removed.
  2. Plain npx react-native spm no longer converts a CocoaPods app. It stops before it changes anything and points to spm add --deintegrate.
  3. pod install stops on an app that uses SwiftPM. The React Native CocoaPods scripts check for the SwiftPM marker file (.spm-injected.json) in the .xcodeproj next to the Podfile. If it is there, pod install stops with an error before it writes any file. Without this check, pod install would add CocoaPods back to the Xcode project and rewrite the generated Package.swift with a wrong path, and Xcode would fail to resolve packages.
  4. The uncommitted-changes prompt comes first. If the .xcodeproj has uncommitted changes, the conversion asks before pod deintegrate. If the answer is no, nothing changes.
  5. The conversion warns about other pods. If the Podfile declares pods that are not part of React Native, it lists them and continues. Pods that come from the react-native package (:path or :podspec into node_modules/react-native) are not listed. The Podfile is kept, so spm deinit and then pod install bring those pods back.
  6. spm deinit undoes the .gitignore lines the conversion wrote. The conversion writes .gitignore only after a successful injection and records the lines in the marker file, so deinit removes only them.
  7. spm deinit removes the ReactNativeSPM folders that are empty after it deletes its files, and prints "To go back to CocoaPods, run pod install."

The three SwiftPM iOS workflows checked that the Podfile has no use_react_native! after the conversion. The Podfile is now kept, so they check that the .xcodeproj has no Pods- references instead.

The docs in scripts/spm/__docs__ describe the way back and the pod install error. They also say that the conversion removes the app target's own REACT_NATIVE_PATH, and deinit does not restore it. pod install sets it again through react_native_post_install. Only a Podfile without that call needs a manual check.

Changelog:

[IOS] [CHANGED] - react-native spm add --deintegrate keeps the Podfile, plain react-native spm no longer converts a CocoaPods app, and pod install stops with an error on an app that uses SwiftPM. react-native spm deinit and then pod install take an app back to CocoaPods.

Test Plan:

Unit tests:

yarn jest packages/react-native/scripts/spm/__tests__

25 suites, 1233 tests pass. ESLint and Prettier are clean on the changed files, and ruby -c passes on the three changed Ruby files.

The Ruby scripts have no unit tests in this repo. I checked the pod install error on a real app instead (step 3 below).

A new app outside the monorepo, on React Native 0.89.0-nightly-20261021-c7429b0e9 with @react-native-community/cli 21.0.0-alpha.1. The changed scripts were copied into node_modules/react-native/scripts.

  1. npx react-native spm add --deintegrate --yes converts the app. The Podfile is unchanged.
  2. The SwiftPM build succeeds.
  3. pod install stops: "This app uses Swift Package Manager for React Native (SpmOptOut.xcodeproj has .spm-injected.json), so pod install would break the Xcode project." No Pods/ folder is created. Podfile.lock, Package.swift and the git status of ios/ are unchanged.
  4. The SwiftPM build succeeds again.
  5. npx react-native run-ios and npx react-native build-ios build and run the app. The CLI sees the marker file and does not run pod install.
  6. npx react-native spm deinit removes the marker. The Podfile is unchanged.
  7. cd ios && pod install succeeds, and the CocoaPods build succeeds.

Not checked:

  • The changed workflow checks have not run in CI yet.
  • Flow. My local Flow binary is older than the version .flowconfig requires, so it did not type-check.
  • run-ios --force-pods on a converted app. In the CLI source, this flag runs pod install anyway, which then stops with the error from step 3.
  • CLI 20.
  • After the conversion, the old .xcworkspace stays (as before this PR). The CLI builds it, and the build succeeds.

@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 22, 2026
@chrfalch
chrfalch requested a review from cipolleschi October 22, 2026 16:31
@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 Oct 22, 2026
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Visual regression

1 visual difference(s) at dda80a2cf86c. Visual differences need maintainer approval. Review the report, then apply visual-change-approved.

ios view/z-index/reordered: Changed, 15396 px (2.160%)

diff

Expected and Captured
Expected Captured
expected captured

Converting an app to SwiftPM with `--deintegrate` now moves the Podfile
to Podfile.bak instead of deleting React Native lines from it (that edit
left invalid Ruby for multi-line calls). `spm deinit` restores the
Podfile, undoes the .gitignore lines the conversion wrote, removes empty
ReactNativeSPM folders, and says to run `pod install`.

The conversion refuses, before changing anything, when the Podfile has
pods that are not React Native or when Podfile.bak already exists.
The Podfile is moved to Podfile.bak, so no pod is lost and `spm deinit`
restores it. An explicit `--deintegrate` now lists the other pods, says
CocoaPods will not install them while the app uses SwiftPM unless SwiftPM
provides them, and continues. RNTester needs this: its Podfile has local
example pods that SwiftPM provides.

The automatic conversion of a new app still skips such Podfiles.
@chrfalch
chrfalch force-pushed the chrfalch/spm-cocoapods-opt-out branch from b75a2b8 to dda80a2 Compare October 28, 2026 14:06

@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.

Thanks for this!
I'd changes some bits, to simplify the feature... See my comments!

```

On a fresh CocoaPods app, this converts the app to SwiftPM. It runs
`pod deintegrate` and moves the `Podfile` to `Podfile.bak`. A Podfile with pods

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.

Podfile can remain Podfile. Why do we need to change it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. The Podfile now stays as it is.

`pod deintegrate` and moves the `Podfile` to `Podfile.bak`. A Podfile with pods
that are not part of React Native is not converted automatically;
`spm add --deintegrate` converts it and warns that those pods are not installed.
Without a Podfile, the CLI needs `@react-native-community/cli` 21 or newer to

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.

if we keep it, we can lift this requirement

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. The CLI 21 requirement is gone, because the Podfile stays in place.

Comment on lines +46 to +48
2. moves the `Podfile` to `Podfile.bak`, unchanged. `spm deinit` moves it back.
If `Podfile.bak` already exists, the command stops before it changes
anything.

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.

I would not do this step.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. The step is removed.

Non-RN pods can stay side-by-side. Your Podfile still lists them. Remove the
leftover React Native lines (see step 2 above), then re-integrate the pods with
a normal install:
`--deintegrate` converts an app whose Podfile has pods that are not part of

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.

it might be confusing that we have two different behaviors for spm --deintegrate and spm.
I understand that we would like to remove a step (manually calling --deintegrate) but perhaps it is not a bad thing that the user has to call it themselves.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Plain spm no longer runs --deintegrate. On a CocoaPods app it stops and points to spm add --deintegrate.

[local native module](#local-native-modules). The pods stay in `Podfile.bak`,
and `spm deinit` restores the Podfile.

To keep a CocoaPods-only pod next to SwiftPM, convert by hand:

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.

are there Cocoapods-only pods? I believe that most Native iOS libraries nowadays supports Swift PM. For React Native packages, we should strive to auto-convert them or to fail. If the users have a pod that still needs to be converted to SwiftPM and that can't be converted automatically, I would probably fail and ask them to open a PR/Issue to convert the library.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I kept this a warning, as in my top-level comment (point 4). The Podfile now stays unchanged, so spm deinit and then pod install bring those pods back. An error would also block RNTester: its Podfile has local pods that SwiftPM provides through spmModules.

return true;
}

function restorePodfile(appRoot /*: string */) /*: void */ {

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.

The move isn't recorded in the marker (unlike the .gitignore lines), so deinit renames any Podfile.bak to Podfile, including one the user made themselves. This goes away if we keep the Podfile in place.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Gone. The Podfile.bak code is removed.

return false;
}
if (podfileHasThirdPartyPods(appRoot)) {
if (podfileHasThirdPartyPods(appRoot) || hasPodfileBackup(appRoot)) {

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.

When Podfile.bak exists, this skips without saying why. The fail-fast check then suggests spm add --deintegrate, which refuses for the same reason. Could we log the reason here?

If we get rid of the Podfile.bak, we can simplify the code significantly

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Gone. The Podfile.bak code is removed.

function podfileHasThirdPartyPods(appRoot /*: string */) /*: boolean */ {
// The names of the pods the Podfile declares explicitly (`pod '...'` or
// `pod('...')`), deduplicated, in order. React Native itself comes in through
// use_react_native!, so every name here is a pod that is not part of it.

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.

Older or brownfield Podfiles can list RN pods explicitly (e.g. pod 'React-Core', :path => '../node_modules/react-native/'), so this would report them as non-RN. Pods from test targets also end up in the list.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Pods with :path or :podspec into node_modules/react-native now count as React Native pods. Pods in test targets stay in the list: to find them, we would need to parse the Ruby target blocks.

const podNames = podfilePodNames(appRoot);
if (podNames.length > 0) {
logError(
'Your Podfile declares pods that are not part of React Native: ' +

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.

Nit: this is a warning, but it goes through logError and the run continues. If we decide to fail here instead (see my comment in the docs), this is the place to do it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. It is now printed as a warning.

function runDeintegrate(appRoot /*: string */) /*: void */ {
const podfilePath = path.join(appRoot, 'Podfile');
const backupPath = path.join(appRoot, 'Podfile.bak');
if (refuseToOverwritePodfileBackup(appRoot)) {

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.

Nit: main already runs this check before we get here, so this one never fires on that path. Is it only meant for direct callers?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Gone. This check was part of the Podfile.bak code.

@chrfalch

Copy link
Copy Markdown
Collaborator Author

Thanks for the review, Riccardo! I agree with most of it. My plan:

  1. Keep the Podfile in place. --deintegrate runs pod deintegrate and leaves the Podfile unchanged. All the Podfile.bak code goes, and with it the bugs you found around it and the need for CLI 21. The way back is then spm deinit followed by pod install. To catch a pod install run by mistake, the next spm run or Xcode sync stops with a clear error when CocoaPods is back in the project.
  2. Plain npx react-native spm no longer converts a CocoaPods app. The user, or the CLI, runs spm add --deintegrate explicitly.
  3. Order of steps: the uncommitted-changes prompt moves before any change, and .gitignore is written only after a successful injection.
  4. Other pods: I would like to keep this a warning, not an error. The Podfile is now kept unchanged, so nothing is lost. An error would also block RNTester: its Podfile has local pods that SwiftPM provides through spmModules. I will fix the two problems you found: React Native pods that are listed by name now count as React Native pods, and the message is printed as a warning.
  5. deinit → remove: I would leave the name as it is in this PR. deinit already exists, and other tools call these scripts directly. Happy to do the rename in a separate PR if you want it.

I will push these as new commits.

Review feedback: `spm add --deintegrate` no longer moves the Podfile to
Podfile.bak. It runs `pod deintegrate` and keeps the Podfile unchanged, so
`spm deinit` and then `pod install` go back to CocoaPods.

- Plain `spm` no longer converts a CocoaPods app; it points to
  `spm add --deintegrate`.
- The React Native CocoaPods scripts stop `pod install` before any write when
  the .xcodeproj next to the Podfile has .spm-injected.json. Otherwise
  `pod install` rewrites the generated Package.swift with a wrong path and
  Xcode fails to resolve packages before any build phase runs.
- The uncommitted-changes prompt comes before `pod deintegrate`.
- .gitignore is written only after a successful injection.
- Pods sourced from node_modules/react-native count as React Native pods,
  and the other-pods message is a warning.
- The SwiftPM iOS workflows check for `Pods-` references in the pbxproj
  instead of the Podfile content.
@chrfalch chrfalch changed the title Let spm deinit take an app back to CocoaPods Keep the Podfile so spm deinit can take an app back to CocoaPods Oct 30, 2026
@chrfalch chrfalch changed the title Keep the Podfile so spm deinit can take an app back to CocoaPods Let spm deinit take an app back to CocoaPods Oct 30, 2026
@chrfalch

Copy link
Copy Markdown
Collaborator Author

Pushed the changes in 0640f08. One change from my earlier plan (point 1): a mistaken pod install is now stopped by pod install itself. The React Native CocoaPods scripts see the SwiftPM marker in the .xcodeproj and stop with an error before they write any file. A later check in spm or the Xcode sync phase was too late: pod install rewrites the generated Package.swift, and Xcode fails to resolve packages before any build phase runs. The test plan in the description has the details.

This branch has not been deployed

No deployments
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