Skip to content

Commit 214ebf9

Browse files
fix(analytics, messaging): dedupe GoogleUtilities classes under SPM dynamic 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.
1 parent 50a946a commit 214ebf9

4 files changed

Lines changed: 164 additions & 1 deletion

File tree

‎.github/workflows/scripts/test-expo-ios-link.sh‎

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,6 @@ xcodebuild_args=(
257257
ARCHS="${HOST_ARCH}"
258258
VALID_ARCHS="${HOST_ARCH}"
259259
ONLY_ACTIVE_ARCH=YES
260-
CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++
261260
-workspace "$WORKSPACE"
262261
-scheme "$SCHEME"
263262
-configuration Release
@@ -303,3 +302,82 @@ if grep -q "duplicate symbol '_FIRFirebaseVersion'" "$XCODEBUILD_LOG" ||
303302
fi
304303

305304
log "PASS: Expo documented path links RNFBApp/RNFBMessaging as frameworks without duplicate Firebase symbols"
305+
306+
# #9322: without an explicit top-level spm_dependency declaration on
307+
# RNFBAnalytics.podspec / RNFBMessaging.podspec (see the comments there for
308+
# the full writeup, including what's confirmed vs. still an open question),
309+
# GoogleUtilities classes (GULNetwork, GULReachabilityChecker, etc.) end up
310+
# compiled privately into both RNFBAnalytics.framework and
311+
# RNFBMessaging.framework instead of one shared dynamic framework, and
312+
# collide at runtime once both are loaded in-process.
313+
#
314+
# Requires @react-native-firebase/analytics alongside messaging in
315+
# test-expo/package.json to actually exercise both sides of the graph.
316+
log "--- #9322 GoogleUtilities dedupe diagnosis ---"
317+
BUILT_PRODUCTS_DIR="$(grep -oE '/[^ ]*/Build/Products/[A-Za-z0-9_.-]+' "$XCODEBUILD_LOG" | head -1 || true)"
318+
if [[ -z "$BUILT_PRODUCTS_DIR" || ! -d "$BUILT_PRODUCTS_DIR" ]]; then
319+
log "ERROR: could not determine BUILT_PRODUCTS_DIR from xcodebuild log for #9322 check"
320+
exit 1
321+
fi
322+
323+
nine322_classes=(
324+
GULNetwork
325+
GULMutableDictionary
326+
GULNetworkURLSession
327+
GULNetworkURLSessionWeakHolder
328+
GULSessionDeallocTracker
329+
GULReachabilityChecker
330+
GULSwizzler
331+
GoogleUtilities_GoogleUtilities_Network_SWIFTPM_MODULE_BUNDLER_FINDER
332+
GoogleUtilities_GoogleUtilities_Reachability_SWIFTPM_MODULE_BUNDLER_FINDER
333+
)
334+
335+
nine322_work_dir="$(mktemp -d)"
336+
trap 'rm -rf "$nine322_work_dir"' EXIT
337+
338+
while IFS= read -r fw_dir; do
339+
fw_name="$(basename "$fw_dir" .framework)"
340+
bin="$fw_dir/$fw_name"
341+
[[ -f "$bin" ]] || continue
342+
fw_syms="$(nm -gU "$bin" 2>/dev/null || true)"
343+
[[ -z "$fw_syms" ]] && continue
344+
for class_name in "${nine322_classes[@]}"; do
345+
if grep -qE '_OBJC_CLASS_\$_'"${class_name}"'$' <<<"$fw_syms"; then
346+
echo "$fw_name" >>"${nine322_work_dir}/${class_name}.txt"
347+
fi
348+
done
349+
done < <(find "$BUILT_PRODUCTS_DIR" -type d -name "*.framework" ! -path "*/*.app/*" 2>/dev/null)
350+
351+
nine322_failed=0
352+
for class_name in "${nine322_classes[@]}"; do
353+
owners_file="${nine322_work_dir}/${class_name}.txt"
354+
if [[ ! -f "$owners_file" ]]; then
355+
log "ERROR: #9322 -- ${class_name} not defined in any built framework"
356+
nine322_failed=1
357+
continue
358+
fi
359+
owner_count="$(wc -l <"$owners_file" | tr -d ' ')"
360+
owners="$(tr '\n' ' ' <"$owners_file")"
361+
if [[ "$owner_count" -ne 1 ]]; then
362+
log "ERROR: #9322 -- expected exactly 1 framework defining ${class_name}, found ${owner_count}: ${owners}"
363+
nine322_failed=1
364+
continue
365+
fi
366+
case "$owners" in
367+
RNFB*)
368+
log "ERROR: #9322 -- ${class_name} is privately duplicated into ${owners} instead of a shared framework"
369+
nine322_failed=1
370+
;;
371+
*)
372+
log "ok: ${class_name} shared in ${owners}"
373+
;;
374+
esac
375+
done
376+
log "--- end #9322 GoogleUtilities dedupe diagnosis ---"
377+
378+
if [[ "$nine322_failed" -ne 0 ]]; then
379+
log "ERROR: #9322 GoogleUtilities duplicate-class regression detected"
380+
exit 1
381+
fi
382+
383+
log "PASS: no RNFB* framework privately duplicates a shared GoogleUtilities class (#9322)"

‎okf-bundle/ios-spm-native-imports.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,30 @@ CocoaPods-only `IdentitySupport` on the SPM path.
248248
Consumer-facing version pin and `-ObjC` notes:
249249
[`docs/ios-spm.mdx`](../docs/ios-spm.mdx).
250250

251+
### GoogleUtilities: explicit `spm_dependency` lets pods share one product
252+
253+
The FirebaseCore limitation above is about *automatic* SPM products declared
254+
only transitively. A related but narrower case: some `GoogleUtilities`
255+
products (`GULNetwork`, `GULReachability`, `GULMethodSwizzler`) were only
256+
reachable transitively through `FirebaseAnalytics`/`FirebaseMessaging` on
257+
`RNFBAnalytics.podspec`/`RNFBMessaging.podspec`, and Xcode's SPM integration
258+
did not reliably promote them to a shared `PackageProduct.framework` in that
259+
graph — each pod compiled a private copy instead, colliding at runtime once
260+
both were loaded (GitHub
261+
[#9322](https://github.com/invertase/react-native-firebase/issues/9322)).
262+
263+
Declaring the same GoogleUtilities products as an explicit top-level
264+
`spm_dependency` directly on each consuming podspec (same pattern as the
265+
existing `GULAppDelegateSwizzler` declaration) is enough for Xcode to build
266+
one shared dynamic framework per product and link every consumer against it,
267+
instead of duplicating it per pod. This is scoped to `GoogleUtilities`
268+
products specifically; it does **not** change or fix the `FirebaseCore`/
269+
`FIRApp` sharing limitation described above, which stays unsupported under
270+
SPM regardless of how explicitly any product is declared. Add this
271+
declaration only to podspecs actually shown to privately duplicate a watched
272+
class by `yarn test-expo:ios:link`'s `#9322` diagnosis, not pre-emptively to
273+
every RNFB podspec.
274+
251275
## App target FirebaseCore link: package dependency alone is not enough
252276

253277
`rnfirebase_add_spm_core_to_app_target` exists for the case in the table above

‎packages/analytics/RNFBAnalytics.podspec‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,49 @@ Pod::Spec.new do |s|
6767
firebase_dependency(s, firebase_sdk_version, ['FirebaseAnalytics'], 'FirebaseAnalytics/Core')
6868
end
6969

70+
# SPM: fix for #9322 -- GULNetwork/GULReachability/GULMethodSwizzler ended
71+
# up compiled privately into both RNFBAnalytics.framework and
72+
# RNFBMessaging.framework instead of one shared dynamic framework,
73+
# producing duplicate ObjC class definitions (GULNetwork,
74+
# GULMutableDictionary, GULNetworkURLSession, GULReachabilityChecker,
75+
# GULSessionDeallocTracker, GULSwizzler) at runtime once both frameworks
76+
# are loaded together.
77+
#
78+
# Confirmed: the raw FirebaseAnalytics/GoogleAppMeasurement .xcframework
79+
# binaries do NOT bake in their own copies of these classes (checked via
80+
# `nm`); they only reference them as undefined externals, same as any
81+
# source-based product.
82+
#
83+
# Not fully confirmed: *why* Xcode's SPM integration fails to promote this
84+
# specific cluster to a shared PackageProduct.framework once RNFBAnalytics
85+
# is in the graph, when an equivalent pure-source pairing (RNFBMessaging +
86+
# RNFBAuth, sharing the GULAppDelegateSwizzler cluster instead) promotes
87+
# cleanly with no private copies at all. It isn't simply "declared
88+
# transitively vs explicitly" either: firebase-ios-sdk's own Package.swift
89+
# already lists GULNetwork/GULMethodSwizzler as explicit top-level
90+
# products on FirebaseAnalyticsWrapper, in the same dependency array as
91+
# GULAppDelegateSwizzler, and only the latter shared correctly. Whether
92+
# Analytics's target also carrying a binaryTarget dependency is what
93+
# changes Xcode's per-target sharing heuristic here is a live, untested
94+
# hypothesis, not something this fix confirms or rules out.
95+
#
96+
# What IS confirmed: declaring these same products as an explicit
97+
# top-level SPM dependency directly on *this* podspec (and on
98+
# RNFBMessaging.podspec) makes Xcode build one shared dynamic framework
99+
# per product and link both RNFBAnalytics and RNFBMessaging against it,
100+
# instead of each compiling its own copy. Verified with a passing run
101+
# (fix applied) and a failing negative control (fix reverted).
102+
if defined?(spm_dependency) && !rnfirebase_spm_disabled?
103+
# 8.1.3 floor matches the GoogleUtilities version firebase-ios-sdk 12.x
104+
# resolves transitively; bump this alongside firebase_sdk_version if
105+
# firebase-ios-sdk ever moves to GoogleUtilities 9.x.
106+
spm_dependency(s,
107+
url: 'https://github.com/google/GoogleUtilities.git',
108+
requirement: { kind: 'upToNextMajorVersion', minimumVersion: '8.1.3' },
109+
products: ['GULNetwork', 'GULReachability', 'GULMethodSwizzler']
110+
)
111+
end
112+
70113
unless defined?(spm_dependency) && !rnfirebase_spm_disabled?
71114
# CocoaPods-only: conditional IdentitySupport subspec
72115
if defined?($RNFirebaseAnalyticsWithoutAdIdSupport) && ($RNFirebaseAnalyticsWithoutAdIdSupport == true)

‎packages/messaging/RNFBMessaging.podspec‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,24 @@ Pod::Spec.new do |s|
6565
['Firebase/Messaging', 'FirebaseCoreExtension']
6666
)
6767

68+
# SPM: fix for #9322 -- see the matching comment in RNFBAnalytics.podspec
69+
# for the full writeup, including what's actually confirmed vs. still an
70+
# open question about *why* Xcode's SPM integration behaves this way.
71+
# RNFBMessaging needs the same explicit top-level declaration for Xcode to
72+
# share a single dynamic framework per product instead of privately
73+
# duplicating GULNetwork/GULReachabilityChecker/GULSwizzler into
74+
# RNFBMessaging.framework too.
75+
if defined?(spm_dependency) && !rnfirebase_spm_disabled?
76+
# 8.1.3 floor matches the GoogleUtilities version firebase-ios-sdk 12.x
77+
# resolves transitively; bump this alongside firebase_sdk_version if
78+
# firebase-ios-sdk ever moves to GoogleUtilities 9.x.
79+
spm_dependency(s,
80+
url: 'https://github.com/google/GoogleUtilities.git',
81+
requirement: { kind: 'upToNextMajorVersion', minimumVersion: '8.1.3' },
82+
products: ['GULNetwork', 'GULReachability', 'GULMethodSwizzler']
83+
)
84+
end
85+
6886
if defined?($RNFirebaseAsStaticFramework)
6987
Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'"
7088
s.static_framework = $RNFirebaseAsStaticFramework

0 commit comments

Comments
 (0)