diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 13082b8df0..efb32a6d89 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/CHANGELOG.md b/CHANGELOG.md index 5743ebc7fe..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 diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 84189b7df6..4162b140dd 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -45,41 +45,115 @@ Pod::Spec.new do |s| # is pulled in here; on Android it is compiled by the dedicated CMake target # in `android/CMakeLists.txt`. The files are guarded with # `RCT_NEW_ARCH_ENABLED` so they compile to empty TUs on Old Arch. - s.source_files = 'ios/**/*.{h,m,mm}', 'cpp/**/*.{h,cpp}' + # + # We include `.swift` (for `RNSentrySwiftLinkStub.swift`) only on RN >= + # 0.75. Adding a Swift file makes CocoaPods treat RNSentry as a Swift + # pod, which then requires modular headers from its ObjC dependencies + # (React-Core, React-hermes) — RN < 0.75 doesn't emit those, so + # `pod install` fails with: + # "The Swift pod `RNSentry` depends upon `React-hermes`, which does + # not define modules." + # The stub is only needed when linking Sentry.xcframework's Swift + # symbols into a dynamic framework anyway (RN 0.86+ `use_frameworks! + # :dynamic`), so gating on RN 0.75 is safe. + supports_swift_stub = rn_version[:major] >= 1 || (rn_version[:major] == 0 && rn_version[:minor] >= 75) + if supports_swift_stub + s.source_files = 'ios/**/*.{h,m,mm,swift}', 'cpp/**/*.{h,cpp}' + s.swift_versions = ['5.5'] + else + s.source_files = 'ios/**/*.{h,m,mm}', 'cpp/**/*.{h,cpp}' + end s.public_header_files = 'ios/RNSentry.h', 'ios/RNSentrySDK.h', 'ios/RNSentryStart.h', 'ios/RNSentryVersion.h', 'ios/RNSentryBreadcrumb.h', 'ios/RNSentryReplay.h', 'ios/RNSentryReplayBreadcrumbConverter.h', 'ios/Replay/RNSentryReplayMask.h', 'ios/Replay/RNSentryReplayUnmask.h', 'ios/RNSentryTimeToDisplay.h' s.compiler_flags = other_cflags - s.pod_target_xcconfig = { + pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } sentry_cocoa_version = '9.19.1' - # Opt-in to consuming sentry-cocoa via Swift Package Manager. - # When `SENTRY_USE_SPM=1` is set, RNSentry pulls `Sentry` from the - # sentry-cocoa SPM package as a binary xcframework instead of from - # the Sentry CocoaPods source build. Defaults to CocoaPods consumption - # for backward compatibility with the full RN version range we support. + # Consume sentry-cocoa as a prebuilt `Sentry.xcframework` by default. # - # Requires React Native >= 0.75 because the SPM helper - # (`react-native/scripts/cocoapods/spm.rb`) is loaded transitively from - # the Podfile via `react_native_pods.rb`. - if ENV['SENTRY_USE_SPM'] == '1' - unless defined?(SPM) && SPM.respond_to?(:dependency) - raise 'SENTRY_USE_SPM=1 is set but the SPM helper is not loaded. ' \ - 'This requires React Native >= 0.75 and a Podfile that imports ' \ - 'react_native_pods.rb.' + # The xcframework is downloaded from sentry-cocoa's GitHub Release, + # SHA256-verified, and cached under `~/Library/Caches/sentry-react-native/ + # xcframeworks//` (override with `SENTRY_XCFRAMEWORK_CACHE_DIR`). + # Slice paths are then wired into the pod and app target via + # `FRAMEWORK_SEARCH_PATHS` below. Consuming the xcframework this way + # avoids compiling sentry-cocoa from source (fast install) and + # sidesteps the Xcode 16/26 archive bug that affects the same + # xcframework when consumed through Xcode's SPM integration + # (`Signatures/*.signature` collision during archive). + # + # Set `SENTRY_USE_XCFRAMEWORK=0` to fall back to the source-built + # `Sentry` CocoaPod (e.g. for offline builds behind a restrictive proxy). + # + # `SENTRY_USE_SPM` was the name in earlier drafts of this PR; honor it as a + # deprecated alias so CI or local envs still exporting `SENTRY_USE_SPM=0` + # don't silently take the new xcframework path. + env_use_xcframework = ENV['SENTRY_USE_XCFRAMEWORK'] + if env_use_xcframework.nil? && !ENV['SENTRY_USE_SPM'].nil? + Pod::UI.warn '[Sentry] SENTRY_USE_SPM is deprecated; use SENTRY_USE_XCFRAMEWORK instead.' if defined?(Pod::UI) + env_use_xcframework = ENV['SENTRY_USE_SPM'] + end + use_xcframework = case env_use_xcframework + when '0' then false + else true + end + + if use_xcframework + sentry_xcframework_dir = ensure_sentry_xcframework(sentry_cocoa_version, 'Sentry') + + # We deliberately do NOT set `s.vendored_frameworks` even though the + # xcframework is downloaded and referenced below. CocoaPods doesn't + # stage vendored xcframeworks for `path:` (development) pods anyway + # — it did nothing for us functionally — and pointing it at a path + # inside `node_modules/@sentry/react-native/ios/Vendor/` used to be + # actively harmful, because that path only existed when the SDK was + # cached into the package directory (broken under pnpm's isolated + # store / Yarn PnP where node_modules is read-only). Framework + # linking still happens: `@import Sentry;` in RNSentry.mm + user + # code triggers clang's `-fmodules-autolink`, which emits the + # `-framework Sentry` linker directive automatically once the + # module is discovered via `FRAMEWORK_SEARCH_PATHS` below. + + # Xcode's `-F ` 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". + # + # We hardcode the slice → SDK map in `SENTRY_XCFRAMEWORK_SLICES_BY_SDK` + # (see `sentry_utils.rb`) rather than scanning the extracted bundle — + # sentry-cocoa's `Sentry.xcframework` layout is stable across releases. + # Add a slice there if a future release ships one. + # + # Point the search paths at the pod-install-time absolute path to the + # xcframework. `${PODS_TARGET_SRCROOT}` is only defined in per-pod + # xcconfigs, not in aggregate/user-target xcconfigs, and a + # `${PODS_ROOT}`-relative fallback works for one Podfile layout but + # breaks for another (e.g. the RN sample apps put node_modules at a + # different depth from RNSentryCocoaTester). Using the absolute path + # avoids the layout-detection dance — the path is regenerated on + # every `pod install`, so it's not something anyone commits. + xcframework_search_paths = SENTRY_XCFRAMEWORK_SLICES_BY_SDK.each_with_object({}) do |(sdk, slice_ids), acc| + paths = slice_ids.map { |slice| %("#{File.join(sentry_xcframework_dir, slice)}") } + acc["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = (['$(inherited)'] + paths).join(' ') end - SPM.dependency(s, - url: 'https://github.com/getsentry/sentry-cocoa', - requirement: { kind: 'exactVersion', version: sentry_cocoa_version }, - products: ['Sentry'] - ) + + pod_target_xcconfig.merge!(xcframework_search_paths) + s.user_target_xcconfig = xcframework_search_paths else s.dependency 'Sentry', sentry_cocoa_version end + # Assign before `install_modules_dependencies` so it can merge its + # RN-specific settings on top. Assigning after would clobber those and + # break header resolution across the pod. + s.pod_target_xcconfig = pod_target_xcconfig + if defined? install_modules_dependencies # Default React Native dependencies for 0.71 and above (new and legacy architecture) install_modules_dependencies(s) @@ -88,10 +162,13 @@ Pod::Spec.new do |s| if is_new_arch_enabled then # New Architecture on React Native 0.70 and older - s.pod_target_xcconfig.merge!({ + pod_target_xcconfig.merge!({ "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" }) + # `install_modules_dependencies` is not defined on RN < 0.71 so re-assigning + # here is safe — nothing else has written to `s.pod_target_xcconfig` yet. + s.pod_target_xcconfig = pod_target_xcconfig s.dependency "React-RCTFabric" # Required for Fabric Components (like RCTViewComponentView) s.dependency "React-Codegen" 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..43a778c019 100644 --- a/packages/core/scripts/sentry_utils.rb +++ b/packages/core/scripts/sentry_utils.rb @@ -40,3 +40,128 @@ def should_use_folly_flags(rn_version) def is_new_hermes_runtime(rn_version) return (rn_version[:major] >= 1 || (rn_version[:major] == 0 && rn_version[:minor] >= 81)) end + +require 'digest' +require 'fileutils' +require 'open-uri' + +# SHA256 checksums of `.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' => '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 `//.xcframework` exists. +# +# On first invocation, downloads the prebuilt xcframework zip from +# sentry-cocoa's GitHub release, verifies its SHA256 checksum against +# `SENTRY_COCOA_XCFRAMEWORK_CHECKSUMS`, and extracts it. Subsequent +# invocations are no-ops. Returns the absolute path to the extracted +# xcframework, which callers pass to `FRAMEWORK_SEARCH_PATHS`. +# +# The cache lives under a user-writable directory (`~/Library/Caches/ +# sentry-react-native/xcframeworks/` on macOS by default; override with +# `SENTRY_XCFRAMEWORK_CACHE_DIR`). Cannot live under the pod's own source +# tree because pnpm's isolated store makes `node_modules/@sentry/ +# react-native/ios/` read-only, and `Yarn PnP` doesn't materialize the +# package directory at all — writing there fails with `EACCES`. +# Deduplicating cache across multiple RN projects on the same machine +# is a nice side effect. +# +# Consuming sentry-cocoa this way (vs. through Xcode's SPM integration) +# avoids the Xcode 16/26 archive bug where a signed SPM binary xcframework's +# `Signatures/*.signature` file collides during the archive step. +def ensure_sentry_xcframework(version, product = 'Sentry') + cache_root = ENV['SENTRY_XCFRAMEWORK_CACHE_DIR'] || + File.expand_path('~/Library/Caches/sentry-react-native/xcframeworks') + vendor_dir = File.join(cache_root, version) + target_dir = File.join(vendor_dir, "#{product}.xcframework") + # Treat the presence of `Info.plist` inside the xcframework as the "healthy" + # sentinel rather than just the directory existence. A directory without + # `Info.plist` most likely came from an interrupted `unzip` and would + # otherwise silently short-circuit re-download here. + target_manifest = File.join(target_dir, 'Info.plist') + return target_dir if File.file?(target_manifest) + + expected_checksum = SENTRY_COCOA_XCFRAMEWORK_CHECKSUMS.dig(version, product) + unless expected_checksum + raise "sentry-cocoa xcframework checksum not registered for #{product} " \ + "#{version}. Add it to SENTRY_COCOA_XCFRAMEWORK_CHECKSUMS in " \ + "packages/core/scripts/sentry_utils.rb after bumping the version." + end + + # Wipe any stale partial extract from a previous interrupted run so we + # always start from a clean tree. + FileUtils.rm_rf(target_dir) + FileUtils.mkdir_p(vendor_dir) + zip_path = File.join(vendor_dir, "#{product}.xcframework.zip") + url = "https://github.com/getsentry/sentry-cocoa/releases/download/" \ + "#{version}/#{product}.xcframework.zip" + + Pod::UI.puts "[Sentry] Downloading #{product} #{version} from GitHub Releases…" if defined?(Pod::UI) + begin + URI.open(url, redirect: true) do |remote| + File.open(zip_path, 'wb') { |f| IO.copy_stream(remote, f) } + end + rescue OpenURI::HTTPError, SocketError, StandardError => e + FileUtils.rm_f(zip_path) + raise "Failed to download #{url}: #{e.class}: #{e.message}" + end + + actual_checksum = Digest::SHA256.file(zip_path).hexdigest + unless actual_checksum == expected_checksum + FileUtils.rm_f(zip_path) + raise "Checksum mismatch for #{product} #{version}: expected " \ + "#{expected_checksum}, got #{actual_checksum}" + end + + unless system('unzip', '-q', '-o', zip_path, '-d', vendor_dir) + raise "Failed to extract #{zip_path}" + end + FileUtils.rm_f(zip_path) + + # Guard against a release archive whose internal layout changed (e.g. a + # nested folder). Without this check, a wrong layout silently succeeds and + # then fails much later during `pod install` with a confusing "framework + # not found" error. + unless File.file?(target_manifest) + raise "Expected #{target_manifest} after extracting #{product}.xcframework.zip. " \ + "The sentry-cocoa release archive layout may have changed — update " \ + "the extraction logic in packages/core/scripts/sentry_utils.rb." + end + + target_dir +end + 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 \ 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"