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