Conversation
PR SummaryMedium Risk Overview When enabled via Also updates CI to test both CocoaPods and SPM modes in a matrix build, adds a verification script ( Reviewed by Cursor Bugbot for commit 777b3d6. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The helper can modify unrelated application targets, and the global platform switch breaks mixed iOS/tvOS Podfiles.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Adds opt-in Swift Package Manager support for mParticle’s iOS SDKs while preserving default CocoaPods behavior.
Changes:
- Adds an SPM Podfile helper and conditional podspec configuration.
- Updates sample tests, documentation, and migration guidance.
- Extends CI to test both dependency modes and detect duplicate SDK copies.
| File | Description |
|---|---|
react-native-mparticle.podspec |
Configures SPM header discovery and dependency behavior. |
ios/mparticle_spm.rb |
Adds and pins Swift packages in app targets. |
sample/ios/Podfile |
Enables sample SPM mode via environment flag. |
sample/ios/MParticleSampleTests/RNMPRoktPlaceholderTests.m |
Uses the shared SDK import header. |
sample/ios/MParticleSampleTests/RCTConvertCommerceMappingTests.m |
Uses the shared SDK import header. |
scripts/ios/check-single-copy.sh |
Checks archived apps for duplicate SDK classes. |
.github/workflows/pull-request.yml |
Tests CocoaPods and SPM configurations. |
README.md |
Documents SPM setup and troubleshooting. |
MIGRATING.md |
Adds SPM migration and rollback instructions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # against their headers and never links them, so it is a static framework even under | ||
| # `use_frameworks! :linkage => :dynamic`. | ||
| s.static_framework = true | ||
| s.platforms = { :ios => ios_platform } # the Rokt kit Swift package is iOS-only |
There was a problem hiding this comment.
Agreed that this needed addressing. $RNMParticleUseSPM applies to the whole Podfile, and the Rokt kit Swift package is iOS-only, so letting tvOS targets share a Podfile with SPM mode would need a separate design. Instead, 777b3d6 corrects the README: SPM mode is iOS-only, and a Podfile with a tvOS target that uses this package must stay on CocoaPods.
| def self.application_targets(installer) | ||
| installer.aggregate_targets.flat_map do |aggregate| | ||
| aggregate.user_targets | ||
| .select { |t| t.product_type == 'com.apple.product-type.application' } | ||
| .map { |t| [aggregate.user_project, t] } | ||
| end.uniq { |project, target| [project.path.to_s, target.uuid] } | ||
| end |
There was a problem hiding this comment.
Fixed in 777b3d6. application_targets now keeps only aggregates whose platform is iOS and whose pod targets include react-native-mparticle. It matches by pod_name, so scoped variants still match. The conflict guard now uses pod_name too. The README now says the packages go to every iOS application target that uses this package.
With `$RNMParticleUseSPM = true` in the Podfile, the pod drops its mParticle-Apple-SDK-ObjC and RoktContracts dependencies and compiles only against their headers. A build phase finds the headers from the path Xcode records for each package, so no DerivedData layout is assumed. The pod is a static framework in this mode, so every CocoaPods linkage works. Without the flag, the podspec resolves exactly as before. The new ios/mparticle_spm.rb helper, called from post_install, adds the mParticle core and each kit as exactly pinned Swift packages to the app target. It fails pod install if a pod would add a second copy of the SDK. The sample switches mode with MP_USE_SPM=1, and CI runs both modes. The CocoaPods leg keeps the required "iOS Sample App" check name. Each leg archives a Release build and checks that it holds one copy of each SDK class. README and MIGRATING document the mode. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1e1cc47 to
777b3d6
Compare


Why
Today an iOS app using this package must take the mParticle SDK and its kits from CocoaPods, whose central repository becomes read-only on 2026-12-02: after that, new SDK releases can no longer reach apps through it. Apps that add kits with Swift Package Manager also end up with two copies of the SDK and a runtime crash. Once this lands, an app can opt in, with two lines in its Podfile, to taking the mParticle core SDK and kits from Swift Package Manager, with exactly one copy of each in the app. Apps that do nothing see no change.
Programme
Part of the plan to support Swift Package Manager in this package before the CocoaPods central repository becomes read-only (CocoaPods announcement). This is the third of eight pull requests, and the main deliverable; all eight merge into the
workstation/spm-migrationbranch, which merges intomainonce the series is complete, and ship together in one release. The plan record is internal and cannot be linked here.What changes
Before: this package's pod always depends on the
mParticle-Apple-SDK-ObjCandRoktContractspods, and apps add kits as pods.After, with
$RNMParticleUseSPM = truein the Podfile:mparticle_spm_post_install(installer, kits: [...])inios/mparticle_spm.rb, adds the mParticle core and each kit as Swift packages, pinned to exact versions, to each iOS app target that uses this package.pod installfails with a clear message if a pod would add a second copy of the SDK.The helper edits the app's
.xcodeprojon everypod install. It prints each change and is idempotent; the README says so.Also changed:
MP_USE_SPM=1switchessample/ios/Podfileto this mode. The sample unit tests now import the SDK through the library's import header, so they compile in both modes.mainruleset requires; the new leg is "iOS Sample App (SPM)". Each leg now also archives a Release build and fails if the app contains more than one copy of any SDK class (scripts/ios/check-single-copy.sh).Start reading at the
use_spmblock inreact-native-mparticle.podspec, thenios/mparticle_spm.rb, then the samplePodfile. Left alone on purpose: tvOS.$RNMParticleUseSPMapplies to the whole Podfile, so a Podfile with a tvOS target that uses this package stays on CocoaPods (the Rokt kit package is iOS-only); the README says so. The Expo plugin gets this option in the next pull request.Linked work
Depends on: the previous two pull requests in this series (branches
thomson-t/spm-01-duplicate-sdk-warningandthomson-t/spm-02-sdk-import-header); this one needs the private import header from the second.Unblocks: the Expo config plugin option (branch
thomson-t/spm-04-expo-spm), which calls the same helper.Related: #429, later in this series, makes this mode the default, with
$RNMParticleDisableSPM = trueas the opt-out, and replaces themparticle_spm_post_installcall with hooks that run on their own.Rollout
Path: this merges into
workstation/spm-migration, notmain, so nothing reachesmainor a release until the whole series has merged there and that branch is merged intomain. It then ships in the next release. The mode is off unless an app sets$RNMParticleUseSPM.Feature flags: the Podfile global
$RNMParticleUseSPM, off by default. An app's developer turns it on and should first check that a Debug build shows no duplicate-SDK red box.Turning it off: an app removes the flag and the helper call, restores its kit pods and runs
pod install(MIGRATING lists the steps). For the package, reverting this pull request and releasing again removes the mode in the next version.What we watch: both CI legs on every pull request, including the single-copy check. After the release, this repository's issues for build failures in this mode.
Risks
Podfile.lockin default mode matched the previous one apart from the Podfile's own checksum; we would see changed pod versions after upgrading.pod installfails in this mode if any mParticle or Rokt pod remains (the helper's own message, or CocoaPods' "does not define modules" error when no dynamic-framework hook is present; both are in the README), and the previous pull request's Debug red box covers setups outside this mode; we would see a report of the runtime crash.-clonedSourcePackagesDirPath; the build then fails with a message naming the fix; we would see the SPM CI leg fail, and CI runs Xcode 16, which the local runs did not use.Package.resolved; we would see different Rokt versions between builds of the same app.ENABLE_USER_SCRIPT_SANDBOXINGin an app blocks the build phase; not addressed beyond declaring its inputs and outputs, because the sample does not enable sandboxing for pod targets; we would see a sandbox denial in the build log.Risk class: higher — this adds a second way to link the SDKs into partner apps and changes CI. Default CocoaPods behaviour is unchanged and verified.
Who
Written by: an automated coding agent (Claude Code), at an engineer's request, following the internal Swift Package Manager migration plan and its proof of concept.
Code reviewed before opening: an independent review agent reviewed the change before it was committed.
Design reviewed before opening: the requesting engineer approved the migration plan, including this opt-in design and its rejected alternatives. Two alternatives failed in the proof of concept: a package dependency declared inside the pod (duplicate symbols) and a DerivedData-relative header path (archive fails).
Decision this implements: the requesting engineer's approval of the migration plan on 2026-09-28; the record is internal and cannot be linked.
Checked: on 2026-09-28, with Xcode 27.0 on an iOS 26.5 simulator, the sample app (React Native 0.84, New Architecture), SDK and Rokt kit 9.6.1:
use_frameworks!: both:dynamicand:staticpassed the same build, archive, single-copy and runtime checks.pod installfailed, as intended.-clonedSourcePackagesDirPath: build and archive succeeded, with a single copy.Podfile.lockunchanged apart from the Podfile checksum; build, archive, single copy, runtime and unit tests passed.Not checked: the Old Architecture; physical devices; an app with user-script sandboxing enabled; a real Podfile with several app targets, where only a script with stand-in targets checked that the helper skips apps that do not use this package, tvOS apps and test targets. CI, which builds with Xcode 16, passed on this pull request.
Size
Hand-written: about 280 lines added and 21 removed in 9 files (93 of them the Podfile helper, 58 docs).
Generated: none.
Notes for reviewers
How headers are found. For every package, Xcode writes
$(OBJROOT)/GeneratedModuleMaps-$(PLATFORM_NAME)/<module>.modulemap, whoseumbrellaline is the absolute path of the package's public headers, for build and archive alike. The pod'sbefore_compilescript phase readsmParticle_Apple_SDK_ObjC.modulemapand symlinks that directory to$(DERIVED_FILE_DIR)/mParticleSPMInclude, which is on the pod's header search path, soRNMPSDKImports.htakes its flat-header branch.RoktContracts-Swift.his also in that directory, and is declared as an input so the phase runs after that package builds.Why not a package dependency in the podspec (React Native's
spm_dependency): in the proof of concept, Xcode merged the package objects into the pod's static library, and the app linked them again (12,100 duplicate symbols).The sample's test target inherits the pods completely and never links the SDK itself: the host app provides the symbols. In this mode the sample Podfile adds the two header directories above to the test target, so the tests compile.
Package URLs match the kits' byte for byte (the core has no
.gitsuffix), so SwiftPM treats them as one package identity.🤖 Generated with Claude Code