From 0bb9c8b4094fe3726c63fad081953b109c479c5e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 9 Jun 2026 21:43:36 +0200 Subject: [PATCH 1/6] feat(plugin): Auto-install Sentry Cocoa via spm4Kmp When a consumer applies the spm4Kmp plugin (io.github.frankois944.spmForKmp) alongside the Sentry KMP Gradle plugin, the matching Sentry Cocoa version is now added to their Apple targets automatically, so they no longer declare the Sentry Swift package manually or rely on the fragile DerivedData framework linker. - Add Spm4KmpAutoInstallExtension (enabled + sentryCocoaVersion, default BuildConfig.SentryCocoaVersion), exposed as sentryKmp { autoInstall.spm { } } - installSentryForSpm4Kmp adds remotePackageVersion(getsentry/sentry-cocoa, products = { add("Sentry") }) to each Apple target via swiftPackageConfig (link-only: the published SDK klib already carries the Sentry cinterop bindings) - Register during the configuration phase via plugins.withId, since spm4Kmp consumes swiftPackageConfig before afterEvaluate; idempotent + opt-out guarded - Skip the manual CocoaFrameworkLinker when CocoaPods or spm4Kmp is applied; the linker stays as the fallback for plain SPM-in-Xcode consumers - compileOnly against the spm4Kmp DSL (gradlePluginPortal); request Java 17 deps on the compile/test classpaths while keeping Java 11 plugin bytecode - Convert the kmp-app-spm sample to apply spm4Kmp and use the auto-install - Tests + README/migration docs Verified: plugin compileKotlin + test, sample iOS link generates its own sentryCocoa cinterop, SPM sample xcodebuild BUILD SUCCEEDED, spotless/detekt green. Co-Authored-By: Claude Opus 4.8 Co-authored-by: Cursor --- README.md | 32 ++++++++ .../build.gradle.kts | 21 +++++ .../gradle/libs.versions.toml | 2 + .../settings.gradle.kts | 2 + .../gradle/AutoInstallExtension.kt | 7 +- .../multiplatform/gradle/SentryPlugin.kt | 81 ++++++++++++++++++- .../gradle/Spm4KmpAutoInstallExtension.kt | 34 ++++++++ .../multiplatform/gradle/SentryPluginTest.kt | 69 ++++++++++++++++ .../kmp-app-spm/shared/build.gradle.kts | 7 +- 9 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/Spm4KmpAutoInstallExtension.kt diff --git a/README.md b/README.md index ca2cc58ff..a9b66dc0c 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,38 @@ Use the Kotlin Multiplatform and Cocoa SDK combinations listed in the table belo For detailed usage, check out the [Kotlin Multiplatform Documentation](https://docs.sentry.io/platforms/kotlin-multiplatform/). +### Apple linking via spm4Kmp (Gradle plugin) + +If you apply the [spm4Kmp](https://github.com/frankois944/spm4Kmp) plugin (`io.github.frankois944.spmForKmp`) +alongside the Sentry Kotlin Multiplatform Gradle plugin, the matching Sentry Cocoa version is added to +your Apple targets automatically — you don't need to declare the Sentry Swift package yourself: + +```kotlin +plugins { + kotlin("multiplatform") + id("io.github.frankois944.spmForKmp") + id("io.sentry.kotlin.multiplatform.gradle") +} +``` + +You can override the version or opt out (for example if you configure the Sentry Swift package +manually): + +```kotlin +sentryKmp { + autoInstall { + spm { + // enabled = false // opt out of the automatic Sentry Cocoa Swift package + // sentryCocoaVersion = "8.58.2" // override the default version + } + } +} +``` + +Consumers that don't use spm4Kmp keep the existing behavior: the CocoaPods auto-install (when the +Kotlin CocoaPods plugin is applied) or the `linker { frameworkPath / xcodeprojPath }` fallback for +plain SPM-in-Xcode setups. + ## Samples For detailed information on how to build and run the samples, check out our `README.md` in the diff --git a/sentry-kotlin-multiplatform-gradle-plugin/build.gradle.kts b/sentry-kotlin-multiplatform-gradle-plugin/build.gradle.kts index 41aea54be..700bdd92e 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/build.gradle.kts +++ b/sentry-kotlin-multiplatform-gradle-plugin/build.gradle.kts @@ -1,4 +1,5 @@ import io.gitlab.arturbosch.detekt.Detekt +import org.gradle.api.attributes.java.TargetJvmVersion import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -20,8 +21,13 @@ dependencies { compileOnly(kotlin("stdlib")) compileOnly(gradleApi()) compileOnly(kotlin("gradle-plugin")) + // Compile against the spm4Kmp DSL so we can auto-configure the Sentry Cocoa Swift + // package when a consumer applies the spm4Kmp plugin. compileOnly: only used when + // the consumer also brings the plugin onto the classpath. + compileOnly(libs.spmForKmp) testImplementation(kotlin("gradle-plugin")) + testImplementation(libs.spmForKmp) testImplementation(libs.junit) testImplementation(libs.junit.params) testImplementation(libs.mockk) @@ -37,6 +43,21 @@ java { targetCompatibility = JavaVersion.VERSION_11 } +// spm4Kmp's implementation artifact is published for Java 17. It is only a compileOnly dependency +// (used solely when a consumer also applies the spm4Kmp plugin, which itself requires JDK 17), so we +// request Java 17-compatible variants on the compile/test classpaths while still producing Java 11 +// bytecode. This keeps the published plugin runnable on JDK 11 for consumers that don't use spm4Kmp. +listOf("compileClasspath", "testCompileClasspath", "testRuntimeClasspath").forEach { configurationName -> + configurations.named(configurationName).configure { + attributes { + attribute( + TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, + JavaVersion.VERSION_17.majorVersion.toInt(), + ) + } + } +} + tasks.withType().configureEach { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } gradlePlugin { diff --git a/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml b/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml index d1c79ffe5..24a04745c 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml +++ b/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml @@ -5,6 +5,7 @@ pluginPublish = "1.3.1" buildConfig = "5.5.1" vanniktechPublish = "0.30.0" kover = "0.9.1" +spmForKmp = "1.9.2" [plugins] detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt"} @@ -19,3 +20,4 @@ junit = "org.junit.jupiter:junit-jupiter-api:5.10.3" junit-params = "org.junit.jupiter:junit-jupiter-params:5.10.3" mockk = "io.mockk:mockk:1.13.12" truth = { module = "com.google.truth:truth", version = "1.4.4" } +spmForKmp = { module = "io.github.frankois944.spmForKmp:io.github.frankois944.spmForKmp.gradle.plugin", version.ref = "spmForKmp" } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/settings.gradle.kts b/sentry-kotlin-multiplatform-gradle-plugin/settings.gradle.kts index 4ef43ffb2..bb8484ee6 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/settings.gradle.kts +++ b/sentry-kotlin-multiplatform-gradle-plugin/settings.gradle.kts @@ -9,6 +9,8 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + // Hosts the spm4Kmp plugin marker + implementation (compiled against for spm4Kmp auto-install) + gradlePluginPortal() } } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt index e0ec94b3b..bc960fb58 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt @@ -13,8 +13,8 @@ abstract class AutoInstallExtension private val objects = project.objects /** - * Enable auto-installation of the Sentry dependencies through [CocoapodsAutoInstallExtension] - * and [SourceSetAutoInstallExtension]. + * Enable auto-installation of the Sentry dependencies through [CocoapodsAutoInstallExtension], + * [Spm4KmpAutoInstallExtension] and [SourceSetAutoInstallExtension]. * * Disabling this will prevent the plugin from auto installing any dependency. * @@ -25,6 +25,9 @@ abstract class AutoInstallExtension val cocoapods: CocoapodsAutoInstallExtension = objects.newInstance(CocoapodsAutoInstallExtension::class.java, project) + val spm: Spm4KmpAutoInstallExtension = + objects.newInstance(Spm4KmpAutoInstallExtension::class.java, project) + val commonMain: SourceSetAutoInstallExtension = objects.newInstance(SourceSetAutoInstallExtension::class.java, project) } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt index da337b3bd..2d71d6e77 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt @@ -1,5 +1,6 @@ package io.sentry.kotlin.multiplatform.gradle +import io.github.frankois944.spmForKmp.swiftPackageConfig import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project @@ -11,13 +12,16 @@ import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.konan.target.HostManager import org.slf4j.LoggerFactory +import java.net.URI internal const val SENTRY_EXTENSION_NAME = "sentryKmp" internal const val LINKER_EXTENSION_NAME = "linker" internal const val AUTO_INSTALL_EXTENSION_NAME = "autoInstall" internal const val COCOAPODS_AUTO_INSTALL_EXTENSION_NAME = "cocoapods" +internal const val SPM4KMP_AUTO_INSTALL_EXTENSION_NAME = "spm" internal const val COMMON_MAIN_AUTO_INSTALL_EXTENSION_NAME = "commonMain" internal const val KOTLIN_EXTENSION_NAME = "kotlin" +internal const val SPM4KMP_PLUGIN_ID = "io.github.frankois944.spmForKmp" @Suppress("unused") class SentryPlugin : Plugin { @@ -35,11 +39,22 @@ class SentryPlugin : Plugin { COCOAPODS_AUTO_INSTALL_EXTENSION_NAME, sentryExtension.autoInstall.cocoapods, ) + project.extensions.add( + SPM4KMP_AUTO_INSTALL_EXTENSION_NAME, + sentryExtension.autoInstall.spm, + ) project.extensions.add( COMMON_MAIN_AUTO_INSTALL_EXTENSION_NAME, sentryExtension.autoInstall.commonMain, ) + // spm4Kmp consumes its swiftPackageConfig during the configuration phase (before + // afterEvaluate), so the Sentry package must be registered as soon as the spm4Kmp plugin + // is applied rather than in executeConfiguration's afterEvaluate. + project.plugins.withId(SPM4KMP_PLUGIN_ID) { + project.installSentryForSpm4Kmp(sentryExtension.autoInstall) + } + afterEvaluate { executeConfiguration(project) } @@ -52,6 +67,7 @@ class SentryPlugin : Plugin { val sentryExtension = project.extensions.getByType(SentryExtension::class.java) val hasCocoapodsPlugin = project.plugins.findPlugin(KotlinCocoapodsPlugin::class.java) != null + val hasSpm4KmpPlugin = project.plugins.hasPlugin(SPM4KMP_PLUGIN_ID) if (sentryExtension.autoInstall.enabled.get()) { val autoInstall = sentryExtension.autoInstall @@ -63,9 +79,19 @@ class SentryPlugin : Plugin { if (hasCocoapodsPlugin && autoInstall.cocoapods.enabled.get() && hostIsMac) { project.installSentryForCocoapods(autoInstall.cocoapods) } + + // The spm4Kmp install is wired in apply() via plugins.withId, which fires regardless of + // plugin application order, so it is intentionally not invoked here. hasSpm4KmpPlugin is + // only used below to skip the manual DerivedData linker. } - maybeLinkCocoaFramework(project, hasCocoapodsPlugin, hostIsMac) + // When CocoaPods or spm4Kmp provide the Sentry framework, they also handle linking, so the + // manual DerivedData-based linker is only needed as a fallback for plain SPM users. + maybeLinkCocoaFramework( + project, + frameworkProvidedExternally = hasCocoapodsPlugin || hasSpm4KmpPlugin, + hostIsMac, + ) } companion object { @@ -77,10 +103,10 @@ class SentryPlugin : Plugin { private fun maybeLinkCocoaFramework( project: Project, - hasCocoapods: Boolean, + frameworkProvidedExternally: Boolean, hostIsMac: Boolean, ) { - if (hostIsMac && !hasCocoapods) { + if (hostIsMac && !frameworkProvidedExternally) { // Register a task graph listener so that we only configure Cocoa framework linking // if at least one Apple target task is part of the requested task graph. This avoids // executing the (potentially expensive) path-resolution logic when the build is only @@ -187,3 +213,52 @@ internal fun Project.installSentryForCocoapods(cocoapodsAutoInstallExtension: Co } } } + +internal const val SENTRY_COCOA_CINTEROP_NAME = "sentryCocoa" +private const val SENTRY_COCOA_GIT_URL = "https://github.com/getsentry/sentry-cocoa.git" + +/** + * Adds the Sentry Cocoa Swift package to every Apple target via the spm4Kmp DSL so consumers don't + * have to declare it themselves. Idempotent: skips any target that already has a [SENTRY_COCOA_CINTEROP_NAME] + * cinterop (e.g. a user-defined Sentry config) and re-running is a no-op. + */ +internal fun Project.installSentryForSpm4Kmp( + autoInstall: AutoInstallExtension, + hostIsMac: Boolean = HostManager.hostIsMac, +) { + val kmpExtension = extensions.findByName(KOTLIN_EXTENSION_NAME) + if (kmpExtension !is KotlinMultiplatformExtension || !hostIsMac) { + logger.info("Skipping spm4Kmp installation.") + return + } + + kmpExtension.appleTargets().configureEach { target -> + if (!autoInstall.enabled.get() || !autoInstall.spm.enabled.get()) { + return@configureEach + } + + val mainCompilation = target.compilations.findByName("main") + if (mainCompilation?.cinterops?.findByName(SENTRY_COCOA_CINTEROP_NAME) != null) { + logger.info( + "Sentry Cocoa Swift package already configured for ${target.name}. " + + "Skipping spm4Kmp auto installation.", + ) + return@configureEach + } + + target.swiftPackageConfig(cinteropName = SENTRY_COCOA_CINTEROP_NAME) { + dependency { + remotePackageVersion( + url = URI(SENTRY_COCOA_GIT_URL), + version = autoInstall.spm.sentryCocoaVersion.get(), + products = { + // exportToKotlin defaults to false (link only). The published KMP SDK klib + // already contains the Sentry cinterop bindings, so consumers only need the + // framework available at link time. + add("Sentry") + }, + ) + } + } + } +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/Spm4KmpAutoInstallExtension.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/Spm4KmpAutoInstallExtension.kt new file mode 100644 index 000000000..02400bb97 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/Spm4KmpAutoInstallExtension.kt @@ -0,0 +1,34 @@ +package io.sentry.kotlin.multiplatform.gradle + +import io.sentry.BuildConfig +import org.gradle.api.Project +import org.gradle.api.provider.Property +import javax.inject.Inject + +@Suppress("UnnecessaryAbstractClass") +abstract class Spm4KmpAutoInstallExtension + @Inject + constructor( + project: Project, + ) { + private val objects = project.objects + + /** + * Enable auto-installation of the Sentry Cocoa SDK Swift package via spm4Kmp. + * + * If the spm4Kmp plugin (io.github.frankois944.spmForKmp) is applied and no existing Sentry + * Swift package configuration exists, the Sentry-Cocoa SDK will be added to every Apple target. + * + * Defaults to true. + */ + val enabled: Property = objects.property(Boolean::class.java).convention(true) + + /** + * Overrides default Sentry Cocoa version. + * + * Defaults to the version used in the latest KMP SDK. Must be an exact version since the Swift + * Package Manager resolves remote packages by exact version. + */ + val sentryCocoaVersion: Property = + objects.property(String::class.java).convention(BuildConfig.SentryCocoaVersion) + } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt index 052634e5f..3c05f1811 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt @@ -2,6 +2,7 @@ package io.sentry.kotlin.multiplatform.gradle import io.sentry.BuildConfig import org.gradle.api.GradleException +import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.plugins.ExtensionAware import org.gradle.testfixtures.ProjectBuilder import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension @@ -64,6 +65,14 @@ class SentryPluginTest { assertNotNull(project.extensions.getByName("commonMain")) } + @Test + fun `extension spm is created correctly`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assertNotNull(project.extensions.getByName("spm")) + } + @Test fun `plugin applies extensions correctly`() { val project = ProjectBuilder.builder().build() @@ -73,6 +82,7 @@ class SentryPluginTest { assertNotNull(project.extensions.getByName("linker")) assertNotNull(project.extensions.getByName("autoInstall")) assertNotNull(project.extensions.getByName("cocoapods")) + assertNotNull(project.extensions.getByName("spm")) assertNotNull(project.extensions.getByName("commonMain")) } @@ -240,6 +250,65 @@ class SentryPluginTest { assertEquals(cocoapodsExtension.pods.getByName("Sentry").version, "custom version") } + @Test + fun `default cocoa version is set in spm extension`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val spmExtension = project.extensions.getByName("spm") as Spm4KmpAutoInstallExtension + assertEquals(BuildConfig.SentryCocoaVersion, spmExtension.sentryCocoaVersion.get()) + } + + @Test + fun `custom cocoa version overrides default in spm extension`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension + autoInstallExtension.spm.sentryCocoaVersion.set("9.9.9") + + assertEquals("9.9.9", autoInstallExtension.spm.sentryCocoaVersion.get()) + } + + @Test + fun `install Sentry Swift package via spm4Kmp when plugin is applied`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.github.frankois944.spmForKmp") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kmpExtension.iosArm64() + + val autoInstall = project.extensions.getByName("autoInstall") as AutoInstallExtension + project.installSentryForSpm4Kmp(autoInstall, hostIsMac = true) + + // spm4Kmp keys per-target config as "_" in its container. + val swiftPackages = + project.extensions.getByName("swiftPackageConfig") as NamedDomainObjectContainer<*> + assertNotNull(swiftPackages.findByName("${SENTRY_COCOA_CINTEROP_NAME}_IosArm64")) + } + + @Test + fun `do not install Sentry Swift package when spm auto install is disabled`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.github.frankois944.spmForKmp") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val autoInstall = project.extensions.getByName("autoInstall") as AutoInstallExtension + autoInstall.spm.enabled.set(false) + + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kmpExtension.iosArm64() + + project.installSentryForSpm4Kmp(autoInstall, hostIsMac = true) + + val swiftPackages = + project.extensions.getByName("swiftPackageConfig") as NamedDomainObjectContainer<*> + assertNull(swiftPackages.findByName("${SENTRY_COCOA_CINTEROP_NAME}_IosArm64")) + } + @Test fun `do not install Sentry pod if host is not mac`() { val project = ProjectBuilder.builder().build() diff --git a/sentry-samples/kmp-app-spm/shared/build.gradle.kts b/sentry-samples/kmp-app-spm/shared/build.gradle.kts index b639ad0ad..b57a43843 100644 --- a/sentry-samples/kmp-app-spm/shared/build.gradle.kts +++ b/sentry-samples/kmp-app-spm/shared/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { kotlin("multiplatform") id("com.android.library") + id(Config.spmForKmp) id("io.sentry.kotlin.multiplatform.gradle") } @@ -57,8 +58,10 @@ android { } } -// disabling autoInstall because we are using project(":sentry-kotlin-multiplatform") directly -// for our sample apps +// We depend on project(":sentry-kotlin-multiplatform") directly, so the commonMain auto-install +// (which would add the published SDK dependency) is disabled. The spm4Kmp auto-install stays enabled +// to exercise it: applying the spm4Kmp plugin makes the Sentry KMP plugin add the matching +// Sentry Cocoa Swift package to the Apple targets automatically. sentryKmp { autoInstall.commonMain.enabled = false } From ef0514acdd548c9bd3034dcf0348d8d2e8c458a0 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 9 Jun 2026 21:58:08 +0200 Subject: [PATCH 2/6] chore(plugin): Bump spm4Kmp to 1.9.3 Align the plugin's compileOnly spm4Kmp DSL dependency with the SDK's Config.spmForKmpVersion (1.9.3), which adds watchosArm32 (armv7k) support. Co-Authored-By: Claude Opus 4.8 Co-authored-by: Cursor --- .../gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml b/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml index 24a04745c..722d48101 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml +++ b/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml @@ -5,7 +5,7 @@ pluginPublish = "1.3.1" buildConfig = "5.5.1" vanniktechPublish = "0.30.0" kover = "0.9.1" -spmForKmp = "1.9.2" +spmForKmp = "1.9.3" [plugins] detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt"} From baa8214e66e225a7856f0be8d748676729289e83 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 10 Jun 2026 13:18:07 +0200 Subject: [PATCH 3/6] docs: Add changelog entry for spm4Kmp auto-install Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a3f808a0..5ac49723b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Auto-install Sentry Cocoa for Apple targets when the spm4Kmp plugin (`io.github.frankois944.spmForKmp`) is applied ([#559](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/559)) + ### Dependencies - Bump Kotlin from `2.1.21` to `2.2.21` and Gradle from `8.6` to `8.13` ([#556](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/556)) From aa4431844df3d338cda117a09fdd6fa645ba8e87 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 10 Jun 2026 16:59:36 +0200 Subject: [PATCH 4/6] ref(samples): Drop the Xcode-side sentry-cocoa package from the SPM sample With the spm4Kmp auto-install in place, the static Sentry product is linked into the dynamic shared.framework (all Sentry ObjC classes are defined symbols there), so embedding Sentry-Dynamic in the iosApp would load a second copy of every Sentry class at runtime. Remove the XCRemoteSwiftPackageReference, its Package.resolved pins, and the pbxproj editing in update-cocoa.sh; the Sentry Cocoa version is now single-sourced from Config.kt and the plugin's gradle.properties. Verified via xcodebuild (BUILD SUCCEEDED) and a simulator launch of the sample app. Co-Authored-By: Claude Fable 5 --- scripts/update-cocoa.sh | 23 ++----------------- .../xcshareddata/swiftpm/Package.resolved | 14 ----------- .../iosApp.xcodeproj/project.pbxproj | 22 ------------------ .../xcshareddata/swiftpm/Package.resolved | 15 ------------ 4 files changed, 2 insertions(+), 72 deletions(-) delete mode 100644 sentry-samples/kmp-app-spm/iosApp.xcodeproj/.swiftpm/xcode/package.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/scripts/update-cocoa.sh b/scripts/update-cocoa.sh index 73e330ccc..362a45b8e 100755 --- a/scripts/update-cocoa.sh +++ b/scripts/update-cocoa.sh @@ -4,18 +4,12 @@ cd $(dirname "$0")/../ config_file='buildSrc/src/main/java/Config.kt' plugin_properties_file='sentry-kotlin-multiplatform-gradle-plugin/gradle.properties' -spm_sample_pbxproj_file='sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.pbxproj' -spm_sample_project='sentry-samples/kmp-app-spm/iosApp.xcodeproj' -spm_sample_scheme='iosApp' config_content=$(cat $config_file) plugin_properties_content=$(cat $plugin_properties_file) -pbxproj_content=$(cat $spm_sample_pbxproj_file) config_regex='(sentryCocoaVersion *= *)"([0-9\.]+)"' plugin_properties_regex='(sentryCocoaVersion *= *)([0-9\.]+)' -# Matches the sentry-cocoa SwiftPM pin in the SPM sample, e.g. `version = 8.58.2;` -pbxproj_regex='(version = )([0-9\.]+)(;)' if ! [[ $config_content =~ $config_regex ]]; then echo "Failed to find the Cocoa version in $config_file" @@ -34,13 +28,6 @@ fi plugin_properties_whole_match=${BASH_REMATCH[0]} plugin_properties_var_name=${BASH_REMATCH[1]} -if ! [[ $pbxproj_content =~ $pbxproj_regex ]]; then - echo "Failed to find the sentry-cocoa SwiftPM version in $spm_sample_pbxproj_file" - exit 1 -fi - -pbxproj_whole_match=${BASH_REMATCH[0]} - case $1 in get-version) # We only require to return the version number of one of the files @@ -58,14 +45,8 @@ set-version) newValue="${plugin_properties_var_name}$2" echo "${plugin_properties_content/${plugin_properties_whole_match}/$newValue}" >$plugin_properties_file - # Update the sentry-cocoa SwiftPM pin in the SPM sample Xcode project - newValue="version = $2;" - echo "${pbxproj_content/${pbxproj_whole_match}/$newValue}" >$spm_sample_pbxproj_file - - # Refresh the SPM sample lockfiles (Package.resolved) so the pinned revision - # matches the new version. - echo "Resolving SwiftPM dependencies for the SPM sample..." - xcodebuild -resolvePackageDependencies -project $spm_sample_project -scheme $spm_sample_scheme + # The SPM sample needs no update: it gets Sentry Cocoa through the Gradle plugin's + # spm4Kmp auto-install, which is versioned from the two files above. ;; *) echo "Unknown argument $1" diff --git a/sentry-samples/kmp-app-spm/iosApp.xcodeproj/.swiftpm/xcode/package.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sentry-samples/kmp-app-spm/iosApp.xcodeproj/.swiftpm/xcode/package.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 50e76216a..000000000 --- a/sentry-samples/kmp-app-spm/iosApp.xcodeproj/.swiftpm/xcode/package.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pins" : [ - { - "identity" : "sentry-cocoa", - "kind" : "remoteSourceControl", - "location" : "https://github.com/getsentry/sentry-cocoa.git", - "state" : { - "revision" : "b847a202a517a90763e8fd0656d8028aeee7b78d", - "version" : "8.20.0" - } - } - ], - "version" : 2 -} diff --git a/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.pbxproj b/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.pbxproj index b82207755..50699cc67 100644 --- a/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.pbxproj +++ b/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 243652EF29F34FF500FD902A /* sentry.png in Resources */ = {isa = PBXBuildFile; fileRef = 243652EE29F34FF500FD902A /* sentry.png */; }; - 24E1CB3F2C17E00200F78D70 /* Sentry-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 24E1CB3E2C17E00200F78D70 /* Sentry-Dynamic */; }; 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; /* End PBXBuildFile section */ @@ -44,7 +43,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 24E1CB3F2C17E00200F78D70 /* Sentry-Dynamic in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -117,7 +115,6 @@ ); name = iosApp; packageProductDependencies = ( - 24E1CB3E2C17E00200F78D70 /* Sentry-Dynamic */, ); productName = NSExceptionKtSample; productReference = 7555FF7B242A565900829871 /* iosApp.app */; @@ -148,7 +145,6 @@ ); mainGroup = 7555FF72242A565900829871; packageReferences = ( - 24E1CB3D2C17E00200F78D70 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, ); productRefGroup = 7555FF7C242A565900829871 /* Products */; projectDirPath = ""; @@ -418,24 +414,6 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - 24E1CB3D2C17E00200F78D70 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; - requirement = { - kind = exactVersion; - version = 8.58.2; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 24E1CB3E2C17E00200F78D70 /* Sentry-Dynamic */ = { - isa = XCSwiftPackageProductDependency; - package = 24E1CB3D2C17E00200F78D70 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; - productName = "Sentry-Dynamic"; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = 7555FF73242A565900829871 /* Project object */; } diff --git a/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 022f34a4a..000000000 --- a/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "7b5e54f81ac1ebaa640945691cb38c371b637198701f04fba811702fc8e7067e", - "pins" : [ - { - "identity" : "sentry-cocoa", - "kind" : "remoteSourceControl", - "location" : "https://github.com/getsentry/sentry-cocoa.git", - "state" : { - "revision" : "cf44aa8cb4147f39e698c1f28be0b6b2c89f79d2", - "version" : "8.58.2" - } - } - ], - "version" : 3 -} From d941ef40bc90d2df845e43abc8de5a040a46f702 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 11 Jun 2026 13:26:02 +0200 Subject: [PATCH 5/6] fix(plugin): Harden spm4Kmp auto-install against plugin order and opt-out Address review feedback on the spm4Kmp auto-install: - Gate the spm4Kmp install on both the spm4Kmp and Kotlin Multiplatform plugins via nested plugins.withId, so applying KMP after spm4Kmp no longer silently skips the Sentry Swift package registration. - Skip the manual DerivedData fallback linker only when the Sentry Swift package is actually registered with spm4Kmp (auto-installed or user-defined sentryCocoa config), instead of whenever the spm4Kmp plugin is applied. Consumers that use spm4Kmp for other packages but opt out of the Sentry spm auto-install keep the fallback linker. - Document that the sentryKmp spm opt-out and version override must be configured before the kotlin block declares Apple targets, since the Swift package is registered at target creation time. Co-Authored-By: Claude Fable 5 --- README.md | 5 ++ .../multiplatform/gradle/SentryPlugin.kt | 42 ++++++++++-- .../gradle/Spm4KmpAutoInstallExtension.kt | 8 +++ .../multiplatform/gradle/SentryPluginTest.kt | 66 +++++++++++++++++++ 4 files changed, 114 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a9b66dc0c..70b67e108 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,11 @@ sentryKmp { } ``` +> [!NOTE] +> The Sentry Swift package is registered as soon as the Apple targets are created, so place the +> `sentryKmp { }` block **before** the `kotlin { }` block — otherwise the opt-out and version +> override have no effect. + Consumers that don't use spm4Kmp keep the existing behavior: the CocoaPods auto-install (when the Kotlin CocoaPods plugin is applied) or the `linker { frameworkPath / xcodeprojPath }` fallback for plain SPM-in-Xcode setups. diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt index 9104de665..1797de8f9 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt @@ -2,6 +2,7 @@ package io.sentry.kotlin.multiplatform.gradle import io.github.frankois944.spmForKmp.swiftPackageConfig import org.gradle.api.GradleException +import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.execution.TaskExecutionGraph @@ -21,7 +22,9 @@ internal const val COCOAPODS_AUTO_INSTALL_EXTENSION_NAME = "cocoapods" internal const val SPM4KMP_AUTO_INSTALL_EXTENSION_NAME = "spm" internal const val COMMON_MAIN_AUTO_INSTALL_EXTENSION_NAME = "commonMain" internal const val KOTLIN_EXTENSION_NAME = "kotlin" +internal const val KOTLIN_MULTIPLATFORM_PLUGIN_ID = "org.jetbrains.kotlin.multiplatform" internal const val SPM4KMP_PLUGIN_ID = "io.github.frankois944.spmForKmp" +internal const val SPM4KMP_SWIFT_PACKAGE_CONFIG_EXTENSION_NAME = "swiftPackageConfig" @Suppress("unused") class SentryPlugin : Plugin { @@ -50,9 +53,13 @@ class SentryPlugin : Plugin { // spm4Kmp consumes its swiftPackageConfig during the configuration phase (before // afterEvaluate), so the Sentry package must be registered as soon as the spm4Kmp plugin - // is applied rather than in executeConfiguration's afterEvaluate. + // is applied rather than in executeConfiguration's afterEvaluate. The nested withId makes + // this robust to plugin application order: the install only runs once both the spm4Kmp + // and Kotlin Multiplatform plugins are present. project.plugins.withId(SPM4KMP_PLUGIN_ID) { - project.installSentryForSpm4Kmp(sentryExtension.autoInstall) + project.plugins.withId(KOTLIN_MULTIPLATFORM_PLUGIN_ID) { + project.installSentryForSpm4Kmp(sentryExtension.autoInstall) + } } afterEvaluate { @@ -67,7 +74,6 @@ class SentryPlugin : Plugin { val sentryExtension = project.extensions.getByType(SentryExtension::class.java) val hasCocoapodsPlugin = project.plugins.findPlugin(KotlinCocoapodsPlugin::class.java) != null - val hasSpm4KmpPlugin = project.plugins.hasPlugin(SPM4KMP_PLUGIN_ID) if (sentryExtension.autoInstall.enabled.get()) { val autoInstall = sentryExtension.autoInstall @@ -81,15 +87,17 @@ class SentryPlugin : Plugin { } // The spm4Kmp install is wired in apply() via plugins.withId, which fires regardless of - // plugin application order, so it is intentionally not invoked here. hasSpm4KmpPlugin is - // only used below to skip the manual DerivedData linker. + // plugin application order, so it is intentionally not invoked here. } // When CocoaPods or spm4Kmp provide the Sentry framework, they also handle linking, so the - // manual DerivedData-based linker is only needed as a fallback for plain SPM users. + // manual DerivedData-based linker is only needed as a fallback for plain SPM users. Merely + // applying the spm4Kmp plugin (e.g. for other Swift packages, with the Sentry spm + // auto-install opted out) must not disable that fallback, so the spm4Kmp check requires the + // Sentry Swift package to actually be configured. maybeLinkCocoaFramework( project, - frameworkProvidedExternally = hasCocoapodsPlugin || hasSpm4KmpPlugin, + frameworkProvidedExternally = hasCocoapodsPlugin || project.isSentryConfiguredViaSpm4Kmp(), hostIsMac ) } @@ -218,6 +226,26 @@ internal fun Project.installSentryForCocoapods( internal const val SENTRY_COCOA_CINTEROP_NAME = "sentryCocoa" private const val SENTRY_COCOA_GIT_URL = "https://github.com/getsentry/sentry-cocoa.git" +/** + * True when the Sentry Cocoa Swift package is registered with spm4Kmp — either through the + * auto-install or a user-defined [SENTRY_COCOA_CINTEROP_NAME] config. spm4Kmp keys per-target + * entries as "_" in its swiftPackageConfig container. + * + * Only Gradle core types are used here on purpose: spm4Kmp is a compileOnly dependency, so its + * classes must not be touched unless the consumer actually applies the spm4Kmp plugin. + */ +internal fun Project.isSentryConfiguredViaSpm4Kmp(): Boolean { + if (!plugins.hasPlugin(SPM4KMP_PLUGIN_ID)) { + return false + } + val swiftPackageConfigs = + extensions.findByName(SPM4KMP_SWIFT_PACKAGE_CONFIG_EXTENSION_NAME) + as? NamedDomainObjectContainer<*> ?: return false + return swiftPackageConfigs.names.any { name -> + name == SENTRY_COCOA_CINTEROP_NAME || name.startsWith("${SENTRY_COCOA_CINTEROP_NAME}_") + } +} + /** * Adds the Sentry Cocoa Swift package to every Apple target via the spm4Kmp DSL so consumers don't * have to declare it themselves. Idempotent: skips any target that already has a [SENTRY_COCOA_CINTEROP_NAME] diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/Spm4KmpAutoInstallExtension.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/Spm4KmpAutoInstallExtension.kt index 645793feb..aa40de1de 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/Spm4KmpAutoInstallExtension.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/Spm4KmpAutoInstallExtension.kt @@ -19,6 +19,10 @@ constructor( * If the spm4Kmp plugin (io.github.frankois944.spmForKmp) is applied and no existing Sentry * Swift package configuration exists, the Sentry-Cocoa SDK will be added to every Apple target. * + * The Swift package is registered as soon as each Apple target is created, so this must be set + * before the `kotlin { }` block declares the Apple targets — setting it afterwards has no + * effect. + * * Defaults to true. */ val enabled: Property = objects.property(Boolean::class.java).convention(true) @@ -26,6 +30,10 @@ constructor( /** * Overrides default Sentry Cocoa version. * + * The Swift package is registered as soon as each Apple target is created, so this must be set + * before the `kotlin { }` block declares the Apple targets — setting it afterwards has no + * effect. + * * Defaults to the version used in the latest KMP SDK. Must be an exact version since the Swift * Package Manager resolves remote packages by exact version. */ diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt index 7ccaa68bf..779a9f301 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt @@ -7,10 +7,13 @@ import org.gradle.api.plugins.ExtensionAware import org.gradle.testfixtures.ProjectBuilder import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension +import org.jetbrains.kotlin.konan.target.HostManager import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assumptions import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest @@ -308,6 +311,69 @@ class SentryPluginTest { assertNull(swiftPackages.findByName("${SENTRY_COCOA_CINTEROP_NAME}_IosArm64")) } + @Test + fun `install Sentry Swift package when Kotlin Multiplatform plugin is applied last`() { + Assumptions.assumeTrue(HostManager.hostIsMac) + + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + project.pluginManager.apply("io.github.frankois944.spmForKmp") + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kmpExtension.iosArm64() + + val swiftPackages = + project.extensions.getByName("swiftPackageConfig") as NamedDomainObjectContainer<*> + assertNotNull(swiftPackages.findByName("${SENTRY_COCOA_CINTEROP_NAME}_IosArm64")) + } + + @Test + fun `Sentry is not considered configured via spm4Kmp when the plugin is missing`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assertFalse(project.isSentryConfiguredViaSpm4Kmp()) + } + + @Test + fun `Sentry is not considered configured via spm4Kmp when spm auto install is disabled`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.github.frankois944.spmForKmp") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val autoInstall = project.extensions.getByName("autoInstall") as AutoInstallExtension + autoInstall.spm.enabled.set(false) + + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kmpExtension.iosArm64() + + project.installSentryForSpm4Kmp(autoInstall, hostIsMac = true) + + // The fallback linker must stay active for consumers that apply spm4Kmp for other Swift + // packages but provide Sentry via Xcode or the linker extension. + assertFalse(project.isSentryConfiguredViaSpm4Kmp()) + } + + @Test + fun `Sentry is considered configured via spm4Kmp after the Swift package is installed`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.github.frankois944.spmForKmp") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val autoInstall = project.extensions.getByName("autoInstall") as AutoInstallExtension + + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kmpExtension.iosArm64() + + project.installSentryForSpm4Kmp(autoInstall, hostIsMac = true) + + assertTrue(project.isSentryConfiguredViaSpm4Kmp()) + } + @Test fun `do not install Sentry pod if host is not mac`() { val project = ProjectBuilder.builder().build() From e268adc00815693c709876210be95df455dd9562 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 11 Jun 2026 15:29:38 +0200 Subject: [PATCH 6/6] fix(plugin): Detect existing Sentry spm4Kmp configs via the swiftPackageConfig container The spm4Kmp auto-install skipped targets based on a sentryCocoa cinterop on the main compilation, but spm4Kmp creates that cinterop only in its own afterEvaluate and capitalizes the name (SentryCocoa), so the check could never match a user-defined Sentry Swift package config. Check the spm4Kmp swiftPackageConfig container instead (global sentryCocoa entry or per-target sentryCocoa_ key), which exists from the moment the config is declared. Also warn when the spm auto-install opt-out is configured after the kotlin { } block: the Swift package is registered at target creation, so a late opt-out (including the global autoInstall.enabled flag) is read too late to take effect. Document this on AutoInstallExtension and in the README. Co-Authored-By: Claude Fable 5 --- README.md | 4 +- .../gradle/AutoInstallExtension.kt | 4 ++ .../multiplatform/gradle/SentryPlugin.kt | 68 ++++++++++++++++--- .../multiplatform/gradle/SentryPluginTest.kt | 62 +++++++++++++++++ 4 files changed, 127 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 70b67e108..681936b55 100644 --- a/README.md +++ b/README.md @@ -107,8 +107,8 @@ sentryKmp { > [!NOTE] > The Sentry Swift package is registered as soon as the Apple targets are created, so place the -> `sentryKmp { }` block **before** the `kotlin { }` block — otherwise the opt-out and version -> override have no effect. +> `sentryKmp { }` block **before** the `kotlin { }` block — otherwise the opt-out (including the +> global `autoInstall.enabled` flag) and version override have no effect. Consumers that don't use spm4Kmp keep the existing behavior: the CocoaPods auto-install (when the Kotlin CocoaPods plugin is applied) or the `linker { frameworkPath / xcodeprojPath }` fallback for diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt index 4a4ea5325..d97c604d2 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt @@ -14,6 +14,10 @@ abstract class AutoInstallExtension @Inject constructor(project: Project) { * * Disabling this will prevent the plugin from auto installing any dependency. * + * The spm4Kmp auto-install registers the Sentry Swift package as soon as each Apple target is + * created, so to disable it this flag must be set before the `kotlin { }` block declares the + * Apple targets — setting it afterwards only disables the CocoaPods and commonMain installs. + * * Defaults to true. */ val enabled: Property = objects.property(Boolean::class.java).convention(true) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt index 1797de8f9..325604be8 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt @@ -51,11 +51,11 @@ class SentryPlugin : Plugin { sentryExtension.autoInstall.commonMain ) - // spm4Kmp consumes its swiftPackageConfig during the configuration phase (before - // afterEvaluate), so the Sentry package must be registered as soon as the spm4Kmp plugin - // is applied rather than in executeConfiguration's afterEvaluate. The nested withId makes - // this robust to plugin application order: the install only runs once both the spm4Kmp - // and Kotlin Multiplatform plugins are present. + // spm4Kmp consumes its swiftPackageConfig container in its own afterEvaluate, which can + // run before executeConfiguration's afterEvaluate depending on plugin application order, + // so the Sentry package must be registered eagerly rather than in afterEvaluate. The + // nested withId makes this robust to plugin application order: the install only runs + // once both the spm4Kmp and Kotlin Multiplatform plugins are present. project.plugins.withId(SPM4KMP_PLUGIN_ID) { project.plugins.withId(KOTLIN_MULTIPLATFORM_PLUGIN_ID) { project.installSentryForSpm4Kmp(sentryExtension.autoInstall) @@ -90,6 +90,8 @@ class SentryPlugin : Plugin { // plugin application order, so it is intentionally not invoked here. } + warnOnLateSpmAutoInstallOptOut(project, sentryExtension.autoInstall) + // When CocoaPods or spm4Kmp provide the Sentry framework, they also handle linking, so the // manual DerivedData-based linker is only needed as a fallback for plain SPM users. Merely // applying the spm4Kmp plugin (e.g. for other Swift packages, with the Sentry spm @@ -102,6 +104,27 @@ class SentryPlugin : Plugin { ) } + /** + * The Sentry Swift package is registered with spm4Kmp as soon as each Apple target is created + * (inside the `kotlin { }` block), so an auto-install opt-out configured after that block is + * read too late to take effect. By afterEvaluate both states are final, so a disabled flag + * combined with the registration marker means the opt-out was silently ignored — warn instead. + */ + private fun warnOnLateSpmAutoInstallOptOut( + project: Project, + autoInstall: AutoInstallExtension + ) { + val spmAutoInstalled = project.extensions.extraProperties.has(SPM_AUTO_INSTALLED_MARKER) + val spmOptedOut = !autoInstall.enabled.get() || !autoInstall.spm.enabled.get() + if (spmAutoInstalled && spmOptedOut) { + project.logger.warn( + "The Sentry Cocoa Swift package was already registered with spm4Kmp before the " + + "auto-install was disabled. Place the sentryKmp { } block before the " + + "kotlin { } block for the opt-out to take effect." + ) + } + } + companion object { internal val logger by lazy { LoggerFactory.getLogger(SentryPlugin::class.java) @@ -226,6 +249,12 @@ internal fun Project.installSentryForCocoapods( internal const val SENTRY_COCOA_CINTEROP_NAME = "sentryCocoa" private const val SENTRY_COCOA_GIT_URL = "https://github.com/getsentry/sentry-cocoa.git" +/** + * Extra-property marker set when the spm4Kmp auto-install actually registered the Sentry Swift + * package, used to warn when the auto-install opt-out is configured too late to take effect. + */ +internal const val SPM_AUTO_INSTALLED_MARKER = "io.sentry.kotlin.multiplatform.spmAutoInstalled" + /** * True when the Sentry Cocoa Swift package is registered with spm4Kmp — either through the * auto-install or a user-defined [SENTRY_COCOA_CINTEROP_NAME] config. spm4Kmp keys per-target @@ -246,10 +275,31 @@ internal fun Project.isSentryConfiguredViaSpm4Kmp(): Boolean { } } +/** + * True when a [SENTRY_COCOA_CINTEROP_NAME] Swift package config already exists for [targetName] in + * the spm4Kmp container — either a global (non target-scoped) "sentryCocoa" entry or the + * "sentryCocoa_" key that spm4Kmp's `swiftPackageConfig(cinteropName)` creates. + * + * The container is the only reliable "already configured" signal at configuration time: spm4Kmp + * creates the actual Kotlin cinterop only in its own afterEvaluate, and capitalizes its name + * ("SentryCocoa"), so checking the compilation's cinterops would never match a spm4Kmp-managed + * config. + */ +private fun Project.hasSentrySwiftPackageConfig(targetName: String): Boolean { + val swiftPackageConfigs = + extensions.findByName(SPM4KMP_SWIFT_PACKAGE_CONFIG_EXTENSION_NAME) + as? NamedDomainObjectContainer<*> ?: return false + val perTargetName = + "${SENTRY_COCOA_CINTEROP_NAME}_${targetName.replaceFirstChar { it.uppercase() }}" + return SENTRY_COCOA_CINTEROP_NAME in swiftPackageConfigs.names || + perTargetName in swiftPackageConfigs.names +} + /** * Adds the Sentry Cocoa Swift package to every Apple target via the spm4Kmp DSL so consumers don't - * have to declare it themselves. Idempotent: skips any target that already has a [SENTRY_COCOA_CINTEROP_NAME] - * cinterop (e.g. a user-defined Sentry config) and re-running is a no-op. + * have to declare it themselves. Idempotent: skips any target that already has a + * [SENTRY_COCOA_CINTEROP_NAME] Swift package config registered with spm4Kmp (e.g. a user-defined + * Sentry config) and re-running is a no-op. */ internal fun Project.installSentryForSpm4Kmp( autoInstall: AutoInstallExtension, @@ -266,8 +316,7 @@ internal fun Project.installSentryForSpm4Kmp( return@configureEach } - val mainCompilation = target.compilations.findByName("main") - if (mainCompilation?.cinterops?.findByName(SENTRY_COCOA_CINTEROP_NAME) != null) { + if (hasSentrySwiftPackageConfig(target.name)) { logger.info( "Sentry Cocoa Swift package already configured for ${target.name}. " + "Skipping spm4Kmp auto installation." @@ -275,6 +324,7 @@ internal fun Project.installSentryForSpm4Kmp( return@configureEach } + extensions.extraProperties.set(SPM_AUTO_INSTALLED_MARKER, true) target.swiftPackageConfig(cinteropName = SENTRY_COCOA_CINTEROP_NAME) { dependency { remotePackageVersion( diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt index 779a9f301..f7af770ad 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt @@ -1,5 +1,6 @@ package io.sentry.kotlin.multiplatform.gradle +import io.github.frankois944.spmForKmp.swiftPackageConfig import io.sentry.BuildConfig import org.gradle.api.GradleException import org.gradle.api.NamedDomainObjectContainer @@ -289,6 +290,7 @@ class SentryPluginTest { val swiftPackages = project.extensions.getByName("swiftPackageConfig") as NamedDomainObjectContainer<*> assertNotNull(swiftPackages.findByName("${SENTRY_COCOA_CINTEROP_NAME}_IosArm64")) + assertTrue(project.extensions.extraProperties.has(SPM_AUTO_INSTALLED_MARKER)) } @Test @@ -328,6 +330,66 @@ class SentryPluginTest { assertNotNull(swiftPackages.findByName("${SENTRY_COCOA_CINTEROP_NAME}_IosArm64")) } + @Test + fun `do not install Sentry Swift package when a user-defined per-target config exists`() { + Assumptions.assumeTrue(HostManager.hostIsMac) + + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.github.frankois944.spmForKmp") + + // A consumer-defined Sentry Swift package config that exists before the Sentry plugin is + // applied. spm4Kmp keys it as "sentryCocoa_IosArm64" in its container — the auto-install + // must detect it there (no Kotlin cinterop exists at configuration time). + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kmpExtension.iosArm64().swiftPackageConfig(cinteropName = SENTRY_COCOA_CINTEROP_NAME) { } + + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assertFalse(project.extensions.extraProperties.has(SPM_AUTO_INSTALLED_MARKER)) + } + + @Test + fun `do not install Sentry Swift package when a user-defined global config exists`() { + Assumptions.assumeTrue(HostManager.hostIsMac) + + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.github.frankois944.spmForKmp") + + // Old-style (non target-scoped) spm4Kmp config registered directly under "sentryCocoa". + val swiftPackages = + project.extensions.getByName("swiftPackageConfig") as NamedDomainObjectContainer<*> + swiftPackages.create(SENTRY_COCOA_CINTEROP_NAME) + + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kmpExtension.iosArm64() + + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assertFalse(project.extensions.extraProperties.has(SPM_AUTO_INSTALLED_MARKER)) + } + + @Test + fun `do not install Sentry Swift package when global auto install is disabled`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.github.frankois944.spmForKmp") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val autoInstall = project.extensions.getByName("autoInstall") as AutoInstallExtension + autoInstall.enabled.set(false) + + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kmpExtension.iosArm64() + + project.installSentryForSpm4Kmp(autoInstall, hostIsMac = true) + + val swiftPackages = + project.extensions.getByName("swiftPackageConfig") as NamedDomainObjectContainer<*> + assertNull(swiftPackages.findByName("${SENTRY_COCOA_CINTEROP_NAME}_IosArm64")) + } + @Test fun `Sentry is not considered configured via spm4Kmp when the plugin is missing`() { val project = ProjectBuilder.builder().build()