Skip to content

Commit a63d95e

Browse files
committed
draft commit
1 parent 9c82a2a commit a63d95e

File tree

12 files changed

+585
-198
lines changed

12 files changed

+585
-198
lines changed

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

Lines changed: 24 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -8,115 +8,59 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer
88
import org.jetbrains.kotlin.gradle.plugin.mpp.Framework
99
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
1010
import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable
11-
import org.jetbrains.kotlin.konan.target.HostManager
1211
import java.io.File
1312

1413
/**
1514
* Configures Sentry Cocoa framework linking for Apple targets in Kotlin Multiplatform projects.
1615
*
1716
* Resolves framework paths and applies necessary linker options to both test and framework binaries.
18-
*/
17+
*/
1918
class CocoaFrameworkLinker(
20-
private val project: Project,
21-
private val logger: Logger
19+
private val logger: Logger,
20+
private val pathResolver: FrameworkPathResolver,
21+
private val binaryLinker: FrameworkLinker,
22+
private val hostIsMac: Boolean
2223
) {
23-
private val pathResolver = FrameworkPathResolver(project)
24-
private val binaryLinker = FrameworkLinker(logger)
25-
26-
fun configure(extension: LinkerExtension) {
27-
val kmpExtension =
28-
project.extensions.findByName("kotlin") as? KotlinMultiplatformExtension ?: run {
29-
logger.info("Skipping Apple framework linking: Kotlin Multiplatform extension not found")
30-
return
31-
}
32-
33-
if (!HostManager.hostIsMac) {
24+
fun configure(
25+
appleTargets: List<KotlinNativeTarget>,
26+
) {
27+
if (!hostIsMac) {
3428
logger.info("Skipping Apple framework linking: Requires macOS host")
3529
return
3630
}
3731

38-
kmpExtension.appleTargets().all { target ->
32+
appleTargets.forEach { target ->
3933
try {
40-
processTarget(target, extension)
34+
logger.lifecycle(
35+
"Start resolving cocoa framework paths for target: ${target.name}"
36+
)
37+
processTarget(target)
4138
} catch (e: Exception) {
4239
throw GradleException("Failed to configure ${target.name}: ${e.message}", e)
4340
}
4441
}
4542
}
4643

47-
private fun processTarget(target: KotlinNativeTarget, linker: LinkerExtension) {
44+
private fun processTarget(target: KotlinNativeTarget) {
4845
val architectures =
4946
target.toSentryFrameworkArchitecture().takeIf { it.isNotEmpty() } ?: run {
5047
logger.warn("Skipping target ${target.name}: Unsupported architecture")
5148
return
5249
}
5350

54-
val (dynamicPath, staticPath) = pathResolver.resolvePaths(linker, architectures)
51+
val (dynamicPath, staticPath) = pathResolver.resolvePaths(architectures)
5552
binaryLinker.configureBinaries(target.binaries, dynamicPath, staticPath)
5653
}
5754
}
5855

59-
internal class FrameworkPathResolver(
60-
private val project: Project
61-
) {
62-
fun resolvePaths(
63-
linker: LinkerExtension,
64-
architectures: Set<String>
65-
): Pair<String?, String?> {
66-
val customPath = linker.frameworkPath.orNull?.takeIf { it.isNotEmpty() }
67-
val derivedData = linker.xcodeprojPath.orNull?.let(project::findDerivedDataPath)
68-
?: project.findDerivedDataPath(null)
69-
70-
return customPath?.let { path ->
71-
validateCustomPath(path)
72-
when {
73-
path.isStaticFrameworkPath() -> null to path
74-
path.isDynamicFrameworkPath() -> path to null
75-
else -> throw FrameworkLinkingException(
76-
"Invalid framework name at $path - must be Sentry.xcframework or Sentry-Dynamic.xcframework"
77-
)
78-
}
79-
} ?: run {
80-
Pair(
81-
getFrameworkPath(FrameworkType.DYNAMIC, derivedData, architectures),
82-
getFrameworkPath(FrameworkType.STATIC, derivedData, architectures)
83-
)
84-
}
85-
}
86-
87-
private fun validateCustomPath(path: String) {
88-
if (!File(path).exists()) {
89-
throw FrameworkLinkingException(
90-
"Custom framework path not found or does not exist: $path"
91-
)
92-
}
93-
94-
if (path.isStaticFrameworkPath().not() && path.isDynamicFrameworkPath().not()) {
95-
throw FrameworkLinkingException("Invalid framework at $path - path must end with Sentry.xcframework or Sentry-Dynamic.xcframework")
96-
}
97-
}
98-
99-
/**
100-
* Fallback method for fetching paths
101-
*/
102-
private fun getFrameworkPath(
103-
type: FrameworkType,
104-
derivedData: String,
105-
architectures: Set<String>
106-
) = project.providers.of(FrameworkPathValueSource::class.java) {
107-
it.parameters.frameworkType.set(type)
108-
it.parameters.derivedDataPath.set(derivedData)
109-
it.parameters.frameworkArchitectures.set(architectures)
110-
}.orNull
111-
112-
private fun String.isStaticFrameworkPath() = endsWith("Sentry.xcframework")
113-
private fun String.isDynamicFrameworkPath() = endsWith("Sentry-Dynamic.xcframework")
114-
}
115-
116-
internal class FrameworkLinker(
56+
class FrameworkLinker(
11757
private val logger: Logger
11858
) {
119-
fun configureBinaries(binaries: KotlinNativeBinaryContainer, dynamicPath: String?, staticPath: String?) {
59+
fun configureBinaries(
60+
binaries: KotlinNativeBinaryContainer,
61+
dynamicPath: String?,
62+
staticPath: String?
63+
) {
12064
validatePaths(dynamicPath, staticPath)
12165

12266
binaries.all { binary ->
@@ -146,6 +90,7 @@ internal class FrameworkLinker(
14690
}
14791

14892
private fun linkTestBinary(binary: TestExecutable, path: String) {
93+
// Linking in test binaries works with both dynamic and static framework
14994
binary.linkerOpts("-rpath", path, "-F$path")
15095
logger.info("Linked Sentry Cocoa framework to test binary ${binary.name}")
15196
}
@@ -232,7 +177,7 @@ internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set<String> = b
232177
}
233178
}
234179

235-
internal fun Project.findDerivedDataPath(customXcodeprojPath: String? = null): String {
180+
internal fun Project.findDerivedDataPath(customXcodeprojPath: String? = null): String? {
236181
val xcodeprojPath = customXcodeprojPath ?: findXcodeprojFile(rootDir)?.absolutePath
237182
?: throw GradleException("Xcode project file not found")
238183

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.sentry.kotlin.multiplatform.gradle
22

3-
import org.gradle.api.GradleException
43
import org.gradle.api.provider.Property
54
import org.gradle.api.provider.ValueSource
65
import org.gradle.api.provider.ValueSourceParameters
@@ -9,8 +8,13 @@ import org.gradle.process.ExecOperations
98
import java.io.ByteArrayOutputStream
109
import javax.inject.Inject
1110

12-
internal abstract class DerivedDataPathValueSource :
13-
ValueSource<String, DerivedDataPathValueSource.Parameters> {
11+
/**
12+
* Provides the derived data path for an Xcode project using the xcodebuild command.
13+
*
14+
* e.g /Users/theusername/Library/Developer/Xcode/DerivedData/iosApp-ddefikekigqzzgcnpfkkdallksmlfpln/
15+
*/
16+
abstract class DerivedDataPathValueSource :
17+
ValueSource<String?, DerivedDataPathValueSource.Parameters> {
1418
interface Parameters : ValueSourceParameters {
1519
@get:Input
1620
val xcodeprojPath: Property<String>
@@ -23,7 +27,7 @@ internal abstract class DerivedDataPathValueSource :
2327
private val buildDirRegex = Regex("BUILD_DIR = (.+)")
2428
}
2529

26-
override fun obtain(): String {
30+
override fun obtain(): String? {
2731
val buildDirOutput = ByteArrayOutputStream()
2832
execOperations.exec {
2933
it.commandLine = listOf(
@@ -37,7 +41,10 @@ internal abstract class DerivedDataPathValueSource :
3741
val buildSettings = buildDirOutput.toString("UTF-8")
3842
val buildDirMatch = buildDirRegex.find(buildSettings)
3943
val buildDir = buildDirMatch?.groupValues?.get(1)
40-
?: throw GradleException("BUILD_DIR not found in xcodebuild output")
44+
?: return null
45+
if (buildDir.contains("DerivedData").not()) {
46+
return null
47+
}
4148
return buildDir.replace("/Build/Products", "")
4249
}
4350
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package io.sentry.kotlin.multiplatform.gradle
2+
3+
import org.gradle.api.Project
4+
import java.io.File
5+
6+
data class FrameworkPaths(
7+
val dynamic: String? = null,
8+
val static: String? = null
9+
) {
10+
companion object {
11+
val NONE = FrameworkPaths(null, null)
12+
13+
fun createValidated(
14+
dynamicBasePath: String? = null,
15+
staticBasePath: String? = null,
16+
architectures: Set<String>,
17+
pathExists: (String) -> Boolean = { path -> File(path).exists() }
18+
): FrameworkPaths {
19+
// Find first valid dynamic path
20+
val dynamicPath = dynamicBasePath?.let { basePath ->
21+
architectures.firstNotNullOfOrNull { arch ->
22+
val path = "$basePath/$arch"
23+
path.takeIf { pathExists(it) }
24+
}
25+
}
26+
27+
// Find first valid static path
28+
val staticPath = staticBasePath?.let { basePath ->
29+
architectures.firstNotNullOfOrNull { arch ->
30+
val path = "$basePath/$arch"
31+
path.takeIf { pathExists(it) }
32+
}
33+
}
34+
35+
return when {
36+
dynamicPath != null && staticPath != null ->
37+
FrameworkPaths(dynamic = dynamicPath, static = staticPath)
38+
39+
dynamicPath != null ->
40+
FrameworkPaths(dynamic = dynamicPath)
41+
42+
staticPath != null ->
43+
FrameworkPaths(static = staticPath)
44+
45+
else ->
46+
NONE
47+
}
48+
}
49+
}
50+
}
51+
52+
sealed interface FrameworkResolutionStrategy {
53+
fun resolvePaths(
54+
architectures: Set<String>,
55+
): FrameworkPaths
56+
}
57+
58+
/**
59+
* Handles the custom framework paths set by the user. This should generally be executed first.
60+
*/
61+
class CustomPathStrategy(
62+
private val project: Project,
63+
) : FrameworkResolutionStrategy {
64+
private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java)
65+
66+
// In this function we don't really distinguish between static and dynamic framework
67+
// We trust that the user knows the distinction if they purposefully override the framework path
68+
override fun resolvePaths(architectures: Set<String>): FrameworkPaths {
69+
return linker.frameworkPath.orNull?.takeIf { it.isNotEmpty() }?.let { basePath ->
70+
when {
71+
basePath.endsWith("Sentry.xcframework") -> FrameworkPaths.createValidated(
72+
staticBasePath = basePath,
73+
architectures = architectures
74+
)
75+
76+
basePath.endsWith("Sentry-Dynamic.xcframework") -> FrameworkPaths.createValidated(
77+
dynamicBasePath = basePath,
78+
architectures = architectures
79+
)
80+
81+
else -> null
82+
}
83+
} ?: FrameworkPaths.NONE
84+
}
85+
}
86+
87+
class DerivedDataStrategy(
88+
private val project: Project,
89+
) : FrameworkResolutionStrategy {
90+
private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java)
91+
92+
override fun resolvePaths(architectures: Set<String>): FrameworkPaths {
93+
val xcodeprojPath = linker.xcodeprojPath.orNull
94+
val derivedDataPath = xcodeprojPath?.let { path ->
95+
project.providers.of(DerivedDataPathValueSource::class.java) {
96+
it.parameters.xcodeprojPath.set(path)
97+
}.get()
98+
} ?: return FrameworkPaths.NONE
99+
val dynamicBasePath =
100+
"${derivedDataPath}/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework"
101+
val staticBasePath =
102+
"${derivedDataPath}/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework"
103+
104+
return FrameworkPaths.createValidated(
105+
dynamicBasePath = dynamicBasePath,
106+
staticBasePath = staticBasePath,
107+
architectures = architectures
108+
)
109+
}
110+
}
111+
112+
class ManualSearchStrategy(
113+
private val project: Project,
114+
) : FrameworkResolutionStrategy {
115+
override fun resolvePaths(architectures: Set<String>): FrameworkPaths {
116+
val dynamicValueSource =
117+
project.providers.of(ManualFrameworkPathSearchValueSource::class.java) {
118+
it.parameters.frameworkType.set(FrameworkType.DYNAMIC)
119+
it.parameters.frameworkArchitectures.set(architectures)
120+
}
121+
val staticValueSource =
122+
project.providers.of(ManualFrameworkPathSearchValueSource::class.java) {
123+
it.parameters.frameworkType.set(FrameworkType.STATIC)
124+
it.parameters.frameworkArchitectures.set(architectures)
125+
}
126+
127+
return FrameworkPaths.createValidated(
128+
dynamicBasePath = dynamicValueSource.orNull,
129+
staticBasePath = staticValueSource.orNull,
130+
architectures = architectures
131+
)
132+
}
133+
}
134+
135+
class FrameworkPathResolver(
136+
private val project: Project,
137+
private val strategies: List<FrameworkResolutionStrategy> = defaultStrategies(project),
138+
) {
139+
fun resolvePaths(
140+
architectures: Set<String>
141+
): FrameworkPaths {
142+
return strategies.firstNotNullOfOrNull { strategy ->
143+
try {
144+
strategy.resolvePaths(architectures)
145+
} catch (e: Exception) {
146+
project.logger.debug(
147+
"Path resolution strategy ${strategy::class.simpleName} failed",
148+
e
149+
)
150+
null
151+
}
152+
}
153+
?: throw FrameworkLinkingException("All framework resolution strategies failed. Could not find Sentry Cocoa framework path")
154+
}
155+
156+
companion object {
157+
/**
158+
* Resolution strategies for finding the framework path
159+
*
160+
* The order of resolution strategies matters, as the framework path will be resolved by the first successful strategy
161+
* Specifically here Custom Path will be checked first, if that fails then it is followed by the Derived Data strategy etc...
162+
*/
163+
fun defaultStrategies(project: Project): List<FrameworkResolutionStrategy> {
164+
return listOf(
165+
CustomPathStrategy(project),
166+
DerivedDataStrategy(project),
167+
ManualSearchStrategy(project)
168+
)
169+
}
170+
}
171+
172+
}

0 commit comments

Comments
 (0)