Skip to content

Commit 8ce0d52

Browse files
buenaflorclaudecursoragent
committed
chore: Phase 0 toolchain upgrade for spm4Kmp migration
Prerequisite toolchain bump that unblocks the planned CocoaPods -> spm4Kmp internal Apple build migration, with no public API/ABI change. - Bump Kotlin 2.2.21, Gradle 8.13, AGP 8.7.3, compileSdk 35; plugins (vanniktech-publish 0.30.0, kover 0.9.1, detekt 1.23.8, dokka 1.9.20, buildkonfig 0.17.1, spotless 7.0.2), serialization 1.7.3, coroutines 1.9.0, Compose 1.9.0 - Migrate vanniktech publish DSL, kover 0.9 DSL, AGP 8 namespace, and kotlinOptions -> compilerOptions (SDK + plugin + samples) - ktlint 1.x reformat via spotlessApply + .editorconfig; regenerate detekt baseline - Fix no-op stub target test wiring, Cocoa log-level cinterop type, and tv/watch/macOS test wrappers under Kotlin 2.2.21 - Add SPM4KMP_MIGRATION.md tracking doc (Phase 0 verified green: SDK + plugin build/tests, apiCheck, detekt, and Apple samples incl. CocoaPods xcodebuild) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9f0c371 commit 8ce0d52

171 files changed

Lines changed: 4861 additions & 1915 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
root = true
2+
3+
[*.{kt,kts}]
4+
# ktlint 1.x enables stricter naming rules by default. The codebase relies on
5+
# backtick-wrapped test function names and constant-style vals that predate
6+
# these rules, so keep them disabled to match existing conventions.
7+
ktlint_standard_function-naming = disabled
8+
ktlint_standard_property-naming = disabled

SPM4KMP_MIGRATION.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# CocoaPods → spm4Kmp Migration Plan & Progress
2+
3+
**Goal:** Migrate only the internal Apple build of the Sentry Kotlin Multiplatform SDK from the Kotlin
4+
CocoaPods plugin to [`spm4Kmp`](https://github.com/frankois944/spm4Kmp), with **zero public API/ABI change**
5+
and **no impact on existing consumers** (CocoaPods or SPM).
6+
7+
**Status legend:** `[x]` done · `[~]` in progress · `[ ]` not started
8+
9+
---
10+
11+
## Scope: internal-only, no public breaking change
12+
13+
- The KMP SDK is distributed via Maven (klibs), not published to CocoaPods. The committed
14+
`sentry_kotlin_multiplatform.podspec` is Kotlin-CocoaPods-plugin-generated internal/sample tooling, not a
15+
public artifact, so removing it is internal cleanup.
16+
- Consumer-facing integration stays intact: the Gradle plugin's CocoaPods auto-install
17+
(`installSentryForCocoapods`) and the SPM linker path (`CocoaFrameworkLinker`) are **not** touched.
18+
19+
### Hard compatibility contract (definition of done)
20+
- `cocoapods.Sentry.*` imports and the `SentryPlatformOptions` typealias must still resolve to the same
21+
symbols (achieved via `packageDependencyPrefix = "cocoapods"`).
22+
- Published klib symbols are unchanged, so both the CocoaPods and SPM samples keep building untouched.
23+
- If either cannot be preserved → stop and escalate (would turn an internal change into a public one).
24+
25+
---
26+
27+
## Phase 0 — Toolchain upgrade (prerequisite) ✅ COMPLETE (core SDK + plugin)
28+
29+
Required because both `spm4Kmp` and the installed **Xcode 26** need Kotlin 2.2.20+/Gradle 8.12+, and
30+
Kotlin **2.2.21** is the first version whose cinterop commonizer supports the Xcode 26 Metal SDK headers
31+
(Kotlin 2.2.20 crashes with `Unresolved classifier: platform/Metal/MTLLogicalToPhysicalColorAttachmentMap`).
32+
33+
- [x] Confirm toolchain compatibility / decide target versions
34+
- [x] Establish current build baseline (Gradle runs on JDK 17; CI uses JDK 17)
35+
- [x] Determine a compatible version set
36+
- [x] Bump core versions: **Kotlin 2.2.21**, **Gradle 8.13**, **AGP 8.7.3**, `compileSdk` 35
37+
- [x] Bump plugins: vanniktech-publish `0.30.0`, kover `0.9.1`, detekt `1.23.8`, dokka `1.9.20`,
38+
buildkonfig `0.17.1`, spotless `7.0.2`; serialization `1.7.3`, coroutines `1.9.0`
39+
- [x] Migrate vanniktech publish DSL → `RELEASE_SIGNING_ENABLED=false` Gradle property (root + plugin)
40+
- [x] Migrate kover 0.9 DSL (removed `koverReport { defaults { mergeWith("release") } }`)
41+
- [x] AGP 8 `namespace` migration for all `android {}` blocks + strip `package` from manifests
42+
(SDK + both samples' shared/androidApp)
43+
- [x] Re-enable `buildFeatures { buildConfig = true }` to preserve the Android public API (`apiCheck`)
44+
- [x] `kotlinOptions``compilerOptions` (SDK + plugin)
45+
- [x] Explicit-API return type fix (`SentryKMP.captureUserFeedback(): Unit`)
46+
- [x] Fix no-op stub targets (js/wasmJs/linux/mingw) test wiring under Kotlin 2.2.20's new shared `web`
47+
source set: exclude Ktor from their test classpaths + disable their test compile/run tasks
48+
- [x] Suppress KGP Xcode-compat warning (`kotlin.apple.xcodeCompatibility.nowarn=true`)
49+
- [x] ktlint 1.x reformat via `spotlessApply` (~160 files) + `.editorconfig` keeping repo conventions
50+
(`function-naming`, `property-naming` disabled)
51+
- [x] Regenerate detekt baseline for detekt 1.23.8 + reformatted code
52+
- [x] Fix Cocoa log-level cinterop type change in `SentryLogConvertersTest` (use `convert`)
53+
- [x] Fix tv/watch/macOS test wrapper missing `ApplePlatformOptions` supertype
54+
- [x] **Verified green:** `:sentry-kotlin-multiplatform:build` (all targets + ~341 tests),
55+
`:sentry-kotlin-multiplatform-gradle-plugin:test`, `apiCheck`, `detekt`, `spotlessApply`
56+
57+
### Phase 0 remaining ✅ COMPLETE
58+
- [x] **Samples build** on the upgraded toolchain (`make buildAppleSamples` steps):
59+
- [x] `./gradlew build -p sentry-samples` green (Android + JVM/Compose desktop + iOS klibs,
60+
both CocoaPods and SPM samples) — no Compose bump needed (`Config.composePluginVersion = 1.9.0`
61+
already Kotlin 2.2.x-compatible)
62+
- [x] `:sentry-samples:kmp-app-cocoapods:shared:podInstall` + `pod update` (Sentry Cocoa
63+
`8.58.2`, CocoaPods 1.16.2)
64+
- [x] `xcodebuild` CocoaPods iOS sample (Debug, iphonesimulator, arm64) → **BUILD SUCCEEDED**
65+
on Xcode 26.1.1
66+
- Note: `sudo xcode-select --switch` step skipped — Xcode 26.1.1 already selected. Only benign
67+
warnings (deployment-target 11.0 vs 12.0+ range; run-script-without-outputs).
68+
69+
---
70+
71+
## Phase 1 — spm4Kmp migration (the goal)
72+
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).
94+
95+
---
96+
97+
## Phase 2 — Samples, CI, cleanup
98+
99+
- [ ] Make the SPM sample the primary `spm4Kmp` validation path; update `Makefile` /
100+
`.github/workflows/kotlin-multiplatform.yml` so `buildAppleSamples` builds the SPM sample instead of
101+
`podInstall`/`pod update`.
102+
- [ ] Narrow/replace `scripts/update-cocoa.sh` to update `Config.Libs.sentryCocoaVersion`, plugin metadata,
103+
and SPM sample lockfiles instead of the SDK podspec.
104+
- [ ] Internal CocoaPods build tooling cleanup (SDK podspec, `generateDummyFramework`/`syncFramework`),
105+
**without** touching consumer-facing Gradle plugin CocoaPods support (`installSentryForCocoapods`,
106+
`CocoapodsAutoInstallExtension`, `CocoaFrameworkLinker`).
107+
108+
---
109+
110+
## Risks to track
111+
- `spm4Kmp` ↔ Kotlin 2.2.21 compatibility (verify at the migration step).
112+
- Cross-cinterop casts may not survive binding regeneration → the PoC spike de-risks this first.
113+
- `apiCheck` does not cover Apple klibs → rely on compile/test/sample gates; the CocoaPods sample building
114+
unchanged is the key ABI-parity signal.
115+
- Sentry Cocoa's SwiftPM product is an xcframework-backed binary package → validate `exportToKotlin = true`.
116+
- WatchOS/tvOS coverage (incl. `watchosArm32`) tested explicitly.
117+
118+
---
119+
120+
## Notes
121+
- Local builds require JDK 17 (`JAVA_HOME=/opt/homebrew/Cellar/openjdk@17/...`); Gradle 8.13 + system Java 24
122+
are incompatible.
123+
- Nothing committed yet — Phase 0 is a clean milestone and a natural commit point before Phase 1.

build.gradle.kts

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import com.diffplug.spotless.LineEnding
22
import com.vanniktech.maven.publish.MavenPublishPlugin
3-
import com.vanniktech.maven.publish.MavenPublishPluginExtension
43
import io.gitlab.arturbosch.detekt.Detekt
54
import org.jetbrains.dokka.gradle.DokkaTask
65
import java.util.zip.ZipFile
@@ -18,7 +17,8 @@ plugins {
1817
id(Config.BuildPlugins.buildConfig).version(Config.BuildPlugins.buildConfigVersion).apply(false)
1918
kotlin(Config.kotlinSerializationPlugin).version(Config.kotlinVersion).apply(false)
2019
id(Config.QualityPlugins.kover).version(Config.QualityPlugins.koverVersion).apply(false)
21-
id(Config.QualityPlugins.binaryCompatibility).version(Config.QualityPlugins.binaryCompatibilityVersion)
20+
id(Config.QualityPlugins.binaryCompatibility)
21+
.version(Config.QualityPlugins.binaryCompatibilityVersion)
2222
.apply(false)
2323
}
2424

@@ -52,18 +52,17 @@ subprojects {
5252
}
5353

5454
afterEvaluate {
55-
val platformDists = project.tasks.filter { task ->
56-
task.name.matches(Regex("(.*)DistZip"))
57-
}.toTypedArray()
55+
val platformDists =
56+
project.tasks
57+
.filter { task ->
58+
task.name.matches(Regex("(.*)DistZip"))
59+
}.toTypedArray()
5860
project.tasks.getByName("distZip").finalizedBy(*platformDists)
5961

62+
// signing is done when uploading files to MC
63+
// via gpg:sign-and-deploy-file (release.kts); disabled here via
64+
// the RELEASE_SIGNING_ENABLED Gradle property (see gradle.properties)
6065
apply<MavenPublishPlugin>()
61-
62-
configure<MavenPublishPluginExtension> {
63-
// signing is done when uploading files to MC
64-
// via gpg:sign-and-deploy-file (release.kts)
65-
releaseSigningEnabled = false
66-
}
6766
}
6867
}
6968
}
@@ -78,7 +77,11 @@ tasks.register("validateDistributions") {
7877
}
7978

8079
private fun Project.validateKotlinMultiplatformCoreArtifacts() {
81-
val distributionDir = project.layout.buildDirectory.dir("distributions").get().asFile
80+
val distributionDir =
81+
project.layout.buildDirectory
82+
.dir("distributions")
83+
.get()
84+
.asFile
8285
val expectedNumOfFiles = 20
8386
val filesList = distributionDir.listFiles()
8487
val actualNumOfFiles = filesList?.size ?: 0
@@ -90,34 +93,46 @@ private fun Project.validateKotlinMultiplatformCoreArtifacts() {
9093
}
9194

9295
val baseFileName = "sentry-kotlin-multiplatform"
93-
val platforms = listOf(
94-
"watchosx64", "watchossimulatorarm64", "watchosarm64", "watchosarm32",
95-
"tvosx64", "tvossimulatorarm64", "tvosarm64",
96-
"macosx64", "macosarm64",
97-
"jvm",
98-
"iosx64", "iossimulatorarm64", "iosarm64",
99-
"android",
100-
"js",
101-
"wasm-js",
102-
"linuxx64", "linuxarm64",
103-
"mingwx64"
104-
)
105-
106-
val artifactPaths = buildList {
107-
add(distributionDir.resolve("$baseFileName-$version.zip"))
108-
addAll(
109-
platforms.map { platform ->
110-
distributionDir.resolve("$baseFileName-$platform-$version.zip")
111-
}
96+
val platforms =
97+
listOf(
98+
"watchosx64",
99+
"watchossimulatorarm64",
100+
"watchosarm64",
101+
"watchosarm32",
102+
"tvosx64",
103+
"tvossimulatorarm64",
104+
"tvosarm64",
105+
"macosx64",
106+
"macosarm64",
107+
"jvm",
108+
"iosx64",
109+
"iossimulatorarm64",
110+
"iosarm64",
111+
"android",
112+
"js",
113+
"wasm-js",
114+
"linuxx64",
115+
"linuxarm64",
116+
"mingwx64",
112117
)
113-
}
114118

115-
val commonRequiredEntries = listOf(
116-
"javadoc",
117-
"sources",
118-
"module",
119-
"pom-default.xml"
120-
)
119+
val artifactPaths =
120+
buildList {
121+
add(distributionDir.resolve("$baseFileName-$version.zip"))
122+
addAll(
123+
platforms.map { platform ->
124+
distributionDir.resolve("$baseFileName-$platform-$version.zip")
125+
},
126+
)
127+
}
128+
129+
val commonRequiredEntries =
130+
listOf(
131+
"javadoc",
132+
"sources",
133+
"module",
134+
"pom-default.xml",
135+
)
121136

122137
artifactPaths.forEach { artifactFile ->
123138
if (!artifactFile.exists()) {
@@ -128,7 +143,12 @@ private fun Project.validateKotlinMultiplatformCoreArtifacts() {
128143
}
129144

130145
ZipFile(artifactFile).use { zip ->
131-
val entries = zip.entries().asSequence().map { it.name }.toList()
146+
val entries =
147+
zip
148+
.entries()
149+
.asSequence()
150+
.map { it.name }
151+
.toList()
132152

133153
commonRequiredEntries.forEach { requiredEntry ->
134154
if (entries.none { it.contains(requiredEntry) }) {
@@ -146,7 +166,9 @@ private fun Project.validateKotlinMultiplatformCoreArtifacts() {
146166
val expectedNumOfKlibFiles = 3
147167
val actualKlibFiles = entries.count { it.contains("klib") }
148168
if (actualKlibFiles != expectedNumOfKlibFiles) {
149-
throw GradleException("❌ Expected $expectedNumOfKlibFiles klib files in ${artifactFile.name}, but found $actualKlibFiles")
169+
throw GradleException(
170+
"❌ Expected $expectedNumOfKlibFiles klib files in ${artifactFile.name}, but found $actualKlibFiles",
171+
)
150172
} else {
151173
println("✅ Found $expectedNumOfKlibFiles klib files in ${artifactFile.name}")
152174
}
@@ -197,7 +219,7 @@ val detektBaselineFilePath = "$rootDir/config/detekt/baseline.xml"
197219

198220
detekt {
199221
buildUponDefaultConfig = true
200-
config = files(detektConfigFilePath)
222+
config.setFrom(files(detektConfigFilePath))
201223
baseline = file(detektBaselineFilePath)
202224
}
203225

buildSrc/src/main/java/Config.kt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
object Config {
2-
val agpVersion = "7.4.2"
3-
val kotlinVersion = "2.1.21"
4-
val composePluginVersion = "1.8.0"
5-
val gradleMavenPublishPluginVersion = "0.18.0"
2+
val agpVersion = "8.7.3"
3+
val kotlinVersion = "2.2.21"
4+
val composePluginVersion = "1.9.0"
5+
val gradleMavenPublishPluginVersion = "0.30.0"
66

77
val multiplatform = "multiplatform"
88
val cocoapods = "native.cocoapods"
@@ -12,20 +12,20 @@ object Config {
1212
val androidGradle = "com.android.library"
1313
val kotlinSerializationPlugin = "plugin.serialization"
1414
val dokka = "org.jetbrains.dokka"
15-
val dokkaVersion = "1.8.10"
15+
val dokkaVersion = "1.9.20"
1616

1717
object BuildPlugins {
1818
val buildConfig = "com.codingfeline.buildkonfig"
19-
val buildConfigVersion = "0.13.3"
19+
val buildConfigVersion = "0.17.1"
2020
}
2121

2222
object QualityPlugins {
2323
val spotless = "com.diffplug.spotless"
24-
val spotlessVersion = "6.11.0"
24+
val spotlessVersion = "7.0.2"
2525
val kover = "org.jetbrains.kotlinx.kover"
26-
val koverVersion = "0.7.3"
26+
val koverVersion = "0.9.1"
2727
val detekt = "io.gitlab.arturbosch.detekt"
28-
val detektVersion = "1.22.0"
28+
val detektVersion = "1.23.8"
2929
val binaryCompatibility = "org.jetbrains.kotlinx.binary-compatibility-validator"
3030
val binaryCompatibilityVersion = "0.18.0"
3131
}
@@ -51,9 +51,9 @@ object Config {
5151
val kotlinCommon = "org.jetbrains.kotlin:kotlin-test-common"
5252
val kotlinCommonAnnotation = "org.jetbrains.kotlin:kotlin-test-annotations-common"
5353
val kotlinJunit = "org.jetbrains.kotlin:kotlin-test-junit"
54-
val kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-RC"
55-
val kotlinCoroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0-RC"
56-
val kotlinxSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0"
54+
val kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0"
55+
val kotlinCoroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0"
56+
val kotlinxSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
5757

5858
val ktorClientCore = "io.ktor:ktor-client-core:2.3.6"
5959
val ktorClientSerialization = "io.ktor:ktor-client-serialization:2.3.6"
@@ -66,7 +66,7 @@ object Config {
6666
}
6767

6868
object Android {
69-
private val sdkVersion = 33
69+
private val sdkVersion = 35
7070

7171
val minSdkVersion = 21
7272
val targetSdkVersion = sdkVersion

0 commit comments

Comments
 (0)