Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 -->

## 8.18.0-alpha.3

### 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.2

### Fixes
Expand Down
119 changes: 98 additions & 21 deletions packages/core/RNSentry.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -45,41 +45,115 @@ 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.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.
#
# 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.'
# The xcframework is downloaded from sentry-cocoa's GitHub Release,
# SHA256-verified, and cached under `~/Library/Caches/sentry-react-native/
# xcframeworks/<version>/` (override with `SENTRY_XCFRAMEWORK_CACHE_DIR`).
# Slice paths are then wired into the pod and app target via
# `FRAMEWORK_SEARCH_PATHS` below. Consuming the xcframework this way
# 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).
#
# Set `SENTRY_USE_XCFRAMEWORK=0` to fall back to the source-built
# `Sentry` CocoaPod (e.g. for offline builds behind a restrictive proxy).
#
# `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')

# We deliberately do NOT set `s.vendored_frameworks` even though the
# xcframework is downloaded and referenced below. CocoaPods doesn't
# stage vendored xcframeworks for `path:` (development) pods anyway
# โ€” it did nothing for us functionally โ€” and pointing it at a path
# inside `node_modules/@sentry/react-native/ios/Vendor/` used to be
# actively harmful, because that path only existed when the SDK was
# cached into the package directory (broken under pnpm's isolated
# store / Yarn PnP where node_modules is read-only). Framework
# linking still happens: `@import Sentry;` in RNSentry.mm + user
# code triggers clang's `-fmodules-autolink`, which emits the
# `-framework Sentry` linker directive automatically once the
# module is discovered via `FRAMEWORK_SEARCH_PATHS` below.

# 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".
#
# We hardcode the slice โ†’ SDK map in `SENTRY_XCFRAMEWORK_SLICES_BY_SDK`
# (see `sentry_utils.rb`) rather than scanning the extracted bundle โ€”
# sentry-cocoa's `Sentry.xcframework` layout is stable across releases.
# Add a slice there if a future release ships one.
#
# 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.each_with_object({}) do |(sdk, slice_ids), acc|
paths = slice_ids.map { |slice| %("#{File.join(sentry_xcframework_dir, slice)}") }
acc["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 +162,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"

// 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 = ()
125 changes: 125 additions & 0 deletions packages/core/scripts/sentry_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,128 @@ 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'
require 'open-uri'

# 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.
#
# Kept in sync with `sentry_cocoa_version` in `RNSentry.podspec` by
# `scripts/update-cocoa.sh set-version <new>`, which downloads the new
# release archive, computes its SHA256, and rewrites the entry below.
SENTRY_COCOA_XCFRAMEWORK_CHECKSUMS = {
# `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.
'9.19.1' => {
'Sentry' => 'd6d545af17e49851cda2747b0f45cde78ce08ea37709dde5a956c6b4671224e8',
},
}.freeze

# Static map from xcframework slice directory name to the Xcode SDK selector
# it should be attached to. `sentry-cocoa`'s `Sentry.xcframework` layout is
# stable across releases โ€” same slice names come out of every build โ€” so we
# hardcode the mapping rather than scanning the extracted bundle. Add a
# new entry here if `sentry-cocoa` ever ships a new platform slice.
SENTRY_XCFRAMEWORK_SLICES_BY_SDK = {
'iphoneos' => %w[ios-arm64_arm64e],
'iphonesimulator' => %w[ios-arm64_x86_64-simulator],
'maccatalyst' => %w[ios-arm64_arm64e_x86_64-maccatalyst],
'macosx' => %w[macos-arm64_arm64e_x86_64],
'appletvos' => %w[tvos-arm64_arm64e],
'appletvsimulator' => %w[tvos-arm64_x86_64-simulator],
'watchos' => %w[watchos-arm64_arm64_32_arm64e_armv7k],
'watchsimulator' => %w[watchos-arm64_x86_64-simulator],
'xros' => %w[xros-arm64_arm64e],
'xrsimulator' => %w[xros-arm64_x86_64-simulator],
}.freeze

# Ensures `<cache>/<version>/<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. Returns the absolute path to the extracted
# xcframework, which callers pass to `FRAMEWORK_SEARCH_PATHS`.
#
# The cache lives under a user-writable directory (`~/Library/Caches/
# sentry-react-native/xcframeworks/` on macOS by default; override with
# `SENTRY_XCFRAMEWORK_CACHE_DIR`). Cannot live under the pod's own source
# tree because pnpm's isolated store makes `node_modules/@sentry/
# react-native/ios/` read-only, and `Yarn PnP` doesn't materialize the
# package directory at all โ€” writing there fails with `EACCES`.
# Deduplicating cache across multiple RN projects on the same machine
# is a nice side effect.
#
# 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')
cache_root = ENV['SENTRY_XCFRAMEWORK_CACHE_DIR'] ||
File.expand_path('~/Library/Caches/sentry-react-native/xcframeworks')
vendor_dir = File.join(cache_root, version)
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)
begin
URI.open(url, redirect: true) do |remote|
File.open(zip_path, 'wb') { |f| IO.copy_stream(remote, f) }
end
rescue OpenURI::HTTPError, SocketError, StandardError => e
FileUtils.rm_f(zip_path)
raise "Failed to download #{url}: #{e.class}: #{e.message}"
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

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
Loading