Skip to content

feat(ios): add SPM dependency resolution support alongside CocoaPods#8933

Open
jsnavarroc wants to merge 5 commits intoinvertase:mainfrom
jsnavarroc:main
Open

feat(ios): add SPM dependency resolution support alongside CocoaPods#8933
jsnavarroc wants to merge 5 commits intoinvertase:mainfrom
jsnavarroc:main

Conversation

@jsnavarroc
Copy link
Copy Markdown

@jsnavarroc jsnavarroc commented Mar 17, 2026

Summary

  • Add dual SPM/CocoaPods dependency resolution for Firebase iOS SDK via a centralized firebase_dependency() helper (packages/app/firebase_spm.rb)
  • 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 as fallback.
  • Solves Xcode 26 compilation errors caused by explicit modules not finding Firebase internal modules (FirebaseCoreInternal, FirebaseSharedSwift) when using CocoaPods

Changes

  • packages/app/firebase_spm.rb — New helper with firebase_dependency() function that auto-detects SPM support
  • packages/app/package.json — Added firebaseSpmUrl field as single source of truth
  • 16 podspecs — Updated to use firebase_dependency() instead of direct s.dependency
  • 43 native iOS files — Added #if __has_include guards for dual SPM/CocoaPods imports
  • CI matrix — Extended E2E workflow with dep-resolution: ['spm', 'cocoapods'] dimension (4 job combinations)
  • Unit tests — Added Ruby Minitest suite for firebase_spm.rb logic
  • Documentation — Added docs/ios-spm.md with architecture, integration guides, and troubleshooting

How it works

# Each podspec calls:
firebase_dependency(s, version, ['FirebaseAuth'], 'Firebase/Auth')

# Internally decides:
# - SPM path:      spm_dependency(spec, url: ..., products: ['FirebaseAuth'])
# - CocoaPods path: spec.dependency 'Firebase/Auth', version

Decision logic:

  1. Is spm_dependency() defined? (RN >= 0.75 injects it) → YES → use SPM
  2. Is $RNFirebaseDisableSPM set in Podfile? → YES → force CocoaPods
  3. Neither available → fall back to CocoaPods

User-facing configuration

SPM mode (default for RN >= 0.75):

# ios/Podfile
linkage = 'dynamic'
use_frameworks! :linkage => linkage.to_sym

CocoaPods mode (legacy/opt-out):

# ios/Podfile
$RNFirebaseDisableSPM = true
linkage = 'static'
use_frameworks! :linkage => linkage.to_sym

Xcode 26 workaround (both modes):

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO'
    end
  end
end

Test plan

  • Ruby unit tests pass (packages/app/__tests__/firebase_spm_test.rb)
  • CI: Code Quality Checks pass (clang-format, linting)
  • CI: E2E iOS debug + SPM passes
  • CI: E2E iOS debug + CocoaPods passes
  • CI: E2E iOS release + SPM passes
  • CI: E2E iOS release + CocoaPods passes
  • $RNFirebaseDisableSPM = true correctly forces CocoaPods
  • Log messages indicate which resolution mode is active

Note

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 from packages/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.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 17, 2026

CLA assistant check
All committers have signed the CLA.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 17, 2026

@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
jsnavarroc and others added 2 commits March 18, 2026 11:55
…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
@jsnavarroc
Copy link
Copy Markdown
Author

jsnavarroc commented Mar 18, 2026

Additional fix included: FirebaseAnalyticsCore when SPM + $RNFirebaseAnalyticsWithoutAdIdSupport = true

While integrating this PR in a tvOS app (React Native tvOS 0.77, Xcode 26), we encountered a linker error that is only triggered when all three conditions are true simultaneously:

  1. SPM dependency resolution is active (this PR)
  2. $RNFirebaseAnalyticsWithoutAdIdSupport = true in Podfile
  3. FirebasePerformance is not installed

Linker error

Undefined symbols for architecture arm64:
  "_OBJC_CLASS_$_APMETaskManager"
  "_OBJC_CLASS_$_APMMeasurement"

Root cause

The FirebaseAnalytics SPM product resolves to GoogleAppMeasurement, which contains cross-references to APMETaskManager and APMMeasurement (Firebase Performance classes). When FirebasePerformance is not in the project, those symbols are missing at link time.

The fix: when SPM + WithoutAdIdSupport = true, use FirebaseAnalyticsCore instead — it resolves to GoogleAppMeasurementCore (no IDFA, no APM dependencies).

Fix applied in RNFBAnalytics.podspec

if defined?(spm_dependency) && !defined?($RNFirebaseDisableSPM) &&
   defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && $RNFirebaseAnalyticsWithoutAdIdSupport
  firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalyticsCore'], 'FirebaseAnalytics/Core')
else
  firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalytics'], 'FirebaseAnalytics/Core')
end

This change is fully backwards compatible — all existing paths (CocoaPods, SPM without the flag, SPM with IDFA) are unchanged.

This fix is already included in the current head of this PR. Happy to extract it into a separate PR if preferred.


Context: why SPM matters for React Native in 2026

This article is highly relevant for the community — it covers the broader roadmap and real-world pain points of migrating React Native apps to SPM, including the Xcode 26 requirement and Firebase-specific issues:

📖 React Native — Roadmap to Swift Package Manager 2026

@jsnavarroc
Copy link
Copy Markdown
Author

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!

Copy link
Copy Markdown
Collaborator

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

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

Comment thread .github/workflows/tests_e2e_ios.yml Outdated
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 }}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I prefer the variable interpolations at the front and all together and the various naming / key "sauce" at the end

Suggested change
restore-keys: ${{ runner.os }}-ios-pods-v3-${{ matrix.dep-resolution }}
restore-keys: ${{ runner.os }}-${{ matrix.dep-resolution }}-ios-pods-v3

Copy link
Copy Markdown
Author

@jsnavarroc jsnavarroc Apr 22, 2026

Choose a reason for hiding this comment

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

Done, reordered in the latest commit.

#import <Firebase/Firebase.h>
#else
@import FirebaseCore;
@import FirebaseAnalytics;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Author

@jsnavarroc jsnavarroc Apr 22, 2026

Choose a reason for hiding this comment

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

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).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 🤔

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

@jsnavarroc jsnavarroc Apr 22, 2026

Choose a reason for hiding this comment

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

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:

  1. A clear comment explaining the limitation
  2. A runtime warning when the user enables it in SPM mode
  3. Documentation pointing users to set $RNFirebaseDisableSPM = true if they need on-device conversion.

If firebase-ios-sdk adds it as an SPM product in the future, we can remove this restriction.

Comment on lines +18 to +21
#if __has_include(<Firebase/Firebase.h>)
#import <Firebase/Firebase.h>
#import <FirebaseAppCheck/FIRAppCheck.h>
#elif __has_include(<FirebaseAppCheck/FirebaseAppCheck.h>)
#import <FirebaseAppCheck/FirebaseAppCheck.h>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

<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

Copy link
Copy Markdown
Author

@jsnavarroc jsnavarroc Apr 22, 2026

Choose a reason for hiding this comment

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

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.

Comment thread packages/app/README.md
Comment on lines +74 to +82
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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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')
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment on lines +34 to +37
# 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This in This is required... is perhaps ambiguous for something this important

Suggested change
# 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated the comment in latest commit. It now reads:

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.

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'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment thread DOCUMENTACION_SPM_IMPLEMENTACION.md Outdated
@@ -0,0 +1,1135 @@
# Documentacion Completa: Soporte Dual SPM + CocoaPods para Firebase en React Native
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deleted in latest commit. It was a Spanish duplicate of docs/ios-spm.md.

@mikehardy mikehardy added Workflow: Waiting for User Response Blocked waiting for user response. Workflow: Needs Review Pending feedback or review from a maintainer. and removed Needs Attention labels Mar 19, 2026
@mikehardy
Copy link
Copy Markdown
Collaborator

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

@mikehardy mikehardy added Workflow: Needs Second Review Waiting on a second review before merge Workflow: Waiting for User Response Blocked waiting for user response. Workflow: Needs Review Pending feedback or review from a maintainer. and removed Workflow: Waiting for User Response Blocked waiting for user response. Workflow: Needs Review Pending feedback or review from a maintainer. Workflow: Needs Second Review Waiting on a second review before merge labels Mar 31, 2026
@jsnavarroc
Copy link
Copy Markdown
Author

jsnavarroc commented Apr 22, 2026

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

  1. Cache key order: Reordered per your suggestion (variable interpolations first)
  2. Analytics podspec comment inconsistency: Fixed, it's FirebasePerformance (APM symbols), not RemoteConfig
  3. GoogleAdsOnDeviceConversion: Added clear documentation about SPM incompatibility + runtime warning when user tries to enable it in SPM mode
  4. AppCheckModule.m spacing: Reverted all unrelated spacing changes, kept only the #if __has_include import block
  5. .npmignore: Added clarifying comment about the !ios/RNFBApp/RNFBVersion.m inclusion and the lack of a files array in package.json
  6. RNFBML.podspec: Removed old commented-out s.dependency and raise lines
  7. firebase_spm.rb: Improved static linkage comment with upstream context, added monorepo/pnpm path resolution note
  8. README: Added Expo section, reframed dynamic/static in terms of pre-built RN core, added monorepo/pnpm notes
  9. ios_config.sh: Replaced find with known deterministic path for SPM upload-symbols
  10. DOCUMENTACION_SPM_IMPLEMENTACION.md: Deleted (was a Spanish duplicate of docs/ios-spm.md)

Ask

If 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'
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 496d97c. Configure here.

#else
@import FirebaseCore;
@import FirebaseInstallations;
#endif
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 496d97c. Configure here.

@paulb777
Copy link
Copy Markdown
Contributor

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.

@jsnavarroc
Copy link
Copy Markdown
Author

jsnavarroc commented Apr 23, 2026

Heads up: tvOS + Xcode 26 hybrid SPM/CocoaPods edge case worth documenting

While integrating this PR in a tvOS React Native project (hybrid CocoaPods + SPM resolution, Xcode 26, RN 0.77 with react-native-tvos), I ran into a subtle TestFlight-only crash that took considerable time to diagnose. Sharing here so maintainers can decide if it's worth adding a warning or a note in the docs.

Symptom

  • App launches fine on tvOS simulator ✅
  • App launches fine when installed directly from Xcode onto a real Apple TV (WiFi pairing or USB-C) ✅
  • App crashes immediately at launch (<200ms, pre-main()) when the same archive is uploaded via Fastlane and installed from TestFlight ❌

No formal crash log is generated, ReportCrash logs Failed to create bundle record because the process dies too early. The only signal in the system log (from Console.app connected to the device) is:

PineBoard  [app<com.example.myapp>:738] Now flagged as pending exit for reason: launch failed
PineBoard  [app<com.example.myapp>:738] Process exited:
  <RBSProcessExitContext| specific, status:<RBSProcessExitStatus| domain:dyld(6) code:0>>.

Root cause

The combination that triggers it:

  1. Firebase resolved via SPM (source packages, statically linked into the main app binary)
  2. @react-native-firebase/* frameworks compiled as dynamic frameworks with -undefined dynamic_lookup (necessary in hybrid setup to avoid duplicate Firebase class registrations, as documented in other threads)
  3. Release configuration with default stripping settings (DEAD_CODE_STRIPPING=YES, STRIP_SWIFT_SYMBOLS=YES, etc.)

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 evidence

Before fix (binary that crashed in TestFlight):

  • Main app binary size: 3.3 MB
  • nm -g <binary> | grep "FIR" | wc -l0
  • nm -g <binary> | grep "InstallationIDDidChange" → nothing

After fix (binary that works in TestFlight):

  • Binary size: 6.0 MB (+80%)
  • nm -g <binary> | grep "FIR" | wc -l692
  • nm -g <binary> | grep "InstallationIDDidChange"S _FIRInstallationIDDidChangeNotification

Workaround

Anti-stripping settings on the main app target's Release configuration + exporting static symbols to the dynamic symbol table, added via post_install hook in the integrator's Podfile:

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
end

This 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).

Suggestion

Since 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:

  1. A non-blocking warning during pod install when the risky combination is detected (platform: tvos + SPM active + main target missing anti-stripping in Release), pointing to the fix.
  2. A troubleshooting section in the README / docs/ios-spm.md documenting the symptom and the Podfile snippet.

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, use_frameworks! :linkage => :static, Firebase iOS SDK 12.x via SPM). Others on the same configuration may hit it too.

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.

@ncooke3
Copy link
Copy Markdown

ncooke3 commented Apr 23, 2026

Hi @jsnavarroc, nice testing & investigation! If you didn't consider it, adding the -ObjC linker flag could possibly be an additional path that may (or may not) work, though it's not as precise as the workaround you documented. I'm unsure if it handles the risk of duplicate symbols.

In any case, I'll defer to @mikehardy here, but your suggestions sound reasonable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs Attention Workflow: Needs Review Pending feedback or review from a maintainer. Workflow: Waiting for User Response Blocked waiting for user response.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants