Skip to content

Commit 33cbde1

Browse files
buenaflorclaudecursoragent
committed
feat(apple): Migrate internal Apple build from CocoaPods to spm4Kmp
Replace the Kotlin CocoaPods plugin with spm4Kmp (io.github.frankois944.spmForKmp 1.9.2) for the SDK's internal Apple build, with no public API/ABI change. - Swap cocoapods{} for swiftPackageConfig{} on all Apple targets; consume the remote Sentry Cocoa Swift package (product "Sentry", exportToKotlin) and restore the cocoapods.Sentry.* import prefix via packageDependencyPrefix = "cocoapods" - Port the KT-41709 -D...Unavailable macro workaround via swiftPackageConfig extraOpts (forwarded to cinterop; compilerOpts does not reach the modular cinterop) - Keep the private Sentry.Internal cinterop unchanged (self-contained headers) - Declare Apple targets once (appleTargets) and reuse for swiftPackageConfig - Keep the cocoapods plugin in root for the CocoaPods sample; gitignore spm4Kmp generated dirs (exportedSentryCocoa/, src/swift/) Verified: 11/12 Apple targets compile, appleTest runs (macos/iosSimulator), CocoaPods sample xcodebuild SUCCEEDS (klib/ABI parity), apiCheck/detekt/spotless green. watchosArm32 (armv7k) temporarily disabled: spm4Kmp 1.9.2 has no armv7k entry in its AppleCompileTarget enum, so it cannot generate the exported Sentry cinterop for that target (reported upstream). Must be re-enabled before release. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a2f0ed5 commit 33cbde1

5 files changed

Lines changed: 109 additions & 81 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ local.properties
1717

1818
Pods/
1919

20+
# spm4Kmp (io.github.frankois944.spmForKmp) generated sources/packages.
21+
# Regenerated by swiftPackageConfig on every build; not needed for klib publishing.
22+
**/exportedSentryCocoa/
23+
**/src/swift/
24+
2025
.project
2126
.settings
2227
.classpath

SPM4KMP_MIGRATION.md

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -70,27 +70,48 @@ Kotlin **2.2.21** is the first version whose cinterop commonizer supports the Xc
7070

7171
## Phase 1 — spm4Kmp migration (the goal)
7272

73-
- [ ] **Proof-of-concept spike** (one simulator target, throwaway): prove `cocoapods.Sentry.*` import
74-
parity via `packageDependencyPrefix = "cocoapods"` **and** that the fragile cross-cinterop casts still
75-
compile/resolve:
76-
- `(it as InternalSentryEvent).isFatalEvent` and `storeEnvelope(envelope as objcnames.classes.SentryEnvelope)`
77-
in `nsexception/SentryUnhandledExceptions.kt`
78-
- `scope as? Internal.Sentry.SentryScope` in `CocoaScopeProvider.kt`
79-
- If parity holds → proceed; if not → re-scope (single cinterop / Swift `@objc` bridge).
80-
- [ ] **SDK Gradle migration:** replace the `cocoapods {}` block in
81-
`sentry-kotlin-multiplatform/build.gradle.kts` with `spm4Kmp` (`io.github.frankois944.spmForKmp`) +
82-
`swiftPackageConfig {}` for all Apple targets; remote Swift package on
83-
`https://github.com/getsentry/sentry-cocoa.git` at `Config.Libs.sentryCocoaVersion`, product `Sentry`,
84-
`exportToKotlin = true`; port deployment targets and the KT-41709 `-D...Unavailable` macro workaround.
85-
Confirm `spm4Kmp` supports Kotlin 2.2.21 (requires 2.2.20+).
86-
- [ ] **Private `Sentry.Internal` cinterop:** keep `cinterops.create("Sentry.Internal")` for all Apple
87-
targets and wire include/compiler paths to the Sentry Cocoa headers resolved by SwiftPM (instead of the
88-
CocoaPods header layout). Fallback: `remoteBinary`/checked-in package wrapper.
89-
- [ ] **API/behavior verification:** compile all Apple targets + `appleTest`; keep the **CocoaPods sample
90-
building unchanged** as the klib-symbol-parity regression check.
91-
Note: `apiCheck` covers android/jvm only — Apple coverage relies on compile/test/sample gates.
92-
- [ ] Keep SDK-reported telemetry `SENTRY_COCOA_PACKAGE_NAME = "cocoapods:sentry-cocoa"` unchanged
93-
(observable behavior; out of scope to relabel).
73+
- [x] **Proof-of-concept spike** (iosSimulatorArm64) — **PARITY HOLDS, PROCEED.** `spmForKmp 1.9.2`
74+
(Kotlin 2.2.21 OK). `compileKotlinIosSimulatorArm64` is **green**, proving:
75+
- `cocoapods.Sentry.*` import parity via `packageDependencyPrefix = "cocoapods"` (`add("Sentry",
76+
exportToKotlin = true)` → exported-product cinterop named `Sentry`, package `cocoapods.Sentry`).
77+
- The fragile cross-cinterop casts compile: `(it as InternalSentryEvent).isFatalEvent` +
78+
`storeEnvelope(... as objcnames.classes.SentryEnvelope)` (`SentryUnhandledExceptions.kt`) and
79+
`scope as? Internal.Sentry.SentryScope` (`CocoaScopeProvider.kt`).
80+
- The private `Sentry.Internal` cinterop coexists and links — headers are self-contained
81+
(Foundation-only, private API redeclared as standalone ObjC interfaces), so it is independent of
82+
how the Sentry framework is delivered. No SwiftPM-header wiring needed.
83+
- KT-41709 workaround ported via `swiftPackageConfig { extraOpts = listOf("-compiler-option",
84+
"-DSentryMechanismMeta=...Unavailable", ...) }` (NOT `compilerOpts`, which only sets the def's
85+
clang flags and does not reach the modular cinterop).
86+
- [x] **SDK Gradle migration:** `cocoapods {}` block replaced with `spmForKmp` `swiftPackageConfig {}` for
87+
all Apple targets; plugin declared in root + `Config.spmForKmp` 1.9.2; `cocoapods` plugin kept in root
88+
for the CocoaPods sample. spm4Kmp generated dirs (`exportedSentryCocoa/`, `src/swift/`) gitignored.
89+
- [x] **Private `Sentry.Internal` cinterop:** kept `cinterops.create("Sentry.Internal")`; no SwiftPM header
90+
wiring required (self-contained headers). Fallback (`remoteBinary`/wrapper) not needed.
91+
- [x] **API/behavior verification (11/12 Apple targets):**
92+
- All Apple targets compile **except watchosArm32** (see blocker below): ios{Arm64,X64,SimulatorArm64},
93+
watchos{Arm64,X64,SimulatorArm64}, tvos{Arm64,X64,SimulatorArm64}, macos{X64,Arm64}.
94+
- `appleTest` green: `macosArm64Test` + `iosSimulatorArm64Test` compile and run.
95+
- **CocoaPods sample `xcodebuild` → BUILD SUCCEEDED** (Debug/iphonesimulator/arm64): the sample uses
96+
the legacy CocoaPods plugin and links the migrated SDK klib — confirms `cocoapods.Sentry.*`
97+
klib-symbol/ABI parity end-to-end.
98+
- `spotlessApply` + `detekt` green.
99+
- Note: `apiCheck` covers android/jvm only — Apple coverage relies on these compile/test/sample gates.
100+
- [x] Keep SDK-reported telemetry `SENTRY_COCOA_PACKAGE_NAME = "cocoapods:sentry-cocoa"` unchanged —
101+
`buildkonfig` block untouched.
102+
103+
### ⚠️ Phase 1 blocker — watchosArm32 (armv7k) temporarily disabled
104+
- spm4Kmp **1.9.2 does not support watchosArm32**: its `AppleCompileTarget` enum has no armv7k entry, so it
105+
never generates the `CompileSwiftPackage` / `GenerateCInteropDefinition` / exported-`Sentry` cinterop tasks
106+
for that target (verified by decompiling the plugin). Neither `remotePackageVersion` nor `remoteBinary`
107+
helps — the gap is in the plugin's per-target pipeline, not the dependency source.
108+
- This is purely a plugin limitation: Kotlin/Native cinterop itself supports armv7k (our manual
109+
`cinteropSentry.InternalWatchosArm32` builds fine), and the Sentry xcframework ships an armv7k slice.
110+
- **Temporarily disabled** `watchosArm32()` in `build.gradle.kts` (target list + swiftPackageConfig forEach),
111+
marked with a `TODO(spm4Kmp)`. Reported upstream to the spm4Kmp author.
112+
- **Must re-enable before release** (it is a published target — `validateDistributions` expects
113+
`watchosarm32`; binary-compat `apiCheck` golden may also need re-sync). Re-enable once spm4Kmp adds armv7k,
114+
or fall back to a manual header-based `cinterops.create("Sentry")` for that one target.
94115

95116
---
96117

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ plugins {
1111
id(Config.dokka).version(Config.dokkaVersion)
1212
kotlin(Config.multiplatform).version(Config.kotlinVersion).apply(false)
1313
kotlin(Config.cocoapods).version(Config.kotlinVersion).apply(false)
14+
id(Config.spmForKmp).version(Config.spmForKmpVersion).apply(false)
1415
id(Config.jetpackCompose).version(Config.composePluginVersion).apply(false)
1516
id(Config.kotlinCompose).version(Config.kotlinVersion).apply(false)
1617
id(Config.androidGradle).version(Config.agpVersion).apply(false)

buildSrc/src/main/java/Config.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ object Config {
66

77
val multiplatform = "multiplatform"
88
val cocoapods = "native.cocoapods"
9+
val spmForKmp = "io.github.frankois944.spmForKmp"
10+
val spmForKmpVersion = "1.9.2"
911
val jetpackCompose = "org.jetbrains.compose"
1012
val kotlinCompose = "org.jetbrains.kotlin.plugin.compose"
1113
val gradleMavenPublishPlugin = "com.vanniktech.maven.publish"

sentry-kotlin-multiplatform/build.gradle.kts

Lines changed: 59 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
@file:OptIn(ExperimentalWasmDsl::class)
22

33
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
4+
import io.github.frankois944.spmForKmp.swiftPackageConfig
45
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
56
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
67
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
78
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
8-
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
99
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
1010

1111
plugins {
1212
kotlin(Config.multiplatform)
13-
kotlin(Config.cocoapods)
13+
id(Config.spmForKmp)
1414
id(Config.androidGradle)
1515
id(Config.BuildPlugins.buildConfig)
1616
kotlin(Config.kotlinSerializationPlugin)
@@ -57,18 +57,27 @@ kotlin {
5757
publishLibraryVariants("release")
5858
}
5959
jvm()
60-
iosArm64()
61-
iosSimulatorArm64()
62-
iosX64()
63-
watchosSimulatorArm64()
64-
watchosArm32()
65-
watchosArm64()
66-
watchosX64()
67-
tvosSimulatorArm64()
68-
tvosArm64()
69-
tvosX64()
70-
macosX64()
71-
macosArm64()
60+
61+
// Apple targets are declared once here and reused for the spm4Kmp swiftPackageConfig below.
62+
// TODO(spm4Kmp): watchosArm32 (armv7k) is temporarily disabled. spm4Kmp 1.9.2 has no
63+
// watchosArm32 entry in its AppleCompileTarget enum, so it cannot generate the exported
64+
// `cocoapods.Sentry` cinterop for it. K/N cinterop itself supports armv7k, so this is purely a
65+
// plugin gap (reported upstream). Re-add it to this list once the plugin adds armv7k support
66+
// (or via a manual header cinterop fallback).
67+
val appleTargets =
68+
listOf(
69+
iosArm64(),
70+
iosSimulatorArm64(),
71+
iosX64(),
72+
watchosArm64(),
73+
watchosX64(),
74+
watchosSimulatorArm64(),
75+
tvosArm64(),
76+
tvosX64(),
77+
tvosSimulatorArm64(),
78+
macosX64(),
79+
macosArm64(),
80+
)
7281
addNoOpTargets()
7382

7483
sourceSets {
@@ -153,60 +162,50 @@ kotlin {
153162
macosTest.get().dependsOn(commonTvWatchMacOsTest)
154163
watchosTest.get().dependsOn(commonTvWatchMacOsTest)
155164

156-
cocoapods {
157-
summary = "Official Sentry SDK Kotlin Multiplatform"
158-
homepage = "https://github.com/getsentry/sentry-kotlin-multiplatform"
159-
version = "0.0.1"
160-
161-
pod(Config.Libs.sentryCocoa) {
162-
version = Config.Libs.sentryCocoaVersion
163-
extraOpts += listOf("-compiler-option", "-fmodules")
165+
appleTargets.forEach { target ->
166+
target.swiftPackageConfig(cinteropName = "sentryCocoa") {
167+
// Restore the legacy Kotlin CocoaPods `cocoapods.Sentry.*` import prefix so
168+
// published klib symbols stay identical and consumers keep building unchanged.
169+
packageDependencyPrefix = "cocoapods"
170+
minIos = Config.Cocoa.iosDeploymentTarget
171+
minMacos = Config.Cocoa.osxDeploymentTarget
172+
minTvos = Config.Cocoa.tvosDeploymentTarget
173+
minWatchos = Config.Cocoa.watchosDeploymentTarget
174+
// KT-41709 workaround forwarded to the exported-product cinterop: Sentry classes
175+
// containing "Meta" in their name (e.g. SentryMechanismMeta) are otherwise declared
176+
// twice. extraOpts is passed straight to cinterop (unlike compilerOpts, which only
177+
// populates the generated def's clang flags). https://youtrack.jetbrains.com/issue/KT-41709
178+
extraOpts =
179+
listOf(
180+
"-compiler-option",
181+
"-DSentryMechanismMeta=SentryMechanismMetaUnavailable",
182+
"-compiler-option",
183+
"-DSentryIntegrationProtocol=SentryIntegrationProtocolUnavailable",
184+
"-compiler-option",
185+
"-DSentryMetricsAPIDelegate=SentryMetricsAPIDelegateUnavailable",
186+
)
187+
dependency {
188+
remotePackageVersion(
189+
url = uri("https://github.com/getsentry/sentry-cocoa.git"),
190+
version = Config.Libs.sentryCocoaVersion,
191+
products = {
192+
add("Sentry", exportToKotlin = true)
193+
},
194+
)
195+
}
164196
}
165197

166-
ios.deploymentTarget = Config.Cocoa.iosDeploymentTarget
167-
osx.deploymentTarget = Config.Cocoa.osxDeploymentTarget
168-
tvos.deploymentTarget = Config.Cocoa.tvosDeploymentTarget
169-
watchos.deploymentTarget = Config.Cocoa.watchosDeploymentTarget
170-
}
171-
172-
listOf(
173-
iosArm64(),
174-
iosX64(),
175-
iosSimulatorArm64(),
176-
watchosArm32(),
177-
watchosArm64(),
178-
watchosX64(),
179-
watchosSimulatorArm64(),
180-
tvosArm64(),
181-
tvosX64(),
182-
tvosSimulatorArm64(),
183-
macosX64(),
184-
macosArm64(),
185-
).forEach {
186-
it.compilations.getByName("main") {
198+
// The private `Sentry.Internal` cinterop uses self-contained, Foundation-only headers
199+
// (private Sentry API redeclared as standalone ObjC interfaces); its symbols resolve at
200+
// link time against the Sentry framework above, so it is independent of how that
201+
// framework is delivered (CocoaPods vs SwiftPM) and can stay unchanged.
202+
target.compilations.getByName("main") {
187203
cinterops.create("Sentry.Internal") {
188204
includeDirs("$projectDir/src/nativeInterop/cinterop/SentryInternal")
189205
}
190206
}
191207
}
192208

193-
// workaround for https://youtrack.jetbrains.com/issue/KT-41709 due to having "Meta" in the class name
194-
// if we need to use this class, we'd need to find a better way to work it out
195-
targets
196-
.withType<KotlinNativeTarget>()
197-
.matching {
198-
it.konanTarget.family.isAppleFamily
199-
}.forEach { target ->
200-
target.compilations["main"].cinterops["Sentry"].extraOpts(
201-
"-compiler-option",
202-
"-DSentryMechanismMeta=SentryMechanismMetaUnavailable",
203-
"-compiler-option",
204-
"-DSentryIntegrationProtocol=SentryIntegrationProtocolUnavailable",
205-
"-compiler-option",
206-
"-DSentryMetricsAPIDelegate=SentryMetricsAPIDelegateUnavailable",
207-
)
208-
}
209-
210209
val commonStub by creating {
211210
dependsOn(commonMain.get())
212211
}

0 commit comments

Comments
 (0)