From d988c1b627b1df1c0e05b29cd09bf6a2c820f137 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Tue, 30 Jun 2026 15:05:13 +0200 Subject: [PATCH 01/18] feat(ios): Default to consuming sentry-cocoa via Swift Package Manager On React Native >= 0.75, RNSentry now pulls Sentry as a pre-built binary xcframework through the SPM helper exposed by `react_native_pods.rb`, instead of building Sentry from source as a CocoaPod. On older RN versions the SPM helper is unavailable and RNSentry transparently falls back to the CocoaPods source build. Override the choice with `SENTRY_USE_SPM=0` (force CocoaPods) or `SENTRY_USE_SPM=1` (require SPM; errors if the helper is unavailable). Sample-application CI gets one extra `sentry-consumption: cocoapods` job on the production iOS build to keep the fallback path covered. Clang-format script gets a `DerivedData/**` exclusion so the SPM-fetched xcframework headers don't appear in the lint scope. Refs: #5780 Co-Authored-By: Claude Opus 4.7 --- .github/workflows/sample-application.yml | 20 +++++++++++--- CHANGELOG.md | 3 +++ packages/core/RNSentry.podspec | 34 +++++++++++++++--------- packages/core/scripts/sentry_utils.rb | 9 +++++++ scripts/clang-format.sh | 1 + 5 files changed, 51 insertions(+), 16 deletions(-) diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 13082b8df0..75138817ef 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,15 @@ jobs: rn-architecture: ["new"] ios-use-frameworks: ["no-frameworks"] build-type: ["dev", "production"] + sentry-consumption: ["spm"] + # SPM is the default consumption path for sentry-cocoa on RN >= 0.75. + # Keep a single CocoaPods job to catch regressions in the fallback path + # used by older React Native versions. + 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 +104,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_SPM=0 ./scripts/pod-install.sh @@ -112,7 +122,9 @@ 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 SPM 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 == 'spm' }} working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} run: | zip -r \ @@ -120,7 +132,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 == 'spm' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: sample-rn-${{ matrix.rn-architecture }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-ios @@ -131,7 +143,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/CHANGELOG.md b/CHANGELOG.md index 5743ebc7fe..4ef1440b13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,9 @@ ### Changes - Default `mobileReplayIntegration({ networkCaptureBodies })` to `true`, matching the iOS and Android native SDK defaults ([#6372](https://github.com/getsentry/sentry-react-native/pull/6372)) +- Consume `sentry-cocoa` via Swift Package Manager by default on React Native >= 0.75 ([#5780](https://github.com/getsentry/sentry-react-native/issues/5780)) + + On RN 0.75+, `pod install` now pulls Sentry as a pre-built binary xcframework through the SPM helper exposed by `react_native_pods.rb`, instead of building Sentry from source as a CocoaPod. On older React Native versions the SPM helper is unavailable and RNSentry transparently falls back to the CocoaPods source build. Override with `SENTRY_USE_SPM=0` to force CocoaPods, or `SENTRY_USE_SPM=1` to require SPM. ### Fixes diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 84189b7df6..08e0fe43db 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -56,20 +56,30 @@ Pod::Spec.new do |s| 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 via Swift Package Manager 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) + # On React Native >= 0.75, RNSentry pulls `Sentry` from the sentry-cocoa SPM + # package (a pre-built binary xcframework). On older RN, the SPM helper + # (`react-native/scripts/cocoapods/spm.rb`) is not available and we + # transparently fall back to the Sentry CocoaPods source build. + # + # Override the choice with the `SENTRY_USE_SPM` environment variable: + # * `SENTRY_USE_SPM=0` — force CocoaPods consumption even on RN >= 0.75. + # * `SENTRY_USE_SPM=1` — force SPM (errors if the helper is unavailable). + spm_helper_available = defined?(SPM) && SPM.respond_to?(:dependency) + env_use_spm = ENV['SENTRY_USE_SPM'] + use_spm = case env_use_spm + when '1' then true + when '0' then false + else supports_spm(rn_version) && spm_helper_available + end + + if use_spm + unless spm_helper_available 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.' + 'This requires React Native >= 0.75 and a Podfile that loads ' \ + '`react_native_pods.rb`. Either upgrade React Native, or set ' \ + 'SENTRY_USE_SPM=0 to fall back to the Sentry CocoaPods build.' end SPM.dependency(s, url: 'https://github.com/getsentry/sentry-cocoa', diff --git a/packages/core/scripts/sentry_utils.rb b/packages/core/scripts/sentry_utils.rb index 5dc57a3b52..36d1bc0da0 100644 --- a/packages/core/scripts/sentry_utils.rb +++ b/packages/core/scripts/sentry_utils.rb @@ -40,3 +40,12 @@ 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 + +# React Native >= 0.75 transitively loads `react-native/scripts/cocoapods/spm.rb` +# via `react_native_pods.rb`, which exposes the `SPM` helper that the RNSentry +# podspec uses to pull `sentry-cocoa` as a Swift Package binary xcframework. +# Below 0.75 the helper is unavailable and we fall back to consuming Sentry as +# a regular CocoaPods source build. +def supports_spm(rn_version) + return (rn_version[:major] >= 1 || (rn_version[:major] == 0 && rn_version[:minor] >= 75)) +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 \ From 9f862f621d618386e4624152258306e62579b997 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Wed, 1 Jul 2026 17:09:32 +0200 Subject: [PATCH 02/18] docs(changelog): Link SPM entry to PR and flag breaking-change escape hatch Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef1440b13..7658b2bd21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,9 +48,11 @@ ### Changes - Default `mobileReplayIntegration({ networkCaptureBodies })` to `true`, matching the iOS and Android native SDK defaults ([#6372](https://github.com/getsentry/sentry-react-native/pull/6372)) -- Consume `sentry-cocoa` via Swift Package Manager by default on React Native >= 0.75 ([#5780](https://github.com/getsentry/sentry-react-native/issues/5780)) +- Consume `sentry-cocoa` via Swift Package Manager by default on React Native >= 0.75 ([#6381](https://github.com/getsentry/sentry-react-native/pull/6381)) - On RN 0.75+, `pod install` now pulls Sentry as a pre-built binary xcframework through the SPM helper exposed by `react_native_pods.rb`, instead of building Sentry from source as a CocoaPod. On older React Native versions the SPM helper is unavailable and RNSentry transparently falls back to the CocoaPods source build. Override with `SENTRY_USE_SPM=0` to force CocoaPods, or `SENTRY_USE_SPM=1` to require SPM. + **Warning** + + **This may be a breaking change for some setups.** On RN 0.75+, `pod install` now pulls Sentry as a pre-built binary xcframework, 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), set `SENTRY_USE_SPM=0` before `pod install` to restore the previous behavior. On older React Native versions the SPM helper is unavailable and RNSentry transparently falls back to the CocoaPods source build. Use `SENTRY_USE_SPM=1` to require SPM. ### Fixes From cbe639ac03444f7caee627572e9087f23a22a279 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 11:11:26 +0200 Subject: [PATCH 03/18] fix(ios): Use Sentry-Dynamic SPM product to avoid archive signature collision The static `Sentry` product ships a signed xcframework whose `Signatures/Sentry.xcframework-ios.signature` file collides with an existing entry during `xcodebuild archive` on Xcode 16/26, failing the archive step. Switching to `Sentry-Dynamic` (dynamic xcframework) avoids the copy path that produces the duplicate. Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 08e0fe43db..2fbaa05cbe 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -81,10 +81,16 @@ Pod::Spec.new do |s| '`react_native_pods.rb`. Either upgrade React Native, or set ' \ 'SENTRY_USE_SPM=0 to fall back to the Sentry CocoaPods build.' end + # Use the `Sentry-Dynamic` SPM product (dynamic xcframework). The static + # `Sentry` product ships a signed xcframework whose `Signatures/` entry + # collides during `xcodebuild archive` on Xcode 16/26, failing with + # `"Sentry.xcframework-ios.signature" couldn't be copied to "Signatures" + # because an item with the same name already exists`. The dynamic product + # avoids the copy path that triggers the collision. SPM.dependency(s, url: 'https://github.com/getsentry/sentry-cocoa', requirement: { kind: 'exactVersion', version: sentry_cocoa_version }, - products: ['Sentry'] + products: ['Sentry-Dynamic'] ) else s.dependency 'Sentry', sentry_cocoa_version From d99d143e70d3fa645d24d04ac3b3b4c22d8a33e0 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 11:43:35 +0200 Subject: [PATCH 04/18] fix(ios): Use SentrySPM source product to avoid archive signature collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The binary SPM products (`Sentry`, `Sentry-Dynamic`, ...) ship signed xcframeworks whose `Signatures/*.signature` file collides during `xcodebuild archive` on Xcode 16/26. Switching to the `SentrySPM` source-compile product (backed by the `SentryObjCInternal` SPM target) avoids the xcframework path entirely — SPM compiles sentry-cocoa from source, so there is no `Signatures/` directory to duplicate. Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 2fbaa05cbe..38c51c915e 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -81,16 +81,17 @@ Pod::Spec.new do |s| '`react_native_pods.rb`. Either upgrade React Native, or set ' \ 'SENTRY_USE_SPM=0 to fall back to the Sentry CocoaPods build.' end - # Use the `Sentry-Dynamic` SPM product (dynamic xcframework). The static - # `Sentry` product ships a signed xcframework whose `Signatures/` entry - # collides during `xcodebuild archive` on Xcode 16/26, failing with - # `"Sentry.xcframework-ios.signature" couldn't be copied to "Signatures" - # because an item with the same name already exists`. The dynamic product - # avoids the copy path that triggers the collision. + # Use the `SentrySPM` source-compile product. sentry-cocoa's binary + # products (`Sentry`, `Sentry-Dynamic`, ...) ship signed xcframeworks whose + # `Signatures/*.signature` file collides during `xcodebuild archive` on + # Xcode 16/26, failing with `"…-ios.signature" couldn't be copied to + # "Signatures" because an item with the same name already exists`. + # `SentrySPM` is compiled from source (no xcframework, no `Signatures/`), + # which sidesteps the archive collision entirely. SPM.dependency(s, url: 'https://github.com/getsentry/sentry-cocoa', requirement: { kind: 'exactVersion', version: sentry_cocoa_version }, - products: ['Sentry-Dynamic'] + products: ['SentrySPM'] ) else s.dependency 'Sentry', sentry_cocoa_version From f1aa3a37cae085333504a41cccd2b4a8b44b4048 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 11:52:45 +0200 Subject: [PATCH 05/18] Revert "fix(ios): Use SentrySPM source product to avoid archive signature collision" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SentrySPM` doesn't expose the `Sentry` module the way the binary xcframework product does — RNSentry.mm fails to resolve `#import `, breaking every iOS build. Back to `Sentry-Dynamic` (archive-only failure) while we pick a real fix. Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 38c51c915e..2fbaa05cbe 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -81,17 +81,16 @@ Pod::Spec.new do |s| '`react_native_pods.rb`. Either upgrade React Native, or set ' \ 'SENTRY_USE_SPM=0 to fall back to the Sentry CocoaPods build.' end - # Use the `SentrySPM` source-compile product. sentry-cocoa's binary - # products (`Sentry`, `Sentry-Dynamic`, ...) ship signed xcframeworks whose - # `Signatures/*.signature` file collides during `xcodebuild archive` on - # Xcode 16/26, failing with `"…-ios.signature" couldn't be copied to - # "Signatures" because an item with the same name already exists`. - # `SentrySPM` is compiled from source (no xcframework, no `Signatures/`), - # which sidesteps the archive collision entirely. + # Use the `Sentry-Dynamic` SPM product (dynamic xcframework). The static + # `Sentry` product ships a signed xcframework whose `Signatures/` entry + # collides during `xcodebuild archive` on Xcode 16/26, failing with + # `"Sentry.xcframework-ios.signature" couldn't be copied to "Signatures" + # because an item with the same name already exists`. The dynamic product + # avoids the copy path that triggers the collision. SPM.dependency(s, url: 'https://github.com/getsentry/sentry-cocoa', requirement: { kind: 'exactVersion', version: sentry_cocoa_version }, - products: ['SentrySPM'] + products: ['Sentry-Dynamic'] ) else s.dependency 'Sentry', sentry_cocoa_version From d1a7e8b3dcb24727bb320c41f2ecc852e173362c Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 12:00:06 +0200 Subject: [PATCH 06/18] feat(ios): Vendor sentry-cocoa as prebuilt xcframework instead of SPM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consuming sentry-cocoa through Xcode's SPM integration triggers a signature-copy collision during `xcodebuild archive` on Xcode 16/26: xcodebuild: error: "Sentry-Dynamic.xcframework-ios.signature" couldn't be copied to "Signatures" because an item with the same name already exists.: File exists The collision is specific to Xcode's SPM archive pipeline. Consuming the same prebuilt xcframework through CocoaPods' `vendored_frameworks` avoids that pipeline entirely — the archive step goes through the regular CocoaPods embed path, which does not produce the duplicate signature copy. RNSentry.podspec now downloads `Sentry-Dynamic.xcframework.zip` from sentry-cocoa's GitHub release on first `pod install`, verifies the SHA256 checksum against a version-pinned table in `sentry_utils.rb`, and vendors the extracted xcframework via `s.vendored_frameworks`. Subsequent `pod install` runs reuse the cached copy under `packages/core/ios/Vendor/`. Set `SENTRY_USE_XCFRAMEWORK=0` to fall back to the source-built `Sentry` CocoaPod (e.g. for offline builds behind a restrictive proxy). Co-Authored-By: Claude Opus 4.7 --- .github/workflows/sample-application.yml | 20 ++++---- .gitignore | 3 ++ CHANGELOG.md | 4 +- packages/core/RNSentry.podspec | 53 +++++++------------ packages/core/scripts/sentry_utils.rb | 65 +++++++++++++++++++++--- 5 files changed, 93 insertions(+), 52 deletions(-) diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 75138817ef..efb32a6d89 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -60,10 +60,11 @@ jobs: rn-architecture: ["new"] ios-use-frameworks: ["no-frameworks"] build-type: ["dev", "production"] - sentry-consumption: ["spm"] - # SPM is the default consumption path for sentry-cocoa on RN >= 0.75. - # Keep a single CocoaPods job to catch regressions in the fallback path - # used by older React Native versions. + 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" @@ -104,7 +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_SPM=0 + [[ "${{ matrix.sentry-consumption }}" == "cocoapods" ]] && export SENTRY_USE_XCFRAMEWORK=0 ./scripts/pod-install.sh @@ -122,9 +123,10 @@ jobs: ./scripts/build-ios.sh - name: Archive iOS App - # Only upload from the SPM 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 == 'spm' }} + # 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 \ @@ -132,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' && matrix.sentry-consumption == 'spm' }} + 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 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 7658b2bd21..7185523bbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,11 +48,11 @@ ### Changes - Default `mobileReplayIntegration({ networkCaptureBodies })` to `true`, matching the iOS and Android native SDK defaults ([#6372](https://github.com/getsentry/sentry-react-native/pull/6372)) -- Consume `sentry-cocoa` via Swift Package Manager by default on React Native >= 0.75 ([#6381](https://github.com/getsentry/sentry-react-native/pull/6381)) +- 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.** On RN 0.75+, `pod install` now pulls Sentry as a pre-built binary xcframework, 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), set `SENTRY_USE_SPM=0` before `pod install` to restore the previous behavior. On older React Native versions the SPM helper is unavailable and RNSentry transparently falls back to the CocoaPods source build. Use `SENTRY_USE_SPM=1` to require SPM. + **This may be a breaking change for some setups.** `pod install` now downloads `Sentry-Dynamic.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. ### Fixes diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 2fbaa05cbe..75cf5f0d32 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -46,6 +46,7 @@ Pod::Spec.new do |s| # 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}' + 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 @@ -56,42 +57,26 @@ Pod::Spec.new do |s| sentry_cocoa_version = '9.19.1' - # Consume sentry-cocoa via Swift Package Manager by default. + # Consume sentry-cocoa as a prebuilt `Sentry-Dynamic.xcframework` by default. # - # On React Native >= 0.75, RNSentry pulls `Sentry` from the sentry-cocoa SPM - # package (a pre-built binary xcframework). On older RN, the SPM helper - # (`react-native/scripts/cocoapods/spm.rb`) is not available and we - # transparently fall back to the Sentry CocoaPods source build. + # The xcframework is downloaded from sentry-cocoa's GitHub Release, + # SHA256-verified, and cached under `ios/Vendor/`. CocoaPods then embeds 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 it is consumed through Xcode's SPM + # integration (`Signatures/*.signature` collision during archive). # - # Override the choice with the `SENTRY_USE_SPM` environment variable: - # * `SENTRY_USE_SPM=0` — force CocoaPods consumption even on RN >= 0.75. - # * `SENTRY_USE_SPM=1` — force SPM (errors if the helper is unavailable). - spm_helper_available = defined?(SPM) && SPM.respond_to?(:dependency) - env_use_spm = ENV['SENTRY_USE_SPM'] - use_spm = case env_use_spm - when '1' then true - when '0' then false - else supports_spm(rn_version) && spm_helper_available - end - - if use_spm - unless spm_helper_available - raise 'SENTRY_USE_SPM=1 is set but the SPM helper is not loaded. ' \ - 'This requires React Native >= 0.75 and a Podfile that loads ' \ - '`react_native_pods.rb`. Either upgrade React Native, or set ' \ - 'SENTRY_USE_SPM=0 to fall back to the Sentry CocoaPods build.' - end - # Use the `Sentry-Dynamic` SPM product (dynamic xcframework). The static - # `Sentry` product ships a signed xcframework whose `Signatures/` entry - # collides during `xcodebuild archive` on Xcode 16/26, failing with - # `"Sentry.xcframework-ios.signature" couldn't be copied to "Signatures" - # because an item with the same name already exists`. The dynamic product - # avoids the copy path that triggers the collision. - SPM.dependency(s, - url: 'https://github.com/getsentry/sentry-cocoa', - requirement: { kind: 'exactVersion', version: sentry_cocoa_version }, - products: ['Sentry-Dynamic'] - ) + # Set `SENTRY_USE_XCFRAMEWORK=0` to fall back to the source-built + # `Sentry` CocoaPod (e.g. for offline builds behind a restrictive proxy). + env_use_xcframework = ENV['SENTRY_USE_XCFRAMEWORK'] + use_xcframework = case env_use_xcframework + when '0' then false + else true + end + + if use_xcframework + ensure_sentry_xcframework(sentry_cocoa_version, 'Sentry-Dynamic') + s.vendored_frameworks = 'ios/Vendor/Sentry-Dynamic.xcframework' else s.dependency 'Sentry', sentry_cocoa_version end diff --git a/packages/core/scripts/sentry_utils.rb b/packages/core/scripts/sentry_utils.rb index 36d1bc0da0..ec165f2c56 100644 --- a/packages/core/scripts/sentry_utils.rb +++ b/packages/core/scripts/sentry_utils.rb @@ -41,11 +41,62 @@ def is_new_hermes_runtime(rn_version) return (rn_version[:major] >= 1 || (rn_version[:major] == 0 && rn_version[:minor] >= 81)) end -# React Native >= 0.75 transitively loads `react-native/scripts/cocoapods/spm.rb` -# via `react_native_pods.rb`, which exposes the `SPM` helper that the RNSentry -# podspec uses to pull `sentry-cocoa` as a Swift Package binary xcframework. -# Below 0.75 the helper is unavailable and we fall back to consuming Sentry as -# a regular CocoaPods source build. -def supports_spm(rn_version) - return (rn_version[:major] >= 1 || (rn_version[:major] == 0 && rn_version[:minor] >= 75)) +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-Dynamic' => '7d0fb876a35b40ef942d36cd43dcab0ee16d2874d5cc7cc668e8e01e0c83db2a', + }, +}.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-Dynamic') + vendor_dir = File.expand_path('../ios/Vendor', __dir__) + target_dir = File.join(vendor_dir, "#{product}.xcframework") + return target_dir if File.directory?(target_dir) + + 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 + + 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) + + target_dir end From f8e312ffd2620a5e6add9ba7fb24e44f441e9263 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 12:08:05 +0200 Subject: [PATCH 07/18] fix(ios): Use Sentry (static) xcframework so CocoaPods resolves the module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sentry-Dynamic.xcframework contains a framework named `Sentry.framework` inside, so `@import Sentry;` needs `-framework Sentry`. CocoaPods derives the `-framework` flag from the xcframework's own filename, so with `Sentry-Dynamic.xcframework` it generates `-framework Sentry-Dynamic` and RNSentry fails to compile with `Module 'Sentry' not found`. Switch to `Sentry.xcframework` (the static product) — enclosing name matches the framework name inside, so CocoaPods generates the correct `-framework Sentry` and the module resolves. Static also means no embed step, so no chance of the archive-signature collision either. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 2 +- packages/core/RNSentry.podspec | 13 +++++++------ packages/core/scripts/sentry_utils.rb | 10 ++++++++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7185523bbd..329f76073d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,7 @@ **Warning** - **This may be a breaking change for some setups.** `pod install` now downloads `Sentry-Dynamic.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. + **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. ### Fixes diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 75cf5f0d32..70f4ccea7c 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -57,14 +57,15 @@ Pod::Spec.new do |s| sentry_cocoa_version = '9.19.1' - # Consume sentry-cocoa as a prebuilt `Sentry-Dynamic.xcframework` by default. + # 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 embeds it + # 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 it is consumed through Xcode's SPM - # integration (`Signatures/*.signature` collision during archive). + # 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). @@ -75,8 +76,8 @@ Pod::Spec.new do |s| end if use_xcframework - ensure_sentry_xcframework(sentry_cocoa_version, 'Sentry-Dynamic') - s.vendored_frameworks = 'ios/Vendor/Sentry-Dynamic.xcframework' + ensure_sentry_xcframework(sentry_cocoa_version, 'Sentry') + s.vendored_frameworks = 'ios/Vendor/Sentry.xcframework' else s.dependency 'Sentry', sentry_cocoa_version end diff --git a/packages/core/scripts/sentry_utils.rb b/packages/core/scripts/sentry_utils.rb index ec165f2c56..ffff963032 100644 --- a/packages/core/scripts/sentry_utils.rb +++ b/packages/core/scripts/sentry_utils.rb @@ -50,7 +50,13 @@ def is_new_hermes_runtime(rn_version) # sentry-cocoa version we ship a prebuilt xcframework for. SENTRY_COCOA_XCFRAMEWORK_CHECKSUMS = { '9.19.1' => { - 'Sentry-Dynamic' => '7d0fb876a35b40ef942d36cd43dcab0ee16d2874d5cc7cc668e8e01e0c83db2a', + # `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 @@ -64,7 +70,7 @@ def is_new_hermes_runtime(rn_version) # 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-Dynamic') +def ensure_sentry_xcframework(version, product = 'Sentry') vendor_dir = File.expand_path('../ios/Vendor', __dir__) target_dir = File.join(vendor_dir, "#{product}.xcframework") return target_dir if File.directory?(target_dir) From b644e2e2762aa95425e1e42bf3fc3477b4ef014a Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 12:10:17 +0200 Subject: [PATCH 08/18] fix(ios): Address reviewer feedback on xcframework install - Sentinel-check `Sentry.xcframework/Info.plist` instead of directory existence so an interrupted `unzip` doesn't leave a partial tree that short-circuits future re-downloads (cursor-bot low). - `rm_rf(target_dir)` before extracting so a stale partial tree from an earlier failure doesn't survive the next attempt. - Verify `Info.plist` after extraction so a release with a changed archive layout fails fast with a clear message instead of silently succeeding and later confusing `pod install` (sentry-bot medium). - Honor `SENTRY_USE_SPM=0` as a deprecated alias for the CocoaPods fallback so CI/local envs still exporting the old draft variable don't silently opt into the new xcframework path (cursor-bot medium). Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 8 ++++++++ packages/core/scripts/sentry_utils.rb | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 70f4ccea7c..f088c25306 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -69,7 +69,15 @@ Pod::Spec.new do |s| # # 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 diff --git a/packages/core/scripts/sentry_utils.rb b/packages/core/scripts/sentry_utils.rb index ffff963032..6f9d386a41 100644 --- a/packages/core/scripts/sentry_utils.rb +++ b/packages/core/scripts/sentry_utils.rb @@ -73,7 +73,12 @@ def is_new_hermes_runtime(rn_version) def ensure_sentry_xcframework(version, product = 'Sentry') vendor_dir = File.expand_path('../ios/Vendor', __dir__) target_dir = File.join(vendor_dir, "#{product}.xcframework") - return target_dir if File.directory?(target_dir) + # 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 @@ -82,6 +87,9 @@ def ensure_sentry_xcframework(version, product = 'Sentry') "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/" \ @@ -104,5 +112,15 @@ def ensure_sentry_xcframework(version, product = 'Sentry') 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 From deccc6a81d777f9f8a5fe113238d942dd8bf99fa Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 13:25:04 +0200 Subject: [PATCH 09/18] fix(ios): Add explicit FRAMEWORK_SEARCH_PATHS for vendored Sentry.xcframework Even with `s.vendored_frameworks = 'ios/Vendor/Sentry.xcframework'`, CocoaPods didn't propagate a framework search path into RNSentry's own compile phase, so `#import ` failed with "file not found". Set `FRAMEWORK_SEARCH_PATHS` explicitly via `pod_target_xcconfig` so the pod can resolve the `Sentry` framework module from the vendored xcframework's parent directory. Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index f088c25306..bdc5bd6e43 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -86,6 +86,14 @@ Pod::Spec.new do |s| if use_xcframework ensure_sentry_xcframework(sentry_cocoa_version, 'Sentry') s.vendored_frameworks = 'ios/Vendor/Sentry.xcframework' + # `s.vendored_frameworks` alone doesn't always propagate a framework + # search path to the pod's own compile phase for a vendored xcframework, + # so RNSentry.mm fails to resolve `#import `. Force the search + # path via pod_target_xcconfig — `${PODS_TARGET_SRCROOT}` resolves to + # the pod's source directory (i.e. `packages/core/`) at build time. + s.pod_target_xcconfig.merge!( + 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_TARGET_SRCROOT}/ios/Vendor"' + ) else s.dependency 'Sentry', sentry_cocoa_version end From e63e3945309c7aae60f6201702e230dbf5225bb6 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 13:30:44 +0200 Subject: [PATCH 10/18] fix(ios): Build pod_target_xcconfig locally, assign once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `s.pod_target_xcconfig` is setter-only in the CocoaPods version used by RN 0.86, so calling `s.pod_target_xcconfig.merge!(…)` crashed pod install with `undefined method 'pod_target_xcconfig' for Pod::Specification`. Accumulate the settings into a local hash across the conditional branches and assign to `s.pod_target_xcconfig` once at the end. Same fix applied to the pre-existing new-arch/RN<0.71 branch (which had the same latent bug). Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index bdc5bd6e43..5da78cd810 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -51,7 +51,7 @@ Pod::Spec.new do |s| s.compiler_flags = other_cflags - s.pod_target_xcconfig = { + pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } @@ -88,12 +88,11 @@ Pod::Spec.new do |s| s.vendored_frameworks = 'ios/Vendor/Sentry.xcframework' # `s.vendored_frameworks` alone doesn't always propagate a framework # search path to the pod's own compile phase for a vendored xcframework, - # so RNSentry.mm fails to resolve `#import `. Force the search - # path via pod_target_xcconfig — `${PODS_TARGET_SRCROOT}` resolves to - # the pod's source directory (i.e. `packages/core/`) at build time. - s.pod_target_xcconfig.merge!( - 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "${PODS_TARGET_SRCROOT}/ios/Vendor"' - ) + # so RNSentry.mm fails to resolve `#import `. Add the search + # path explicitly — `${PODS_TARGET_SRCROOT}` resolves to the pod's + # source directory (i.e. `packages/core/`) at build time. + pod_target_xcconfig['FRAMEWORK_SEARCH_PATHS'] = + '$(inherited) "${PODS_TARGET_SRCROOT}/ios/Vendor"' else s.dependency 'Sentry', sentry_cocoa_version end @@ -106,7 +105,7 @@ 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" }) @@ -124,4 +123,6 @@ Pod::Spec.new do |s| s.dependency 'React-hermes' s.dependency 'hermes-engine' end + + s.pod_target_xcconfig = pod_target_xcconfig end From fe92fc86877d8fbbb35f8d7b7a12e8e9a640f564 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 13:41:02 +0200 Subject: [PATCH 11/18] fix(ios): Assign pod_target_xcconfig before install_modules_dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assigning `s.pod_target_xcconfig` at the end of the podspec (after `install_modules_dependencies`) clobbered every setting RN's helper had written, so the RN-provided HEADER_SEARCH_PATHS were dropped and every `#import ` (as well as any other framework header lookup) failed during RNSentry's compile phase. Assign the base `pod_target_xcconfig` (with DEFINES_MODULE and the Sentry FRAMEWORK_SEARCH_PATHS) BEFORE `install_modules_dependencies` so it merges its RN settings on top rather than the other way around. For the RN < 0.71 branch — where `install_modules_dependencies` doesn't exist — the local hash is mutated after that assignment, so re-assign inside that branch. Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 5da78cd810..86b407bdd3 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -97,6 +97,11 @@ Pod::Spec.new do |s| 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) @@ -109,6 +114,9 @@ Pod::Spec.new do |s| "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" @@ -123,6 +131,4 @@ Pod::Spec.new do |s| s.dependency 'React-hermes' s.dependency 'hermes-engine' end - - s.pod_target_xcconfig = pod_target_xcconfig end From aa94ec9637841ba7d787bcd9a4cc80b6f0896eb6 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 14:06:12 +0200 Subject: [PATCH 12/18] fix(ios): Enumerate Sentry.xcframework slice dirs for FRAMEWORK_SEARCH_PATHS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed by local repro: Xcode's `-F ` does NOT descend into an `.xcframework` bundle at search-path lookup time. Given `FRAMEWORK_SEARCH_PATHS = ".../ios/Vendor"`, Xcode only sees `Sentry.xcframework` as an opaque directory and never finds `Sentry.framework` inside — so `#import ` fails with `Module 'Sentry' not found`. Instead of a single parent-of-xcframework path, enumerate every slice directory inside the extracted xcframework (via `Dir.children` — no plist gem needed) and register each as a framework search path. Xcode then picks the matching slice based on the target platform/arch. The list is computed at pod-install time, so future sentry-cocoa releases can add new slices without a code change. Apply the same paths to `s.user_target_xcconfig` so the host app target (any user code writing `#import ` directly) also resolves the module. Verified locally: RNSentry pod and the sample app both compile — only the RN bundle script (unrelated to Sentry) blocks the local build. Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 27 ++++++++++++++++++++------- packages/core/scripts/sentry_utils.rb | 15 +++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 86b407bdd3..576f10c3db 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -84,15 +84,28 @@ Pod::Spec.new do |s| end if use_xcframework - ensure_sentry_xcframework(sentry_cocoa_version, 'Sentry') + sentry_xcframework_dir = ensure_sentry_xcframework(sentry_cocoa_version, 'Sentry') s.vendored_frameworks = 'ios/Vendor/Sentry.xcframework' - # `s.vendored_frameworks` alone doesn't always propagate a framework - # search path to the pod's own compile phase for a vendored xcframework, - # so RNSentry.mm fails to resolve `#import `. Add the search - # path explicitly — `${PODS_TARGET_SRCROOT}` resolves to the pod's - # source directory (i.e. `packages/core/`) at build time. + + # Xcode's `-F ` doesn't descend into `.xcframework` bundles — it + # looks for `Sentry.framework` directly at the given path. Point search + # paths at every slice inside the xcframework so `#import ` + # resolves for whichever platform/arch Xcode ends up building. The + # slice list is enumerated at pod-install time from the extracted + # xcframework, so newly-added slices in future sentry-cocoa releases + # (e.g. new visionOS variants) come along for free. + # + # `pod_target_xcconfig` covers RNSentry's own compile phase; + # `user_target_xcconfig` covers the host app target (any `#import + # ` written directly in user code). + slice_paths = sentry_xcframework_slice_ids(sentry_xcframework_dir).map do |slice| + %("${PODS_TARGET_SRCROOT}/ios/Vendor/Sentry.xcframework/#{slice}") + end pod_target_xcconfig['FRAMEWORK_SEARCH_PATHS'] = - '$(inherited) "${PODS_TARGET_SRCROOT}/ios/Vendor"' + (['$(inherited)'] + slice_paths).join(' ') + s.user_target_xcconfig = { + 'FRAMEWORK_SEARCH_PATHS' => pod_target_xcconfig['FRAMEWORK_SEARCH_PATHS'], + } else s.dependency 'Sentry', sentry_cocoa_version end diff --git a/packages/core/scripts/sentry_utils.rb b/packages/core/scripts/sentry_utils.rb index 6f9d386a41..707ed21676 100644 --- a/packages/core/scripts/sentry_utils.rb +++ b/packages/core/scripts/sentry_utils.rb @@ -124,3 +124,18 @@ def ensure_sentry_xcframework(version, product = 'Sentry') target_dir end + +# Returns the list of slice directory names inside an xcframework bundle +# (e.g. `ios-arm64`, `ios-arm64_x86_64-simulator`). +# +# 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 this list to +# enumerate every slice directory as a separate framework search path so +# `#import ` resolves for whichever platform/arch Xcode is +# building for. +def sentry_xcframework_slice_ids(xcframework_dir) + Dir.children(xcframework_dir).select do |name| + File.directory?(File.join(xcframework_dir, name)) && name != '_CodeSignature' + end.sort +end From d100f78566bdf5b190c3e8dac8019c23dfcdec91 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 15:44:21 +0200 Subject: [PATCH 13/18] fix(ios): SDK-conditional framework search paths per xcframework slice Two problems with the previous flat-slice-list approach: 1. Xcode's Swift module precompiler walked slices that don't match the current build target (e.g. watchos-* on an iOS build) and errored with 'unsupported Swift architecture'. Fix: split slices by target SDK and emit conditional FRAMEWORK_SEARCH_PATHS[sdk=...*] entries so each SDK build only sees the slices it can actually consume. 2. PODS_TARGET_SRCROOT is defined in a pod's own xcconfig but not in the aggregate/user-target xcconfig. Using it in user_target_xcconfig made the app-target search paths resolve to nothing, so app code writing @import Sentry failed with 'Module Sentry not found'. Use PODS_ROOT/../.. for the user-target xcconfig - from the app's Pods dir that's the RN app root where node_modules/@sentry/react-native/ lives. Slice-to-SDK mapping is computed at pod-install time from the extracted xcframework so future sentry-cocoa releases pick up new slices automatically. Verified locally: sample's SentryNativeInitializer.m now resolves @import Sentry. Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 48 +++++++++++++++++--------- packages/core/scripts/sentry_utils.rb | 49 ++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 576f10c3db..e73d7c12e8 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -88,24 +88,40 @@ Pod::Spec.new do |s| 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 search - # paths at every slice inside the xcframework so `#import ` - # resolves for whichever platform/arch Xcode ends up building. The - # slice list is enumerated at pod-install time from the extracted - # xcframework, so newly-added slices in future sentry-cocoa releases - # (e.g. new visionOS variants) come along for free. + # 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. # - # `pod_target_xcconfig` covers RNSentry's own compile phase; - # `user_target_xcconfig` covers the host app target (any `#import - # ` written directly in user code). - slice_paths = sentry_xcframework_slice_ids(sentry_xcframework_dir).map do |slice| - %("${PODS_TARGET_SRCROOT}/ios/Vendor/Sentry.xcframework/#{slice}") + # `pod_target_xcconfig` covers RNSentry's own compile phase — inside + # a pod's xcconfig, `${PODS_TARGET_SRCROOT}` resolves to the pod + # source directory (`packages/core/` here). `user_target_xcconfig` + # covers the host app target so app code writing `#import ` + # (e.g. `SentryNativeInitializer.m`) also finds the framework. The + # aggregate/user xcconfig doesn't define `PODS_TARGET_SRCROOT`, so + # for that target we resolve the path via `${PODS_ROOT}/..` + + # `node_modules/@sentry/react-native`, which is where RN's + # auto-linking places the SDK. + pod_search_paths = {} + user_search_paths = {} + sentry_xcframework_slices_by_sdk(sentry_xcframework_dir).each do |sdk, slice_ids| + pod_paths = slice_ids.map do |slice| + %("${PODS_TARGET_SRCROOT}/ios/Vendor/Sentry.xcframework/#{slice}") + end + user_paths = slice_ids.map do |slice| + %("${PODS_ROOT}/../../node_modules/@sentry/react-native/ios/Vendor/Sentry.xcframework/#{slice}") + end + pod_search_paths["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = + (['$(inherited)'] + pod_paths).join(' ') + user_search_paths["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = + (['$(inherited)'] + user_paths).join(' ') end - pod_target_xcconfig['FRAMEWORK_SEARCH_PATHS'] = - (['$(inherited)'] + slice_paths).join(' ') - s.user_target_xcconfig = { - 'FRAMEWORK_SEARCH_PATHS' => pod_target_xcconfig['FRAMEWORK_SEARCH_PATHS'], - } + + pod_target_xcconfig.merge!(pod_search_paths) + s.user_target_xcconfig = user_search_paths else s.dependency 'Sentry', sentry_cocoa_version end diff --git a/packages/core/scripts/sentry_utils.rb b/packages/core/scripts/sentry_utils.rb index 707ed21676..fb2e3cbe4c 100644 --- a/packages/core/scripts/sentry_utils.rb +++ b/packages/core/scripts/sentry_utils.rb @@ -125,17 +125,50 @@ def ensure_sentry_xcframework(version, product = 'Sentry') target_dir end -# Returns the list of slice directory names inside an xcframework bundle -# (e.g. `ios-arm64`, `ios-arm64_x86_64-simulator`). +# 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 this list to -# enumerate every slice directory as a separate framework search path so -# `#import ` resolves for whichever platform/arch Xcode is -# building for. -def sentry_xcframework_slice_ids(xcframework_dir) - Dir.children(xcframework_dir).select do |name| +# 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 From f78fc8596d566d7810327e1877347380fe2ca92f Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 16:04:46 +0200 Subject: [PATCH 14/18] fix(ios): Use absolute xcframework path + Swift link stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-up fixes for the vendored xcframework path: 1. RNSentryCocoaTester (in packages/core/RNSentryCocoaTester/) had its test target fail with 'Sentry/Sentry.h' file not found because its Podfile layout is different from the RN sample apps — the previous ${PODS_ROOT}/../.. path assumed a RN app root with node_modules, which doesn't apply here. Switch user_target_xcconfig to the absolute path known at pod-install time (via File.expand_path on the podspec dir). Absolute paths are baked into Pods xcconfigs on every install, so shareability across machines isn't a concern — Pods/ is regenerated per install anyway. This also removes the need for a separate 'pod_target' vs 'user_target' path. 2. Build RN 0.86.0 legacy hermes ios production dynamic (use_frameworks! :linkage => :dynamic mode) failed with: Undefined symbols: __swift_FORCE_LOAD_$_swiftCompatibility56 Sentry.xcframework contains Swift code, but RNSentry.podspec's source_files was Objective-C++ only, so Xcode never auto-linked the Swift runtime compatibility libraries required by the static Swift symbols embedded in Sentry.xcframework. Add an empty RNSentrySwiftLinkStub.swift and include `.swift` in source_files glob so Xcode treats RNSentry as Swift-consuming and pulls in the compat libs. Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 43 +++++++++---------- packages/core/ios/RNSentrySwiftLinkStub.swift | 7 +++ 2 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 packages/core/ios/RNSentrySwiftLinkStub.swift diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index e73d7c12e8..b597c8c7c8 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -45,7 +45,11 @@ 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}' + # `.swift` is here for `RNSentrySwiftLinkStub.swift`, which forces Xcode + # to link the Swift runtime compatibility libs required by our vendored + # Sentry.xcframework static Swift library. + s.source_files = 'ios/**/*.{h,m,mm,swift}', 'cpp/**/*.{h,cpp}' + s.swift_versions = ['5.5'] 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' @@ -96,32 +100,25 @@ Pod::Spec.new do |s| # and fail with "unsupported Swift architecture". New slices in future # sentry-cocoa releases are picked up automatically at pod-install. # - # `pod_target_xcconfig` covers RNSentry's own compile phase — inside - # a pod's xcconfig, `${PODS_TARGET_SRCROOT}` resolves to the pod - # source directory (`packages/core/` here). `user_target_xcconfig` - # covers the host app target so app code writing `#import ` - # (e.g. `SentryNativeInitializer.m`) also finds the framework. The - # aggregate/user xcconfig doesn't define `PODS_TARGET_SRCROOT`, so - # for that target we resolve the path via `${PODS_ROOT}/..` + - # `node_modules/@sentry/react-native`, which is where RN's - # auto-linking places the SDK. - pod_search_paths = {} - user_search_paths = {} + # 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| - pod_paths = slice_ids.map do |slice| - %("${PODS_TARGET_SRCROOT}/ios/Vendor/Sentry.xcframework/#{slice}") + paths = slice_ids.map do |slice| + %("#{File.join(sentry_xcframework_dir, slice)}") end - user_paths = slice_ids.map do |slice| - %("${PODS_ROOT}/../../node_modules/@sentry/react-native/ios/Vendor/Sentry.xcframework/#{slice}") - end - pod_search_paths["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = - (['$(inherited)'] + pod_paths).join(' ') - user_search_paths["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = - (['$(inherited)'] + user_paths).join(' ') + xcframework_search_paths["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = + (['$(inherited)'] + paths).join(' ') end - pod_target_xcconfig.merge!(pod_search_paths) - s.user_target_xcconfig = user_search_paths + pod_target_xcconfig.merge!(xcframework_search_paths) + s.user_target_xcconfig = xcframework_search_paths else s.dependency 'Sentry', sentry_cocoa_version end diff --git a/packages/core/ios/RNSentrySwiftLinkStub.swift b/packages/core/ios/RNSentrySwiftLinkStub.swift new file mode 100644 index 0000000000..185eb574f5 --- /dev/null +++ b/packages/core/ios/RNSentrySwiftLinkStub.swift @@ -0,0 +1,7 @@ +// 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" From dabcb3cfe89b42a1828b3bb8341224cd46aeed5f Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 16:16:39 +0200 Subject: [PATCH 15/18] fix(ios): Gate Swift link stub inclusion on RN >= 0.75 Adding a Swift file to RNSentry.podspec's source_files (to force Xcode to link Swift runtime compat libs when consuming Sentry.xcframework) made CocoaPods treat RNSentry as a 'Swift pod'. On RN < 0.75, React-hermes doesn't define modular headers, so pod install fails: The Swift pod `RNSentry` depends upon `React-hermes`, which does not define modules. The Swift compat lib linkage is only needed when linking Sentry.xcframework's static Swift symbols into a dynamic framework (use_frameworks! :dynamic), which is a RN 0.86+ scenario anyway. Gate the .swift file on RN >= 0.75 to preserve pod install on the older matrix cells. Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index b597c8c7c8..c0d5a8b4da 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -45,11 +45,24 @@ 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. - # `.swift` is here for `RNSentrySwiftLinkStub.swift`, which forces Xcode - # to link the Swift runtime compatibility libs required by our vendored - # Sentry.xcframework static Swift library. - s.source_files = 'ios/**/*.{h,m,mm,swift}', 'cpp/**/*.{h,cpp}' - s.swift_versions = ['5.5'] + # + # 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' From 83a6fccd2498e7a0aa90bd09cd18f80a44cde204 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 2 Jul 2026 17:05:19 +0200 Subject: [PATCH 16/18] fix: Address reviewer feedback on CHANGELOG placement and Swift stub - Move the SPM/xcframework entry out of the already-released `## 8.17.0` section into a new `## Unreleased` section (danger check). The merge from main pulled 8.17.0's release notes on top and swept the entry inside them. - Add a private constant to RNSentrySwiftLinkStub.swift. A pure-comment Swift file compiles to nothing, which would defeat the whole reason for the stub (forcing Xcode to link Swift runtime compatibility libs when consuming Sentry.xcframework's static Swift symbols). Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 15 ++++++++++----- packages/core/ios/RNSentrySwiftLinkStub.swift | 5 +++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 329f76073d..7f36e6d9e9 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.2 ### Fixes @@ -48,11 +58,6 @@ ### Changes - Default `mobileReplayIntegration({ networkCaptureBodies })` to `true`, matching the iOS and Android native SDK defaults ([#6372](https://github.com/getsentry/sentry-react-native/pull/6372)) -- 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. ### Fixes diff --git a/packages/core/ios/RNSentrySwiftLinkStub.swift b/packages/core/ios/RNSentrySwiftLinkStub.swift index 185eb574f5..7e456ef6c1 100644 --- a/packages/core/ios/RNSentrySwiftLinkStub.swift +++ b/packages/core/ios/RNSentrySwiftLinkStub.swift @@ -5,3 +5,8 @@ // 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 = () From 1d96e69a32f53321f77ef0ec16f176f8a9078eb3 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 11:09:54 +0200 Subject: [PATCH 17/18] chore(ios): Address itaybre review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sentry_utils.rb: replace system('curl',…) with URI.open from stdlib open-uri. Same net behavior, no shell escaping to worry about, pure Ruby. - sentry_utils.rb + RNSentry.podspec: drop the runtime slice-enumeration helpers. Sentry.xcframework's layout has been stable across releases, so hardcode the slice → SDK map into a frozen constant (SENTRY_XCFRAMEWORK_SLICES_BY_SDK) and iterate it directly in the podspec. Less code, no filesystem scan on every pod install. If sentry-cocoa ever ships a new platform slice, add a line to the constant. - scripts/update-cocoa.sh: extend set-version to also refresh the Sentry.xcframework.zip SHA256 checksum in sentry_utils.rb. Before this, bumping sentry-cocoa required editing the checksum by hand — a silent recipe for typos that would break every user's pod install with a checksum-mismatch error. Now the script downloads the new archive, computes SHA256, and rewrites both the version key and the Sentry value in place with a sanity-check. Co-Authored-By: Claude Opus 4.7 --- packages/core/RNSentry.podspec | 18 +++--- packages/core/scripts/sentry_utils.rb | 91 +++++++++++---------------- scripts/update-cocoa.sh | 48 ++++++++++++-- 3 files changed, 87 insertions(+), 70 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index c0d5a8b4da..f536c8273e 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -110,8 +110,12 @@ Pod::Spec.new do |s| # 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. + # 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 @@ -121,13 +125,9 @@ Pod::Spec.new do |s| # 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(' ') + 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 pod_target_xcconfig.merge!(xcframework_search_paths) diff --git a/packages/core/scripts/sentry_utils.rb b/packages/core/scripts/sentry_utils.rb index fb2e3cbe4c..4d54533704 100644 --- a/packages/core/scripts/sentry_utils.rb +++ b/packages/core/scripts/sentry_utils.rb @@ -43,23 +43,46 @@ def is_new_hermes_runtime(rn_version) require 'digest' require 'fileutils' +require 'open-uri' # 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. +# +# Kept in sync with `sentry_cocoa_version` in `RNSentry.podspec` by +# `scripts/update-cocoa.sh set-version `, 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.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 +# 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 `/ios/Vendor/.xcframework` exists. # # On first invocation, downloads the prebuilt xcframework zip from @@ -96,8 +119,13 @@ def ensure_sentry_xcframework(version, product = 'Sentry') "#{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}" + 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 @@ -125,50 +153,3 @@ def ensure_sentry_xcframework(version, product = 'Sentry') 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/update-cocoa.sh b/scripts/update-cocoa.sh index ed6f2e2919..352f28c658 100755 --- a/scripts/update-cocoa.sh +++ b/scripts/update-cocoa.sh @@ -1,24 +1,60 @@ #!/usr/bin/env bash set -euo pipefail -file="$(dirname "$0")/../packages/core/RNSentry.podspec" -content=$(cat $file) +script_dir="$(cd "$(dirname "$0")" && pwd)" +podspec="$script_dir/../packages/core/RNSentry.podspec" +utils="$script_dir/../packages/core/scripts/sentry_utils.rb" + +content=$(cat "$podspec") regex="(sentry_cocoa_version *= *)'([0-9\.]+)'" if ! [[ $content =~ $regex ]]; then - echo "Failed to find the plugin version in $file" + echo "Failed to find the plugin version in $podspec" exit 1 fi case $1 in get-version) - echo ${BASH_REMATCH[2]} + echo "${BASH_REMATCH[2]}" ;; get-repo) echo "https://github.com/getsentry/sentry-cocoa.git" ;; set-version) - newValue="${BASH_REMATCH[1]}'$2'" - echo "${content/${BASH_REMATCH[0]}/$newValue}" >$file + new_version="$2" + + # 1. Update `sentry_cocoa_version` in the podspec. + newValue="${BASH_REMATCH[1]}'$new_version'" + printf '%s\n' "${content/${BASH_REMATCH[0]}/$newValue}" >"$podspec" + + # 2. Refresh the `Sentry.xcframework.zip` SHA256 checksum in + # `sentry_utils.rb`. The checksum table has a single version key + # that we rewrite in place — download the new archive, compute + # its SHA256, then patch the version key and the `'Sentry'` line. + # `pod install` verifies against this checksum on every fresh + # install so a stale value here breaks every user's build with a + # loud mismatch error rather than a silent corruption. + zip_url="https://github.com/getsentry/sentry-cocoa/releases/download/${new_version}/Sentry.xcframework.zip" + tmp_dir=$(mktemp -d) + trap 'rm -rf "$tmp_dir"' EXIT + echo "Fetching ${zip_url}..." + curl -fsSL --retry 3 -o "$tmp_dir/Sentry.xcframework.zip" "$zip_url" + new_sha=$(shasum -a 256 "$tmp_dir/Sentry.xcframework.zip" | awk '{print $1}') + echo "Sentry.xcframework.zip SHA256: ${new_sha}" + + # Update the version key (any semver-shaped key opening the hash). + perl -i -pe "s|'\\d+\\.\\d+\\.\\d+' => \\{|'${new_version}' => {|" "$utils" + # Update the `Sentry` checksum (a 64-char lowercase hex string). + perl -i -pe "s|'Sentry' => '[a-f0-9]{64}'|'Sentry' => '${new_sha}'|" "$utils" + + # Sanity-check: both lines got rewritten with the new values. + if ! grep -q "'${new_version}' =>" "$utils"; then + echo "Failed to rewrite the checksum version key in $utils" + exit 1 + fi + if ! grep -q "'Sentry' => '${new_sha}'" "$utils"; then + echo "Failed to rewrite the Sentry checksum in $utils" + exit 1 + fi ;; *) echo "Unknown argument $1" From 3eed101fcf3af8e14befcff3e803664e2a64e8e5 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 6 Jul 2026 16:01:00 +0200 Subject: [PATCH 18/18] fix(ios): Cache xcframework in ~/Library/Caches, not inside node_modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repro on a bare RN app installed via `pnpm install --config.node-linker=isolated` (pnpm's strict store makes package directories read-only): pod install failed with Permission denied @ dir_s_mkdir - .../node_modules/.pnpm/ @sentry+react-native.../ios/Vendor Under pnpm's isolated store (and Yarn PnP), the SDK package directory is either a read-only symlink into a shared store or not materialized on disk at all — writing our xcframework cache there is not portable. Move the cache to `~/Library/Caches/sentry-react-native/xcframeworks/ /` (macOS user cache, always writable, deduped across projects on the same machine). Override with `SENTRY_XCFRAMEWORK_CACHE_DIR` for CI or enterprise setups. Also drop `s.vendored_frameworks = 'ios/Vendor/Sentry.xcframework'`. It was decorative — CocoaPods doesn't stage vendored xcframeworks for `path:` (development) pods, which is why we already had to wire FRAMEWORK_SEARCH_PATHS manually. Linking still happens: clang's `-fmodules-autolink` (default) emits the `-framework Sentry` directive as soon as `@import Sentry;` in RNSentry.mm resolves the module through the search paths. With the vendored_frameworks entry gone, the podspec no longer points at a path that only existed inside node_modules. Verified locally against a bare RN 0.86 sample installed via pnpm strict + chmod a-w on the package's ios/ directory: pod install succeeds, build succeeds, app boots, native crash → dSYM upload path still works. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 3 --- packages/core/RNSentry.podspec | 29 +++++++++++++++++++-------- packages/core/scripts/sentry_utils.rb | 18 ++++++++++++++--- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 30f069ab73..b6dea85f87 100644 --- a/.gitignore +++ b/.gitignore @@ -91,9 +91,6 @@ 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/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index f536c8273e..4162b140dd 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -63,7 +63,6 @@ Pod::Spec.new do |s| 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 @@ -77,12 +76,14 @@ Pod::Spec.new do |s| # 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. + # SHA256-verified, and cached under `~/Library/Caches/sentry-react-native/ + # xcframeworks//` (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). @@ -102,7 +103,19 @@ Pod::Spec.new do |s| if use_xcframework sentry_xcframework_dir = ensure_sentry_xcframework(sentry_cocoa_version, 'Sentry') - s.vendored_frameworks = 'ios/Vendor/Sentry.xcframework' + + # 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 ` doesn't descend into `.xcframework` bundles — it # looks for `Sentry.framework` directly at the given path. Point a diff --git a/packages/core/scripts/sentry_utils.rb b/packages/core/scripts/sentry_utils.rb index 4d54533704..43a778c019 100644 --- a/packages/core/scripts/sentry_utils.rb +++ b/packages/core/scripts/sentry_utils.rb @@ -83,18 +83,30 @@ def is_new_hermes_runtime(rn_version) 'xrsimulator' => %w[xros-arm64_x86_64-simulator], }.freeze -# Ensures `/ios/Vendor/.xcframework` exists. +# Ensures `//.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. +# 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') - vendor_dir = File.expand_path('../ios/Vendor', __dir__) + 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