Skip to content

fix(analytics, messaging): dedupe GoogleUtilities classes under SPM dynamic frameworks - #9327

Merged
russellwheatley merged 1 commit into
mainfrom
feature/cprn-465-9322-spm-dynamic-frameworks-googleutilities-classes
Sep 23, 2026
Merged

russellwheatley merged 1 commit into
mainfrom
feature/cprn-465-9322-spm-dynamic-frameworks-googleutilities-classes

Conversation

@russellwheatley

@russellwheatley russellwheatley commented Sep 18, 2026

Copy link
Copy Markdown
Member

RNFBAnalytics.podspec and RNFBMessaging.podspec only declared their direct Firebase SPM product (FirebaseAnalytics, FirebaseMessaging). Before this fix, GULNetwork, GULReachability, and GULMethodSwizzler ended up compiled privately into both RNFBAnalytics.framework and RNFBMessaging.framework instead of a single shared dynamic framework, so several GUL* classes were defined twice at runtime once both pods were loaded together.

Confirmed: the raw FirebaseAnalytics/GoogleAppMeasurement .xcframework binaries don't bake in copies of these classes (checked with nm), they only reference them as undefined externals like any source-based product.

Not fully confirmed: why Xcode's SPM integration fails to promote this specific cluster to a shared framework once Analytics is in the graph, when an equivalent pure-source pairing (Messaging + Auth, sharing the GULAppDelegateSwizzler cluster instead) promotes cleanly with zero private copies. It isn't simply "transitive vs explicit" either: firebase-ios-sdk's own Package.swift already lists GULNetwork/GULMethodSwizzler as explicit top-level products on FirebaseAnalyticsWrapper, in the same array as GULAppDelegateSwizzler, and only the latter shared correctly. Whether Analytics's target also carrying a binaryTarget dependency changes Xcode's per-target sharing heuristic is a live, untested hypothesis.

What's confirmed to fix it: declaring these same GoogleUtilities products as an explicit top-level spm_dependency directly on both podspecs makes Xcode build one shared dynamic framework per product and link both RNFBAnalytics and RNFBMessaging against it.

test-expo is widened to the same first-party package list as #9313 (adds ai, app-check, auth, crashlytics, database, firestore, functions, installations, perf, phone-number-verification, remote-config, storage), so the closer proves this against the real graph new packages land in, not just Analytics/Messaging in isolation.

test-expo-ios-link.sh now discovers every RNFB* target dynamically instead of hardcoding Analytics/Messaging, nm's each built .framework, and fails if any watched GUL* class (including the two SWIFTPM_MODULE_BUNDLER_FINDER finder symbols) is missing, defined in more than one framework, or defined privately inside any RNFB* framework rather than a shared package product. Verified clean across the full 13-target graph: no spm_dependency needed anywhere beyond Analytics/Messaging.

Also dropped the hardcoded CC=clang/LD=clang xcodebuild args in that script. They're a ccache-pairing leftover never actually wired to this workflow, and they broke the pure-Swift SPM target (FirebaseCoreInternal) this repro pulls in once the graph widens.

Adding Crashlytics to test-expo surfaced two unrelated pre-existing bugs in packages/crashlytics/ios_config.sh that blocked the closer from reaching its own diagnosis: the dSYM plist lookup for Expo's target-folder layout (already fixed on main, just needed a rebase here) and upload-symbols having no non-fatal handling, so any upload failure hard-failed the whole xcodebuild instead of warning. The non-fatal-upload fix is tracked and PR'd standalone in #9333; it's duplicated here too since this closer needs it in the meantime, and it'll collapse to a no-op on the next rebase once #9333 merges.

Short OKF note added in okf-bundle/ios-spm-native-imports.md: explicit GoogleUtilities products can share a PackageProduct; that doesn't extend to FirebaseCore/FIRApp.


CI note: Create patch-package Patches has been flaky on this PR (unrelated to the change). The nested yarn add calls inside its Create Patches step aren't wrapped in the retry action the root Yarn Install step uses, so a registry hiccup or runner comms timeout fails the whole job. A re-run on the same commit has passed each time so far.

@russellwheatley
russellwheatley force-pushed the feature/cprn-465-9322-spm-dynamic-frameworks-googleutilities-classes branch 2 times, most recently from 0dc6c5a to 9a052ab Compare September 18, 2026 15:46
@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.81%. Comparing base (50a946a) to head (214ebf9).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9327      +/-   ##
============================================
- Coverage     69.81%   69.81%   -0.00%     
  Complexity     2129     2129              
============================================
  Files           439      439              
  Lines         25518    25518              
  Branches       4257     4257              
============================================
- Hits          17814    17813       -1     
  Misses         6360     6360              
- Partials       1344     1345       +1     
Flag Coverage Δ
android-native 65.91% <ø> (ø)
e2e-ts-android 54.42% <ø> (ø)
e2e-ts-ios 53.89% <ø> (ø)
e2e-ts-macos 49.94% <ø> (-<0.01%) ⬇️
ios-ruby 100.00% <ø> (ø)
jest 48.83% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@russellwheatley
russellwheatley force-pushed the feature/cprn-465-9322-spm-dynamic-frameworks-googleutilities-classes branch from 9a052ab to a109c68 Compare September 22, 2026 14:00
@russellwheatley
russellwheatley marked this pull request as ready for review September 22, 2026 16:21

@mikehardy mikehardy left a comment

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.

In the work queue for Swift / Kotlin port I've got a queue item for ccache / build optimization with some gap analysis already - goal of that queue item is to seed a new queue specifically looking at how ccache fits (or doesn't!) in a Swift-centric world along with some specific probes and things to test from gap analysis. It's persisted in the related linear area

This specific fix looks fine - I think it will conflict a little but not in any difficult to resolve way and only in the test-expo / dSYMs upload area, not the core fix which appears to be +1, so, +1

if [[ ${PODS_ROOT} && -f "${PODS_ROOT}/FirebaseCrashlytics/run" ]]; then
echo "info: Exec FirebaseCrashlytics Run from Pods"
"${PODS_ROOT}/FirebaseCrashlytics/run"
if ! "${PODS_ROOT}/FirebaseCrashlytics/run"; then

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.

didn't the test-expo PR duplicate / touch this area as well?

Comment thread test-expo/app.json
},
"plugins": [
"@react-native-firebase/app",
"@react-native-firebase/analytics",

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 think will collide with test-expo separate PR as well

…ynamic frameworks

Under Expo's SPM + dynamic frameworks path, RNFBAnalytics.podspec and
RNFBMessaging.podspec only declared their direct Firebase SPM products
(FirebaseAnalytics, FirebaseMessaging). GULNetwork, GULReachability, and
GULMethodSwizzler were only reachable transitively through those, and
Xcode's SPM integration doesn't reliably promote a transitively-only
reached product to a shared PackageProduct.framework. It compiled a
private copy of each straight into RNFBAnalytics.framework and
RNFBMessaging.framework instead, so GULNetwork/GULMutableDictionary/
GULNetworkURLSession/GULReachabilityChecker/GULSessionDeallocTracker/
GULSwizzler ended up defined twice at runtime once both pods were loaded.

The fix declares those same GoogleUtilities products as an explicit
top-level spm_dependency on both podspecs, the same way GULAppDelegateSwizzler
already was. That's enough for Xcode to treat them as shared and build one
dynamic framework per product instead of duplicating them.

test-expo-ios-link.sh gained a new #9322 check that walks every built
.framework, nm's its defined external symbols, and fails if any of the
affected GUL classes are defined in more than one framework or privately
inside any RNFB* framework rather than a shared PackageProduct. Also removed
the hardcoded CC=clang/LD=clang xcodebuild args, which broke pure-Swift SPM
targets pulled in by this repro (FirebaseCoreInternal).

test-expo's package list and the closer's RNFB* target discovery are already
on main, so this commit does not touch them. The #9322 check still runs
against that graph. Every watched GUL class, including the two
SWIFTPM_MODULE_BUNDLER_FINDER symbols, has exactly one owner, and that owner
is never an RNFB* framework.

Added a short OKF note in ios-spm-native-imports.md: explicit GoogleUtilities
products can share a PackageProduct. That does not extend to FirebaseCore/
FIRApp.
@russellwheatley
russellwheatley force-pushed the feature/cprn-465-9322-spm-dynamic-frameworks-googleutilities-classes branch from a109c68 to 214ebf9 Compare September 23, 2026 08:37
@russellwheatley
russellwheatley merged commit b70ba88 into main Sep 23, 2026
28 checks passed
@russellwheatley
russellwheatley deleted the feature/cprn-465-9322-spm-dynamic-frameworks-googleutilities-classes branch September 23, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛] SPM + dynamic frameworks (Expo SDK 57): GoogleUtilities classes duplicated in RNFBAnalytics / RNFBMessaging

2 participants