Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c8143aa
feat(ios): Default to consuming sentry-cocoa via Swift Package Manager
alwx Jun 30, 2026
611b091
Merge branch 'main' into alwx/feature/spm-by-default
alwx Jul 1, 2026
347198e
docs(changelog): Link SPM entry to PR and flag breaking-change escapeโ€ฆ
alwx Jul 1, 2026
a18cb61
Merge branch 'main' into alwx/feature/spm-by-default
alwx Jul 2, 2026
e06dcf2
fix(ios): Use Sentry-Dynamic SPM product to avoid archive signature cโ€ฆ
alwx Jul 2, 2026
c1b534e
Merge branch 'main' into alwx/feature/spm-by-default
alwx Jul 2, 2026
48c5743
fix(ios): Use SentrySPM source product to avoid archive signature colโ€ฆ
alwx Jul 2, 2026
9987701
Revert "fix(ios): Use SentrySPM source product to avoid archive signaโ€ฆ
alwx Jul 2, 2026
eb8a766
feat(ios): Vendor sentry-cocoa as prebuilt xcframework instead of SPM
alwx Jul 2, 2026
1ed61c6
fix(ios): Use Sentry (static) xcframework so CocoaPods resolves the mโ€ฆ
alwx Jul 2, 2026
e92872e
fix(ios): Address reviewer feedback on xcframework install
alwx Jul 2, 2026
99df7eb
fix(ios): Add explicit FRAMEWORK_SEARCH_PATHS for vendored Sentry.xcfโ€ฆ
alwx Jul 2, 2026
4a13dc4
fix(ios): Build pod_target_xcconfig locally, assign once
alwx Jul 2, 2026
fec31a0
fix(ios): Assign pod_target_xcconfig before install_modules_dependencies
alwx Jul 2, 2026
bcaf4ba
fix(ios): Enumerate Sentry.xcframework slice dirs for FRAMEWORK_SEARCโ€ฆ
alwx Jul 2, 2026
81d7a9c
fix(ios): SDK-conditional framework search paths per xcframework slice
alwx Jul 2, 2026
1bbd6f7
Merge branch 'main' into alwx/feature/spm-by-default
alwx Jul 2, 2026
69d9dbb
fix(ios): Use absolute xcframework path + Swift link stub
alwx Jul 2, 2026
f702c1b
fix(ios): Gate Swift link stub inclusion on RN >= 0.75
alwx Jul 2, 2026
830db75
Merge branch 'main' into alwx/feature/spm-by-default
alwx Jul 2, 2026
9171a88
fix: Address reviewer feedback on CHANGELOG placement and Swift stub
alwx Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions .github/workflows/sample-application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >-
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -112,15 +123,18 @@ 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 \
${{ github.workspace }}/${{ env.IOS_APP_ARCHIVE_PATH }} \
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
Expand All @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
> make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first.
<!-- prettier-ignore-end -->

## 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
Expand Down
106 changes: 85 additions & 21 deletions packages/core/RNSentry.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
cursor[bot] marked this conversation as resolved.

if use_xcframework
sentry_xcframework_dir = ensure_sentry_xcframework(sentry_cocoa_version, 'Sentry')
s.vendored_frameworks = 'ios/Vendor/Sentry.xcframework'

# Xcode's `-F <dir>` 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 <Sentry/โ€ฆ>` 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)
Expand All @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions packages/core/ios/RNSentrySwiftLinkStub.swift
Original file line number Diff line number Diff line change
@@ -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"
Comment thread
sentry[bot] marked this conversation as resolved.

// 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 = ()
132 changes: 132 additions & 0 deletions packages/core/scripts/sentry_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<product>.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',
Comment thread
sentry[bot] marked this conversation as resolved.
},
}.freeze

# Ensures `<sdk_root>/ios/Vendor/<product>.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.
Comment thread
sentry-warden[bot] marked this conversation as resolved.
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
Comment thread
sentry[bot] marked this conversation as resolved.
end

# Returns a hash of `<xcconfig SDK name> => [slice_id, ...]` for the slice
# directories inside an xcframework bundle.
#
# Xcode's `-F <dir>` 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
1 change: 1 addition & 0 deletions scripts/clang-format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
Loading