feat(ios): add SPM dependency resolution support alongside CocoaPods#8933
feat(ios): add SPM dependency resolution support alongside CocoaPods#8933jsnavarroc wants to merge 5 commits intoinvertase:mainfrom
Conversation
|
@jsnavarroc is attempting to deploy a commit to the Invertase Team on Vercel. A member of the Team first needs to authorize it. |
Add dual SPM/CocoaPods dependency resolution for Firebase iOS SDK. When React Native >= 0.75 is detected, Firebase dependencies are resolved via Swift Package Manager (spm_dependency). For older versions or when explicitly disabled ($RNFirebaseDisableSPM = true), CocoaPods is used. Changes: - Add firebase_spm.rb helper with firebase_dependency() function - Add firebaseSpmUrl to packages/app/package.json (single source of truth) - Update all 16 podspecs to use firebase_dependency() - Add #if __has_include guards in 43 native iOS files for dual imports - Add CI matrix (spm × cocoapods × debug × release) in E2E workflow - Add Ruby unit tests for firebase_spm.rb - Add documentation at docs/ios-spm.md
…dSupport is true When $RNFirebaseAnalyticsWithoutAdIdSupport = true with SPM enabled, FirebaseAnalytics pulls in GoogleAppMeasurement which contains APMETaskManager and APMMeasurement cross-references. These cause linker errors when FirebasePerformance is not installed. Switch to FirebaseAnalyticsCore (-> GoogleAppMeasurementCore) in that case, which has no IDFA and no APM symbols. CocoaPods path is unchanged. docs: add Section 8 with 5 real integration bugs found during tvOS Xcode 26 migration and their solutions
fix(analytics): use FirebaseAnalyticsCore when WithoutAdIdSupport + SPM
Additional fix included:
|
|
Hey @mikehardy — could you approve the workflows to run on this PR when you get a chance? The CI checks are blocked waiting for maintainer approval. Happy to address any feedback once they run. Thanks! |
mikehardy
left a comment
There was a problem hiding this comment.
Wow! This is pretty amazing. Thank you for proposing this - just finished first pass review and while I left comments all over the place I hope none of that gives the feeling that this isn't amazing, and that we won't merge SPM support, it is something this repository obviously needs. So again, thank you
But then of course there are all the comments with some specific questions, some pings out to a firebase-ios-sdk maintainer (hi Paul 👋 ) that I collaborate with on occasion, and some notes to myself regarding testing
| key: ${{ runner.os }}-ios-pods-v3-${{ hashFiles('tests/ios/Podfile.lock') }} | ||
| restore-keys: ${{ runner.os }}-ios-pods-v3 | ||
| key: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }}-${{ hashFiles('tests/ios/Podfile.lock') }} | ||
| restore-keys: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }} |
There was a problem hiding this comment.
I prefer the variable interpolations at the front and all together and the various naming / key "sauce" at the end
| restore-keys: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }} | |
| restore-keys: ${{ runner.os }}-${{ matrix.dep-resolution }}-ios-pods-v3 |
There was a problem hiding this comment.
Done, reordered in the latest commit.
| #import <Firebase/Firebase.h> | ||
| #else | ||
| @import FirebaseCore; | ||
| @import FirebaseAnalytics; |
There was a problem hiding this comment.
Taking note of your comment here on the FirebasePerformance symbols missing at link time if ad ids are disabled does this FirebaseAnalytics import still work here in the FirebaseAnalyticsCore dependency case? #8933 (comment)
There was a problem hiding this comment.
Good question. Yes, @import FirebaseAnalytics still works when the dependency is FirebaseAnalyticsCore. They share the same Clang module and headers, the difference is only at link time: FirebaseAnalyticsCore links GoogleAppMeasurementCore (no IDFA, no APM symbols) while FirebaseAnalytics links GoogleAppMeasurement (includes APMETaskManager, APMMeasurement). The import resolves the same API surface in both cases.
| Pod::UI.puts "#{s.name}: Not installing FirebaseAnalytics/IdentitySupport Pod, no IDFA will be collected." | ||
| # Analytics has conditional dependencies that vary between SPM and CocoaPods. | ||
| # SPM: use FirebaseAnalyticsWithoutAdIdSupport when $RNFirebaseAnalyticsWithoutAdIdSupport = true | ||
| # to avoid GoogleAppMeasurement APM symbols that require FirebaseRemoteConfig (linker error). |
There was a problem hiding this comment.
was it FirebasePerformance symbols or FirebaseRemoteConfig symbols missing at link time? The comment on the PR and the comment here in code and slightly lower in code are inconsistent 🤔
There was a problem hiding this comment.
Fixed in latest commit. It was FirebasePerformance (APM = App Performance Monitoring). The symbols APMETaskManager and APMMeasurement are from Google App Measurement's performance monitoring integration, not RemoteConfig. Comment is now consistent across the podspec.
| s.frameworks = 'AdSupport' | ||
| end | ||
|
|
||
| # GoogleAdsOnDeviceConversion (CocoaPods only, not available in firebase-ios-sdk SPM) |
There was a problem hiding this comment.
Is there some documentation on how to access on-device conversion in the SPM case? This is surprising to me as I thought on-device conversion was one of the newer features of firebase-ios-sdk which creates the expectation in me that it should be available there somehow ? Perhaps just built in to core SPM dep I'm not sure
There was a problem hiding this comment.
GoogleAdsOnDeviceConversion is a static xcframework distributed separately from firebase-ios-sdk. It is NOT available as an SPM product in Package.swift. When using SPM (dynamic linkage), it causes duplicate symbol errors.
In the latest commit I've added:
- A clear comment explaining the limitation
- A runtime warning when the user enables it in SPM mode
- Documentation pointing users to set
$RNFirebaseDisableSPM = trueif they need on-device conversion.
If firebase-ios-sdk adds it as an SPM product in the future, we can remove this restriction.
| #if __has_include(<Firebase/Firebase.h>) | ||
| #import <Firebase/Firebase.h> | ||
| #import <FirebaseAppCheck/FIRAppCheck.h> | ||
| #elif __has_include(<FirebaseAppCheck/FirebaseAppCheck.h>) | ||
| #import <FirebaseAppCheck/FirebaseAppCheck.h> |
There was a problem hiding this comment.
<Firebase/Firebase.h> will always be found in the CocoaPods case won't it? It's the most fundamental header if I understand correctly. Does it transitively include <FirebaseAppCheck/FIRAppCheck.h> such that the explicit import is no longer required then (or maybe was never required?). Surprising this compiles as I think that preprocessor branch is likely the only __has_include branch that will ever be taken, and it makes me think the #elif __has_include(<FirebaseAppCheck/FirebaseAppCheck.h>) branch may not even be needed, I can't see how <Firebase/Firebase.h> won't be found
There was a problem hiding this comment.
You're right that <Firebase/Firebase.h> will always be found in CocoaPods since it's the umbrella header. The #elif branch covers an edge case where someone installs only individual Firebase pods without the umbrella Firebase/Firebase pod. In practice, since RNFB always depends on RNFBApp which brings FirebaseCore, the first branch is effectively always taken in CocoaPods.
The three-branch structure is: CocoaPods umbrella, then CocoaPods individual pods, then SPM (@import). I can simplify to just #if/#else (CocoaPods umbrella vs SPM @import) if you prefer, let me know.
| Add this line at the top of your Podfile (before any `target` block): | ||
|
|
||
| ```ruby | ||
| # Podfile | ||
| $RNFirebaseDisableSPM = true | ||
| ``` | ||
|
|
||
| This forces all RNFB modules to use traditional `s.dependency` CocoaPods declarations. | ||
| You can use either static or dynamic linkage with this option. |
There was a problem hiding this comment.
Similarly - if there is something Expo people need to do to add a Podfile directive we'll need to explain it here or we'll have a lot of issues opened in the repo later about it
There was a problem hiding this comment.
Covered in the Expo section added in latest commit.
|
|
||
| # Read Firebase SPM URL from app package.json (single source of truth) | ||
| $firebase_spm_url ||= begin | ||
| app_package_path = File.join(__dir__, 'package.json') |
There was a problem hiding this comment.
maintainer note to check this as recommended relative/package-local file resolution style (as well as the includes of this file from other packages) for all cases - including monorepos with possible package hoists, pnpm etc
There was a problem hiding this comment.
Added a comment in firebase_spm.rb explaining that __dir__ resolves to packages/app/ and that the ../app/ relative path in other podspecs assumes the standard node_modules/@react-native-firebase/ layout. For monorepos with hoisted dependencies or pnpm strict mode, the require path may need adjustment. Also added a monorepo/pnpm section in the README.
| # Set `$RNFirebaseDisableSPM = true` in your Podfile to force CocoaPods-only | ||
| # dependency resolution. This is required when using `use_frameworks! :linkage => :static` | ||
| # because static frameworks cause each pod to embed Firebase SPM products, | ||
| # resulting in duplicate symbol linker errors. |
There was a problem hiding this comment.
This in This is required... is perhaps ambiguous for something this important
| # Set `$RNFirebaseDisableSPM = true` in your Podfile to force CocoaPods-only | |
| # dependency resolution. This is required when using `use_frameworks! :linkage => :static` | |
| # because static frameworks cause each pod to embed Firebase SPM products, | |
| # resulting in duplicate symbol linker errors. | |
| # Set `$RNFirebaseDisableSPM = true` in your Podfile to force CocoaPods-only | |
| # dependency resolution. You must disable SPM when using `use_frameworks! :linkage => :static` | |
| # because static frameworks cause each pod to embed Firebase SPM products, | |
| # resulting in duplicate symbol linker errors. |
But a further question - the implication is that static frameworks and SPM are simply incompatible (because of duplicate symbols). Is that true? Is there a definitive upstream statement that firebase-ios-sdk, when used via SPM, must always be dynamic frameworks? (vs the Cocoapods path where both are/were supported?)
If there is a definitive statement about this requirement I'd prefer to just state it as a requirement without elaboration and reference the definitive statement via URL for context. If there is no definitive statement then perhaps it could be added in docs upstream via request
There was a problem hiding this comment.
Updated the comment in latest commit. It now reads:
Set
$RNFirebaseDisableSPM = truein your Podfile to force CocoaPods-only dependency resolution. You must disable SPM when usinguse_frameworks! :linkage => :staticbecause static frameworks cause each pod to embed Firebase SPM products, resulting in duplicate symbol linker errors.
Also added a reference to firebase-ios-sdk Package.swift where all products use .library(type: .dynamic), confirming there's no upstream support for SPM + static linkage.
| @@ -1,4 +1,5 @@ | |||
| require 'json' | |||
| require '../app/firebase_spm' | |||
There was a problem hiding this comment.
maintainer note (mirrored in similar note in firebase_spm.rb itself) to verify this is okay in monorepo (possibly hoisted)/pnpm/etc cases - there is likely a known-good inter-package local file resolution mechanism and it is likely not a simple ../<packagename>/ path unfortunately
There was a problem hiding this comment.
Same monorepo/pnpm note applies here. The ../app/ path works with the standard node_modules/@react-native-firebase/ layout. Added documentation in firebase_spm.rb and the README about this assumption.
| @@ -0,0 +1,1135 @@ | |||
| # Documentacion Completa: Soporte Dual SPM + CocoaPods para Firebase en React Native | |||
There was a problem hiding this comment.
Creo que este archivo markdown sea lo mismo como docs/ios_spm.md pero en español y menos obvio para desarrolladores - pienso que debe borrar este ?
There was a problem hiding this comment.
Deleted in latest commit. It was a Spanish duplicate of docs/ios-spm.md.
|
Hey @jsnavarroc 👋 just a gentle ping on this - are you interested in / have time to work on moving this forward? Happy to collaborate, and it's a pretty important feature so I'm very interested personally. Curious for your thoughts |
|
Hey @mikehardy! Yes, absolutely, I'm actively working on this and just pushed a new commit addressing all the review comments. Sorry for the delay. Changes in the latest commit
AskIf you or anyone on the team has bandwidth to test this branch on different devices/platforms (iOS, tvOS, macCatalyst) and with different configurations (SPM vs CocoaPods, dynamic vs static), that would be incredibly valuable. I've verified it on iOS 26 simulator and tvOS 26 (Apple TV) but broader coverage would help catch edge cases before merge. I'll reply to each individual review comment below with details. |
FirebaseInstallations is a transitive dependency of FirebaseCore. With SPM dynamic linking, transitive frameworks are not embedded automatically — they must be declared explicitly as SPM products. Without this, dyld crashes at launch with: 'symbol not found in flat namespace _FIRInstallationIDDidChangeNotification'
… improve SPM comments
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 496d97c. Configure here.
| Pod::UI.puts "#{s.name}: Using FirebaseAnalytics/IdentitySupport with Ad Ids. May require App Tracking Transparency. Not allowed for Kids apps." | ||
| Pod::UI.puts "#{s.name}: You may set variable `$RNFirebaseAnalyticsWithoutAdIdSupport=true` in Podfile to use analytics without ad ids." | ||
| end | ||
| s.dependency 'FirebaseAnalytics/IdentitySupport', firebase_sdk_version |
There was a problem hiding this comment.
IdentitySupport skipped when SPM disabled on RN >= 0.75
High Severity
The unless defined?(spm_dependency) guard on line 65 only checks whether spm_dependency exists, but doesn't account for the $RNFirebaseDisableSPM opt-out flag. When a user has RN >= 0.75 (so spm_dependency is defined) AND sets $RNFirebaseDisableSPM = true to force CocoaPods, this entire block is skipped — silently omitting the FirebaseAnalytics/IdentitySupport CocoaPods dependency. The condition needs to also check !defined?($RNFirebaseDisableSPM) to match the pattern used elsewhere.
Reviewed by Cursor Bugbot for commit 496d97c. Configure here.
| #else | ||
| @import FirebaseCore; | ||
| @import FirebaseInstallations; | ||
| #endif |
There was a problem hiding this comment.
Unconditional import fails in SPM mode
Medium Severity
The #import "FirebaseInstallations/FIRInstallations.h" on line 29 sits outside the new #if __has_include guard added at lines 18–23. In SPM mode, the #else branch uses @import FirebaseInstallations (which already provides all symbols), but this unconditional quoted #import may fail to resolve since SPM doesn't expose the same header paths as CocoaPods. This line needs to be wrapped in a #if __has_include guard or removed.
Reviewed by Cursor Bugbot for commit 496d97c. Configure here.
|
Great to see the progress in this PR! I'm cc'ing @ncooke3, who is now a better contact than me for the firebase-ios-sdk questions in the comments. |
Heads up: tvOS + Xcode 26 hybrid SPM/CocoaPods edge case worth documentingWhile integrating this PR in a tvOS React Native project (hybrid CocoaPods + SPM resolution, Xcode 26, RN 0.77 with Symptom
No formal crash log is generated, Root causeThe combination that triggers it:
The Release + TestFlight pipeline strips Firebase symbols from the main binary's dynamic symbol table because the static analyzer sees them as "unused", nothing in the binary statically references them, RNFB frameworks resolve them at runtime via flat-namespace dynamic lookup. When dyld tries to resolve them on device after TestFlight processing, they're gone. Local Xcode installs don't exhibit this because development builds don't apply the same aggressive stripping. Diagnostic evidenceBefore fix (binary that crashed in TestFlight):
After fix (binary that works in TestFlight):
WorkaroundAnti-stripping settings on the main app target's Release configuration + exporting static symbols to the dynamic symbol table, added via main_target.build_configurations.each do |cfg|
next unless cfg.name == 'Release'
cfg.build_settings['STRIP_SWIFT_SYMBOLS'] = 'NO'
cfg.build_settings['DEAD_CODE_STRIPPING'] = 'NO'
cfg.build_settings['STRIP_INSTALLED_PRODUCT'] = 'NO'
cfg.build_settings['COPY_PHASE_STRIP'] = 'NO'
cfg.build_settings['DEPLOYMENT_POSTPROCESSING'] = 'NO'
ldflags = Array(cfg.build_settings['OTHER_LDFLAGS'] || ['$(inherited)'])
ldflags << '-Wl,-export_dynamic' unless ldflags.include?('-Wl,-export_dynamic')
cfg.build_settings['OTHER_LDFLAGS'] = ldflags
endThis belongs in the integrator's Podfile, not in the library, because it's specific to the hybrid linkage strategy and tvOS, and would be intrusive for iOS projects that don't need it (binary size doubles, Release optimizations disabled). SuggestionSince this is very hard to diagnose without the crashlog (which doesn't get generated) and only manifests after TestFlight upload (long iteration cycle), it might be worth considering one of:
Happy to open a separate PR with either approach if maintainers think it's worth it, fully understand if you'd rather keep this PR focused and tackle it separately later. Flagging it here so it doesn't get lost. Context: reproducible on my setup (Apple TV 4K, tvOS 26, Xcode 26.3, cc @ncooke3 (per @paulb777's earlier comment pointing to you for firebase-ios-sdk questions), sharing in case this stripping interaction is relevant on the Firebase SDK side. |
|
Hi @jsnavarroc, nice testing & investigation! If you didn't consider it, adding the In any case, I'll defer to @mikehardy here, but your suggestions sound reasonable. |


Summary
firebase_dependency()helper (packages/app/firebase_spm.rb)spm_dependency). For older versions or when explicitly disabled ($RNFirebaseDisableSPM = true), CocoaPods is used as fallback.FirebaseCoreInternal,FirebaseSharedSwift) when using CocoaPodsChanges
packages/app/firebase_spm.rb— New helper withfirebase_dependency()function that auto-detects SPM supportpackages/app/package.json— AddedfirebaseSpmUrlfield as single source of truthfirebase_dependency()instead of directs.dependency#if __has_includeguards for dual SPM/CocoaPods importsdep-resolution: ['spm', 'cocoapods']dimension (4 job combinations)firebase_spm.rblogicdocs/ios-spm.mdwith architecture, integration guides, and troubleshootingHow it works
Decision logic:
spm_dependency()defined? (RN >= 0.75 injects it) → YES → use SPM$RNFirebaseDisableSPMset in Podfile? → YES → force CocoaPodsUser-facing configuration
SPM mode (default for RN >= 0.75):
CocoaPods mode (legacy/opt-out):
Xcode 26 workaround (both modes):
Test plan
packages/app/__tests__/firebase_spm_test.rb)$RNFirebaseDisableSPM = truecorrectly forces CocoaPodsNote
Medium Risk
Medium risk because it changes iOS dependency/linkage and native import behavior across many modules, which can break builds in certain Xcode/React Native configurations despite added CI coverage.
Overview
Introduces a centralized
firebase_dependency()helper (packages/app/firebase_spm.rb) that switches RNFirebase iOS Firebase dependencies between SPM (RN >= 0.75 by default) and CocoaPods (fallback or$RNFirebaseDisableSPM), with the SPM repo URL sourced frompackages/app/package.json.Updates all affected RNFirebase module podspecs to use the helper (including special-casing Analytics’ SPM vs CocoaPods behavior), adjusts numerous iOS sources to compile under both header layouts via
__has_include/module imports, and extends Crashlytics symbol upload scripting to find the SPM checkout.Expands iOS E2E CI to run a matrix over
spm/cocoapods×debug/release(including mode-specific Podfile edits and cache keys), adds Ruby unit tests for the helper and runs them in the Jest workflow, and adds new SPM documentation plus a local SPM verification screen.Reviewed by Cursor Bugbot for commit 496d97c. Bugbot is set up for automated code reviews on this repo. Configure here.