Skip to content

Commit 3a6a14a

Browse files
Implement conditional Cocoa linking for targets (#421)
* Optimize Cocoa framework linking by deferring configuration to task graph Co-authored-by: giancarlo.buenaflor <giancarlo.buenaflor@sentry.io> * Add fallback framework search and lazy linking tests for Cocoa targets Co-authored-by: giancarlo.buenaflor <giancarlo.buenaflor@sentry.io> * Revert * Tests * Update * Update * Update CHANGELOG * Update CHANGELOG * Update class name * Update * Update * Update * Fix analyze * Update * Update libs.versions.toml * Update * Fix analyze * Add try/catch * Fix analyze * Fix analyze --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 89115e0 commit 3a6a14a

File tree

5 files changed

+209
-12
lines changed

5 files changed

+209
-12
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Enhancements
6+
7+
- Gradle Plugin: implement conditional Cocoa linking for targets ([#421](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/421))
8+
39
## 0.14.0
410

511
### Dependencies

sentry-kotlin-multiplatform-gradle-plugin/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies {
2525
testImplementation(libs.junit)
2626
testImplementation(libs.junit.params)
2727
testImplementation(libs.mockk)
28+
testImplementation(libs.truth)
2829
}
2930

3031
tasks.test {

sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover"}
1818
junit = "org.junit.jupiter:junit-jupiter-api:5.10.3"
1919
junit-params = "org.junit.jupiter:junit-jupiter-params:5.10.3"
2020
mockk = "io.mockk:mockk:1.13.12"
21+
truth = { module = "com.google.truth:truth", version = "1.4.4" }

sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package io.sentry.kotlin.multiplatform.gradle
33
import org.gradle.api.GradleException
44
import org.gradle.api.Plugin
55
import org.gradle.api.Project
6+
import org.gradle.api.execution.TaskExecutionGraph
67
import org.gradle.api.plugins.ExtensionAware
78
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
89
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension
910
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin
11+
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
1012
import org.jetbrains.kotlin.konan.target.HostManager
1113
import org.slf4j.LoggerFactory
1214

@@ -43,9 +45,13 @@ class SentryPlugin : Plugin<Project> {
4345
}
4446
}
4547

46-
internal fun executeConfiguration(project: Project, hostIsMac: Boolean = HostManager.hostIsMac) {
48+
internal fun executeConfiguration(
49+
project: Project,
50+
hostIsMac: Boolean = HostManager.hostIsMac
51+
) {
4752
val sentryExtension = project.extensions.getByType(SentryExtension::class.java)
48-
val hasCocoapodsPlugin = project.plugins.findPlugin(KotlinCocoapodsPlugin::class.java) != null
53+
val hasCocoapodsPlugin =
54+
project.plugins.findPlugin(KotlinCocoapodsPlugin::class.java) != null
4955

5056
if (sentryExtension.autoInstall.enabled.get()) {
5157
val autoInstall = sentryExtension.autoInstall
@@ -59,25 +65,78 @@ class SentryPlugin : Plugin<Project> {
5965
}
6066
}
6167

62-
if (hostIsMac && !hasCocoapodsPlugin) {
63-
project.logger.info("Cocoapods plugin not found. Attempting to link Sentry Cocoa framework.")
68+
maybeLinkCocoaFramework(project, hasCocoapodsPlugin, hostIsMac)
69+
}
6470

65-
val kmpExtension = project.extensions.findByName(KOTLIN_EXTENSION_NAME) as? KotlinMultiplatformExtension
66-
val appleTargets = kmpExtension?.appleTargets()?.toList()
67-
?: throw GradleException("Error fetching Apple targets from Kotlin Multiplatform plugin.")
71+
companion object {
72+
internal val logger by lazy {
73+
LoggerFactory.getLogger(SentryPlugin::class.java)
74+
}
75+
}
76+
}
77+
78+
private fun maybeLinkCocoaFramework(
79+
project: Project,
80+
hasCocoapods: Boolean,
81+
hostIsMac: Boolean
82+
) {
83+
if (hostIsMac && !hasCocoapods) {
84+
// Register a task graph listener so that we only configure Cocoa framework linking
85+
// if at least one Apple target task is part of the requested task graph. This avoids
86+
// executing the (potentially expensive) path-resolution logic when the build is only
87+
// concerned with non-Apple targets such as Android.
88+
89+
val kmpExtension =
90+
project.extensions.findByName(KOTLIN_EXTENSION_NAME) as? KotlinMultiplatformExtension
91+
?: throw GradleException("Error fetching Kotlin Multiplatform extension.")
92+
93+
val appleTargets = kmpExtension.appleTargets().toList()
94+
95+
if (appleTargets.isEmpty()) {
96+
project.logger.info("No Apple targets detected – skipping Sentry Cocoa framework linking setup.")
97+
return
98+
}
99+
100+
project.gradle.taskGraph.whenReady { graph ->
101+
// Check which of the Kotlin/Native targets are actually in the graph
102+
val activeTarget = getActiveTarget(project, appleTargets, graph)
103+
104+
if (activeTarget == null) {
105+
project.logger.lifecycle(
106+
"No Apple compile task scheduled for this build " +
107+
"- skipping Sentry Cocoa framework linking"
108+
)
109+
return@whenReady
110+
}
111+
112+
project.logger.lifecycle("Set up Sentry Cocoa linking for target: ${activeTarget.name}")
68113

69114
CocoaFrameworkLinker(
70115
logger = project.logger,
71116
pathResolver = FrameworkPathResolver(project),
72117
binaryLinker = FrameworkLinker(project.logger)
73-
).configure(appleTargets)
118+
).configure(appleTargets = listOf(activeTarget))
74119
}
75120
}
121+
}
76122

77-
companion object {
78-
internal val logger by lazy {
79-
LoggerFactory.getLogger(SentryPlugin::class.java)
80-
}
123+
private fun getActiveTarget(
124+
project: Project,
125+
appleTargets: List<KotlinNativeTarget>,
126+
graph: TaskExecutionGraph
127+
): KotlinNativeTarget? = appleTargets.firstOrNull { target ->
128+
val targetName = target.name.replaceFirstChar {
129+
it.uppercase()
130+
}
131+
val path = if (project.path == ":") {
132+
":compileKotlin$targetName"
133+
} else {
134+
"${project.path}:compileKotlin$targetName"
135+
}
136+
try {
137+
graph.hasTask(path)
138+
} catch (_: Exception) {
139+
false
81140
}
82141
}
83142

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package io.sentry.kotlin.multiplatform.gradle
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import org.gradle.testkit.runner.GradleRunner
5+
import org.gradle.testkit.runner.internal.PluginUnderTestMetadataReading
6+
import org.gradle.testkit.runner.internal.io.SynchronizedOutputStream
7+
import org.junit.jupiter.api.Test
8+
import org.junit.jupiter.api.condition.EnabledOnOs
9+
import org.junit.jupiter.api.condition.OS
10+
import org.junit.jupiter.api.io.TempDir
11+
import java.io.ByteArrayOutputStream
12+
import java.io.File
13+
import java.io.OutputStreamWriter
14+
15+
@EnabledOnOs(OS.MAC)
16+
class CocoaFrameworkLinkerIntegrationTest {
17+
/**
18+
* Verifies that the Cocoa linker is **not** configured when the task graph
19+
* contains only non-Apple targets.
20+
*/
21+
@Test
22+
fun `linker is not configured when only non-Apple tasks are requested`(@TempDir projectDir: File) {
23+
writeBuildFiles(projectDir)
24+
25+
val output = ByteArrayOutputStream()
26+
defaultRunner(projectDir, output)
27+
.withArguments("compileKotlinJvm", "--dry-run", "--info")
28+
.build()
29+
30+
assertThat(output.toString())
31+
.contains("No Apple compile task scheduled for this build - skipping Sentry Cocoa framework linking")
32+
}
33+
34+
/**
35+
* Verifies that the Cocoa linker **is** configured when at least one Apple
36+
* task is present in the task graph.
37+
*/
38+
@Test
39+
fun `linker is configured when an Apple task is requested`(@TempDir projectDir: File) {
40+
writeBuildFiles(projectDir)
41+
42+
val output = ByteArrayOutputStream()
43+
defaultRunner(projectDir, output)
44+
.withArguments("compileKotlinIosSimulatorArm64", "--dry-run", "--info")
45+
.build()
46+
47+
assertThat(output.toString())
48+
.contains("Set up Sentry Cocoa linking for target: iosSimulatorArm64")
49+
assertThat(output.toString())
50+
.contains("Start resolving Sentry Cocoa framework paths for target: iosSimulatorArm64")
51+
}
52+
53+
// ---------------------------------------------------------------------
54+
// test-fixture helpers
55+
// ---------------------------------------------------------------------
56+
57+
private fun writeBuildFiles(dir: File) {
58+
// -----------------------------------------------------------------
59+
// Create a fake XCFramework on disk so that the CustomPathStrategy
60+
// can resolve a valid framework path even on CI machines where SPM
61+
// (and hence DerivedData) is not available.
62+
// -----------------------------------------------------------------
63+
val fakeFrameworkDir = File(dir, "Sentry-Dynamic.xcframework").apply {
64+
// Create minimal structure that satisfies path validation logic
65+
val archDirName = "ios-arm64_x86_64-simulator" // architecture used for iosSimulatorArm64
66+
val archDir = File(this, archDirName)
67+
archDir.mkdirs()
68+
}
69+
70+
File(dir, "settings.gradle").writeText("""rootProject.name = "fixture"""")
71+
72+
val pluginClasspath = PluginUnderTestMetadataReading
73+
.readImplementationClasspath()
74+
.joinToString(", ") { "\"${it.absolutePath.replace('\\', '/')}\"" }
75+
76+
File(dir, "build.gradle").writeText(
77+
"""
78+
buildscript {
79+
repositories {
80+
google()
81+
mavenCentral()
82+
gradlePluginPortal()
83+
}
84+
dependencies {
85+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
86+
classpath files($pluginClasspath)
87+
}
88+
}
89+
90+
apply plugin: 'org.jetbrains.kotlin.multiplatform'
91+
apply plugin: 'io.sentry.kotlin.multiplatform.gradle'
92+
93+
repositories {
94+
google()
95+
mavenCentral()
96+
}
97+
98+
kotlin {
99+
jvm() // non-Apple target
100+
iosSimulatorArm64() // Apple target used in tests
101+
}
102+
103+
// -----------------------------------------------------------------
104+
// Configure the plugin to use the fake framework path created above.
105+
// This makes the CustomPathStrategy succeed immediately, bypassing the
106+
// DerivedData and manual search strategies that rely on an SPM setup.
107+
// -----------------------------------------------------------------
108+
sentryKmp {
109+
linker {
110+
frameworkPath.set("${fakeFrameworkDir.absolutePath.replace('\\', '/')}")
111+
}
112+
}
113+
""".trimIndent()
114+
)
115+
}
116+
117+
/** Returns a pre-configured [GradleRunner] that logs into [out]. */
118+
private fun defaultRunner(projectDir: File, out: ByteArrayOutputStream): GradleRunner =
119+
GradleRunner.create()
120+
.withProjectDir(projectDir)
121+
.withPluginClasspath()
122+
.withGradleVersion(org.gradle.util.GradleVersion.current().version)
123+
.forwardStdOutput(OutputStreamWriter(SynchronizedOutputStream(out)))
124+
.forwardStdError(OutputStreamWriter(SynchronizedOutputStream(out)))
125+
.withArguments("--stacktrace")
126+
127+
private companion object {
128+
private const val KOTLIN_VERSION = "2.1.21"
129+
}
130+
}

0 commit comments

Comments
 (0)