diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml
index 5d5a687904..afb6c9ac79 100644
--- a/.github/workflows/sample-application.yml
+++ b/.github/workflows/sample-application.yml
@@ -43,7 +43,7 @@ jobs:
caller_ref: ${{ github.ref }}
build-ios:
- name: Build ${{ matrix.rn-architecture }} ios ${{ matrix.build-type }} ${{ matrix.ios-use-frameworks }}
+ name: Build ${{ matrix.rn-architecture }} ios ${{ matrix.build-type }} ${{ matrix.ios-use-frameworks }} ${{ matrix.sentry-consumption }}
runs-on: macos-26-xlarge
needs: [diff_check, detect-changes]
if: >-
@@ -60,6 +60,16 @@ jobs:
rn-architecture: ["new"]
ios-use-frameworks: ["no-frameworks"]
build-type: ["dev", "production"]
+ sentry-consumption: ["xcframework"]
+ # `xcframework` is the default: RNSentry vendors a prebuilt
+ # `Sentry-Dynamic.xcframework` downloaded from sentry-cocoa's release.
+ # Keep a single CocoaPods job to catch regressions in the source-build
+ # fallback (`SENTRY_USE_XCFRAMEWORK=0`).
+ include:
+ - rn-architecture: "new"
+ ios-use-frameworks: "no-frameworks"
+ build-type: "production"
+ sentry-consumption: "cocoapods"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -95,6 +105,7 @@ jobs:
[[ "${{ matrix.build-type }}" == "production" ]] && export ENABLE_PROD=1 || export ENABLE_PROD=0
[[ "${{ matrix.rn-architecture }}" == "new" ]] && export ENABLE_NEW_ARCH=1 || export ENABLE_NEW_ARCH=0
[[ "${{ matrix.ios-use-frameworks }}" == "dynamic-frameworks" ]] && export USE_FRAMEWORKS=dynamic
+ [[ "${{ matrix.sentry-consumption }}" == "cocoapods" ]] && export SENTRY_USE_XCFRAMEWORK=0
./scripts/pod-install.sh
@@ -112,7 +123,10 @@ jobs:
./scripts/build-ios.sh
- name: Archive iOS App
- if: ${{ matrix.rn-architecture == 'new' && matrix.build-type == 'production' && matrix.ios-use-frameworks == 'no-frameworks' }}
+ # Only upload from the xcframework job (the default consumption path)
+ # to avoid duplicate artifact names when the CocoaPods regression job
+ # runs.
+ if: ${{ matrix.rn-architecture == 'new' && matrix.build-type == 'production' && matrix.ios-use-frameworks == 'no-frameworks' && matrix.sentry-consumption == 'xcframework' }}
working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}
run: |
zip -r \
@@ -120,7 +134,7 @@ jobs:
sentryreactnativesample.app
- name: Upload iOS APP
- if: ${{ matrix.rn-architecture == 'new' && matrix.build-type == 'production' && matrix.ios-use-frameworks == 'no-frameworks' }}
+ if: ${{ matrix.rn-architecture == 'new' && matrix.build-type == 'production' && matrix.ios-use-frameworks == 'no-frameworks' && matrix.sentry-consumption == 'xcframework' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: sample-rn-${{ matrix.rn-architecture }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-ios
@@ -131,7 +145,7 @@ jobs:
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
- name: build-sample-${{ matrix.rn-architecture }}-ios-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-logs
+ name: build-sample-${{ matrix.rn-architecture }}-ios-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-${{ matrix.sentry-consumption }}-logs
path: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/ios/*.log
build-android:
diff --git a/.gitignore b/.gitignore
index b6dea85f87..30f069ab73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -91,6 +91,9 @@ node_modules.bak
# API Extractor temp files
/packages/core/temp/
+# sentry-cocoa xcframework downloaded at pod install time
+/packages/core/ios/Vendor/
+
# Sentry React Native Monorepo
/packages/core/README.md
.env.sentry-build-plugin
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ec3591fa7..17fcd8884d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,16 @@
> make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first.
+## Unreleased
+
+### Changes
+
+- Consume `sentry-cocoa` as a prebuilt xcframework by default on iOS ([#6381](https://github.com/getsentry/sentry-react-native/pull/6381))
+
+ **Warning**
+
+ **This may be a breaking change for some setups.** `pod install` now downloads `Sentry.xcframework` from sentry-cocoa's GitHub release (SHA256-verified) and vendors it, instead of building Sentry from source as a CocoaPod. If your iOS build breaks after upgrading (e.g. when another pod also depends on the `Sentry` CocoaPod), or if your `pod install` environment cannot reach `github.com`, set `SENTRY_USE_XCFRAMEWORK=0` before `pod install` to restore the previous source-build behavior.
+
## 8.17.0
### Features
diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec
index 84189b7df6..c0d5a8b4da 100644
--- a/packages/core/RNSentry.podspec
+++ b/packages/core/RNSentry.podspec
@@ -45,41 +45,102 @@ Pod::Spec.new do |s|
# is pulled in here; on Android it is compiled by the dedicated CMake target
# in `android/CMakeLists.txt`. The files are guarded with
# `RCT_NEW_ARCH_ENABLED` so they compile to empty TUs on Old Arch.
- s.source_files = 'ios/**/*.{h,m,mm}', 'cpp/**/*.{h,cpp}'
+ #
+ # We include `.swift` (for `RNSentrySwiftLinkStub.swift`) only on RN >=
+ # 0.75. Adding a Swift file makes CocoaPods treat RNSentry as a Swift
+ # pod, which then requires modular headers from its ObjC dependencies
+ # (React-Core, React-hermes) — RN < 0.75 doesn't emit those, so
+ # `pod install` fails with:
+ # "The Swift pod `RNSentry` depends upon `React-hermes`, which does
+ # not define modules."
+ # The stub is only needed when linking Sentry.xcframework's Swift
+ # symbols into a dynamic framework anyway (RN 0.86+ `use_frameworks!
+ # :dynamic`), so gating on RN 0.75 is safe.
+ supports_swift_stub = rn_version[:major] >= 1 || (rn_version[:major] == 0 && rn_version[:minor] >= 75)
+ if supports_swift_stub
+ s.source_files = 'ios/**/*.{h,m,mm,swift}', 'cpp/**/*.{h,cpp}'
+ s.swift_versions = ['5.5']
+ else
+ s.source_files = 'ios/**/*.{h,m,mm}', 'cpp/**/*.{h,cpp}'
+ end
+ s.exclude_files = 'ios/Vendor/**/*'
s.public_header_files = 'ios/RNSentry.h', 'ios/RNSentrySDK.h', 'ios/RNSentryStart.h', 'ios/RNSentryVersion.h', 'ios/RNSentryBreadcrumb.h', 'ios/RNSentryReplay.h', 'ios/RNSentryReplayBreadcrumbConverter.h', 'ios/Replay/RNSentryReplayMask.h', 'ios/Replay/RNSentryReplayUnmask.h', 'ios/RNSentryTimeToDisplay.h'
s.compiler_flags = other_cflags
- s.pod_target_xcconfig = {
+ pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES'
}
sentry_cocoa_version = '9.19.1'
- # Opt-in to consuming sentry-cocoa via Swift Package Manager.
- # When `SENTRY_USE_SPM=1` is set, RNSentry pulls `Sentry` from the
- # sentry-cocoa SPM package as a binary xcframework instead of from
- # the Sentry CocoaPods source build. Defaults to CocoaPods consumption
- # for backward compatibility with the full RN version range we support.
+ # Consume sentry-cocoa as a prebuilt `Sentry.xcframework` by default.
+ #
+ # The xcframework is downloaded from sentry-cocoa's GitHub Release,
+ # SHA256-verified, and cached under `ios/Vendor/`. CocoaPods then links it
+ # via `s.vendored_frameworks`. This avoids compiling sentry-cocoa from
+ # source (fast install) and sidesteps the Xcode 16/26 archive bug that
+ # affects the same xcframework when consumed through Xcode's SPM
+ # integration (`Signatures/*.signature` collision during archive) — the
+ # CocoaPods embed path is a different pipeline and is not affected.
+ #
+ # Set `SENTRY_USE_XCFRAMEWORK=0` to fall back to the source-built
+ # `Sentry` CocoaPod (e.g. for offline builds behind a restrictive proxy).
#
- # Requires React Native >= 0.75 because the SPM helper
- # (`react-native/scripts/cocoapods/spm.rb`) is loaded transitively from
- # the Podfile via `react_native_pods.rb`.
- if ENV['SENTRY_USE_SPM'] == '1'
- unless defined?(SPM) && SPM.respond_to?(:dependency)
- raise 'SENTRY_USE_SPM=1 is set but the SPM helper is not loaded. ' \
- 'This requires React Native >= 0.75 and a Podfile that imports ' \
- 'react_native_pods.rb.'
+ # `SENTRY_USE_SPM` was the name in earlier drafts of this PR; honor it as a
+ # deprecated alias so CI or local envs still exporting `SENTRY_USE_SPM=0`
+ # don't silently take the new xcframework path.
+ env_use_xcframework = ENV['SENTRY_USE_XCFRAMEWORK']
+ if env_use_xcframework.nil? && !ENV['SENTRY_USE_SPM'].nil?
+ Pod::UI.warn '[Sentry] SENTRY_USE_SPM is deprecated; use SENTRY_USE_XCFRAMEWORK instead.' if defined?(Pod::UI)
+ env_use_xcframework = ENV['SENTRY_USE_SPM']
+ end
+ use_xcframework = case env_use_xcframework
+ when '0' then false
+ else true
+ end
+
+ if use_xcframework
+ sentry_xcframework_dir = ensure_sentry_xcframework(sentry_cocoa_version, 'Sentry')
+ s.vendored_frameworks = 'ios/Vendor/Sentry.xcframework'
+
+ # Xcode's `-F
` doesn't descend into `.xcframework` bundles — it
+ # looks for `Sentry.framework` directly at the given path. Point a
+ # separate framework search path at each slice, gated by the matching
+ # SDK selector so `#import ` resolves against exactly one
+ # slice per build. An unconditional search-path list would let Xcode's
+ # Swift module precompiler stumble into a slice for a different arch
+ # and fail with "unsupported Swift architecture". New slices in future
+ # sentry-cocoa releases are picked up automatically at pod-install.
+ #
+ # Point the search paths at the pod-install-time absolute path to the
+ # xcframework. `${PODS_TARGET_SRCROOT}` is only defined in per-pod
+ # xcconfigs, not in aggregate/user-target xcconfigs, and a
+ # `${PODS_ROOT}`-relative fallback works for one Podfile layout but
+ # breaks for another (e.g. the RN sample apps put node_modules at a
+ # different depth from RNSentryCocoaTester). Using the absolute path
+ # avoids the layout-detection dance — the path is regenerated on
+ # every `pod install`, so it's not something anyone commits.
+ xcframework_search_paths = {}
+ sentry_xcframework_slices_by_sdk(sentry_xcframework_dir).each do |sdk, slice_ids|
+ paths = slice_ids.map do |slice|
+ %("#{File.join(sentry_xcframework_dir, slice)}")
+ end
+ xcframework_search_paths["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] =
+ (['$(inherited)'] + paths).join(' ')
end
- SPM.dependency(s,
- url: 'https://github.com/getsentry/sentry-cocoa',
- requirement: { kind: 'exactVersion', version: sentry_cocoa_version },
- products: ['Sentry']
- )
+
+ pod_target_xcconfig.merge!(xcframework_search_paths)
+ s.user_target_xcconfig = xcframework_search_paths
else
s.dependency 'Sentry', sentry_cocoa_version
end
+ # Assign before `install_modules_dependencies` so it can merge its
+ # RN-specific settings on top. Assigning after would clobber those and
+ # break header resolution across the pod.
+ s.pod_target_xcconfig = pod_target_xcconfig
+
if defined? install_modules_dependencies
# Default React Native dependencies for 0.71 and above (new and legacy architecture)
install_modules_dependencies(s)
@@ -88,10 +149,13 @@ Pod::Spec.new do |s|
if is_new_arch_enabled then
# New Architecture on React Native 0.70 and older
- s.pod_target_xcconfig.merge!({
+ pod_target_xcconfig.merge!({
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
})
+ # `install_modules_dependencies` is not defined on RN < 0.71 so re-assigning
+ # here is safe — nothing else has written to `s.pod_target_xcconfig` yet.
+ s.pod_target_xcconfig = pod_target_xcconfig
s.dependency "React-RCTFabric" # Required for Fabric Components (like RCTViewComponentView)
s.dependency "React-Codegen"
diff --git a/packages/core/ios/RNSentrySwiftLinkStub.swift b/packages/core/ios/RNSentrySwiftLinkStub.swift
new file mode 100644
index 0000000000..7e456ef6c1
--- /dev/null
+++ b/packages/core/ios/RNSentrySwiftLinkStub.swift
@@ -0,0 +1,12 @@
+// This file exists to force Xcode to link the Swift runtime compatibility
+// libraries (e.g. libswiftCompatibility56, libswiftCompatibilityConcurrency)
+// into RNSentry. Those libs are required by our vendored `Sentry.xcframework`
+// static Swift library and Xcode only auto-links them when the consuming
+// target itself contains Swift code — without this stub, linking a dynamic
+// RNSentry framework fails with:
+// Undefined symbols: "__swift_FORCE_LOAD_$_swiftCompatibility56"
+
+// A private, unused constant so the compiler emits a real object file. A
+// pure-comment file compiles to nothing and would defeat the purpose of
+// the stub.
+private let _rnSentrySwiftLinkStub: Void = ()
diff --git a/packages/core/scripts/sentry_utils.rb b/packages/core/scripts/sentry_utils.rb
index 5dc57a3b52..fb2e3cbe4c 100644
--- a/packages/core/scripts/sentry_utils.rb
+++ b/packages/core/scripts/sentry_utils.rb
@@ -40,3 +40,135 @@ def should_use_folly_flags(rn_version)
def is_new_hermes_runtime(rn_version)
return (rn_version[:major] >= 1 || (rn_version[:major] == 0 && rn_version[:minor] >= 81))
end
+
+require 'digest'
+require 'fileutils'
+
+# SHA256 checksums of `.xcframework.zip` assets published in
+# sentry-cocoa GitHub releases (same value as the SPM binary target checksum
+# in sentry-cocoa's `Package.swift`). Register the checksum for each
+# sentry-cocoa version we ship a prebuilt xcframework for.
+SENTRY_COCOA_XCFRAMEWORK_CHECKSUMS = {
+ '9.19.1' => {
+ # `Sentry.xcframework.zip` — the static product. Its enclosing xcframework
+ # name matches the framework name inside (both `Sentry`), which CocoaPods
+ # requires to generate `-framework Sentry` correctly and to resolve the
+ # `Sentry` module. `Sentry-Dynamic.xcframework` would ship the same
+ # `Sentry.framework` inside but under a mismatched enclosing name, so
+ # CocoaPods generates `-framework Sentry-Dynamic` and fails at link.
+ 'Sentry' => 'd6d545af17e49851cda2747b0f45cde78ce08ea37709dde5a956c6b4671224e8',
+ },
+}.freeze
+
+# Ensures `/ios/Vendor/.xcframework` exists.
+#
+# On first invocation, downloads the prebuilt xcframework zip from
+# sentry-cocoa's GitHub release, verifies its SHA256 checksum against
+# `SENTRY_COCOA_XCFRAMEWORK_CHECKSUMS`, and extracts it. Subsequent
+# invocations are no-ops.
+#
+# Consuming sentry-cocoa this way (vs. through Xcode's SPM integration)
+# avoids the Xcode 16/26 archive bug where a signed SPM binary xcframework's
+# `Signatures/*.signature` file collides during the archive step.
+def ensure_sentry_xcframework(version, product = 'Sentry')
+ vendor_dir = File.expand_path('../ios/Vendor', __dir__)
+ target_dir = File.join(vendor_dir, "#{product}.xcframework")
+ # Treat the presence of `Info.plist` inside the xcframework as the "healthy"
+ # sentinel rather than just the directory existence. A directory without
+ # `Info.plist` most likely came from an interrupted `unzip` and would
+ # otherwise silently short-circuit re-download here.
+ target_manifest = File.join(target_dir, 'Info.plist')
+ return target_dir if File.file?(target_manifest)
+
+ expected_checksum = SENTRY_COCOA_XCFRAMEWORK_CHECKSUMS.dig(version, product)
+ unless expected_checksum
+ raise "sentry-cocoa xcframework checksum not registered for #{product} " \
+ "#{version}. Add it to SENTRY_COCOA_XCFRAMEWORK_CHECKSUMS in " \
+ "packages/core/scripts/sentry_utils.rb after bumping the version."
+ end
+
+ # Wipe any stale partial extract from a previous interrupted run so we
+ # always start from a clean tree.
+ FileUtils.rm_rf(target_dir)
+ FileUtils.mkdir_p(vendor_dir)
+ zip_path = File.join(vendor_dir, "#{product}.xcframework.zip")
+ url = "https://github.com/getsentry/sentry-cocoa/releases/download/" \
+ "#{version}/#{product}.xcframework.zip"
+
+ Pod::UI.puts "[Sentry] Downloading #{product} #{version} from GitHub Releases…" if defined?(Pod::UI)
+ unless system('curl', '-sSfL', '-o', zip_path, url)
+ raise "Failed to download #{url}"
+ end
+
+ actual_checksum = Digest::SHA256.file(zip_path).hexdigest
+ unless actual_checksum == expected_checksum
+ FileUtils.rm_f(zip_path)
+ raise "Checksum mismatch for #{product} #{version}: expected " \
+ "#{expected_checksum}, got #{actual_checksum}"
+ end
+
+ unless system('unzip', '-q', '-o', zip_path, '-d', vendor_dir)
+ raise "Failed to extract #{zip_path}"
+ end
+ FileUtils.rm_f(zip_path)
+
+ # Guard against a release archive whose internal layout changed (e.g. a
+ # nested folder). Without this check, a wrong layout silently succeeds and
+ # then fails much later during `pod install` with a confusing "framework
+ # not found" error.
+ unless File.file?(target_manifest)
+ raise "Expected #{target_manifest} after extracting #{product}.xcframework.zip. " \
+ "The sentry-cocoa release archive layout may have changed — update " \
+ "the extraction logic in packages/core/scripts/sentry_utils.rb."
+ end
+
+ target_dir
+end
+
+# Returns a hash of ` => [slice_id, ...]` for the slice
+# directories inside an xcframework bundle.
+#
+# Xcode's `-F ` does not descend into an `.xcframework` bundle at
+# search-path lookup time — it only sees `Sentry.xcframework` as a directory
+# and doesn't find `Sentry.framework` inside. Callers use these groupings
+# to emit SDK-conditional `FRAMEWORK_SEARCH_PATHS[sdk=…*]` xcconfig entries
+# so each SDK build only sees its own slice — putting all slices under a
+# single unconditional search path lets Xcode's Swift module precompiler
+# stumble into an incompatible slice and fail with
+# "unsupported Swift architecture".
+#
+# Slice-name convention is stable across the xcframeworks Apple has ever
+# published:
+# ios-*-simulator -> iphonesimulator
+# ios-*-maccatalyst -> maccatalyst
+# ios-… -> iphoneos
+# tvos-*-simulator -> appletvsimulator
+# tvos-… -> appletvos
+# watchos-*-simulator -> watchsimulator
+# watchos-… -> watchos
+# xros-*-simulator -> xrsimulator
+# xros-… -> xros
+# macos-… -> macosx
+def sentry_xcframework_slices_by_sdk(xcframework_dir)
+ slice_ids = Dir.children(xcframework_dir).select do |name|
+ File.directory?(File.join(xcframework_dir, name)) && name != '_CodeSignature'
+ end.sort
+
+ slice_ids.each_with_object({}) do |slice, groups|
+ sdk = _sentry_sdk_for_slice(slice)
+ next unless sdk # unknown platform prefix — skip rather than mis-attach
+ (groups[sdk] ||= []) << slice
+ end
+end
+
+def _sentry_sdk_for_slice(slice_id)
+ return 'maccatalyst' if slice_id.include?('-maccatalyst')
+ simulator = slice_id.end_with?('-simulator')
+ case
+ when slice_id.start_with?('ios-') then simulator ? 'iphonesimulator' : 'iphoneos'
+ when slice_id.start_with?('tvos-') then simulator ? 'appletvsimulator' : 'appletvos'
+ when slice_id.start_with?('watchos-') then simulator ? 'watchsimulator' : 'watchos'
+ when slice_id.start_with?('xros-') then simulator ? 'xrsimulator' : 'xros'
+ when slice_id.start_with?('macos-') then 'macosx'
+ end
+end
diff --git a/scripts/clang-format.sh b/scripts/clang-format.sh
index 6a642f4215..b9e94c77d1 100755
--- a/scripts/clang-format.sh
+++ b/scripts/clang-format.sh
@@ -64,6 +64,7 @@ cmd="find . -type f \( \
-path \"**android/build/**\" -or \
-path \"**.cxx/**\" -or \
-path \"**build/generated/**\" -or \
+ -path \"**/DerivedData/**\" -or \
-path \"**/Carthage/Checkouts/*\" -or \
-path \"**/libs/**\" -or \
-path \"**/.yalc/**\" -or \