From f4187efa40003ad82e45a4926cfe9a3fc94629dd Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 30 Jan 2025 21:34:09 +0100 Subject: [PATCH 01/34] update arch folders and test --- .../multiplatform/gradle/SentryPlugin.kt | 96 +++++++++----- .../gradle/SentryFrameworkArchitectureTest.kt | 121 ++++++++++++++---- 2 files changed, 160 insertions(+), 57 deletions(-) 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 edf96df4..80145386 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 @@ -137,47 +137,58 @@ internal fun Project.configureLinkingOptions(linkerExtension: LinkerExtension) { } kmpExtension.appleTargets().all { target -> - val frameworkArchitecture = target.toSentryFrameworkArchitecture() ?: run { + // Contains a set of names where one should match the arch name in the framework + // This is needed for backwards compatibility as the arch names have changed throughout different versions of the Cocoa SDK + val frameworkArchitectures = target.toSentryFrameworkArchitecture() + if (frameworkArchitectures.isEmpty()) { logger.warn("Skipping target ${target.name} - unsupported architecture.") return@all } - val dynamicFrameworkPath: String - val staticFrameworkPath: String + var dynamicFrameworkPath: String? = null + var staticFrameworkPath: String? = null if (frameworkPath?.isNotEmpty() == true) { dynamicFrameworkPath = frameworkPath staticFrameworkPath = frameworkPath } else { - @Suppress("MaxLineLength") - dynamicFrameworkPath = - "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework/$frameworkArchitecture" - @Suppress("MaxLineLength") - staticFrameworkPath = - "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework/$frameworkArchitecture" - } + frameworkArchitectures.forEach { + val dynamicPath = + "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework/$it" + logger.info("Looking for dynamic framework at $dynamicPath") + if (File(dynamicPath).exists()) { + logger.info("Found dynamic framework at $dynamicPath") + dynamicFrameworkPath = dynamicPath + return@forEach + } - val dynamicFrameworkExists = File(dynamicFrameworkPath).exists() - val staticFrameworkExists = File(staticFrameworkPath).exists() + val staticPath = "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework/$it" + logger.info("Looking for dynamic framework at $dynamicPath") + if (File(staticPath).exists()) { + logger.info("Found static framework at $staticPath") + staticFrameworkPath = staticPath + return@forEach + } + } + } - if (!dynamicFrameworkExists && !staticFrameworkExists) { + if (staticFrameworkPath == null && dynamicFrameworkPath == null) { throw GradleException( - "Sentry Cocoa Framework not found at $dynamicFrameworkPath or $staticFrameworkPath" + "Sentry Cocoa Framework not found. Make sure the Sentry Cocoa SDK is installed with SPM in your Xcode project." ) } target.binaries.all binaries@{ binary -> if (binary is TestExecutable) { // both dynamic and static frameworks will work for tests - val finalFrameworkPath = - if (dynamicFrameworkExists) dynamicFrameworkPath else staticFrameworkPath - binary.linkerOpts("-rpath", finalFrameworkPath, "-F$finalFrameworkPath") + val path = (dynamicFrameworkPath ?: staticFrameworkPath)!! + binary.linkerOpts("-rpath", path, "-F$path") } if (binary is Framework) { val finalFrameworkPath = when { - binary.isStatic && staticFrameworkExists -> staticFrameworkPath - !binary.isStatic && dynamicFrameworkExists -> dynamicFrameworkPath + binary.isStatic && staticFrameworkPath != null -> staticFrameworkPath + !binary.isStatic && dynamicFrameworkPath != null -> dynamicFrameworkPath else -> { logger.warn("Linking to framework failed, no sentry framework found for target ${target.name}") return@binaries @@ -191,19 +202,44 @@ internal fun Project.configureLinkingOptions(linkerExtension: LinkerExtension) { } /** - * Transforms a Kotlin Multiplatform target name to the architecture name that is found inside + * Transforms a Kotlin Multiplatform target name to possible architecture names found inside * Sentry's framework directory. + * + * Returns a set of possible architecture names because Sentry Cocoa SDK has changed folder naming + * across different versions. For example: + * - iosArm64 -> ["ios-arm64", "ios-arm64_arm64e"] + * - macosArm64 -> ["macos-arm64_x86_64", "macos-arm64_arm64e_x86_64"] + * * + * @return Set of possible architecture folder names for the given target. Returns empty set if target is not supported. */ -internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): String? { - return when (name) { - "iosSimulatorArm64", "iosX64" -> "ios-arm64_x86_64-simulator" - "iosArm64" -> "ios-arm64" - "macosArm64", "macosX64" -> "macos-arm64_x86_64" - "tvosSimulatorArm64", "tvosX64" -> "tvos-arm64_x86_64-simulator" - "tvosArm64" -> "tvos-arm64" - "watchosArm32", "watchosArm64" -> "watchos-arm64_arm64_32_armv7k" - "watchosSimulatorArm64", "watchosX64" -> "watchos-arm64_i386_x86_64-simulator" - else -> null +internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set = buildSet { + when (name) { + "iosSimulatorArm64", "iosX64" -> add("ios-arm64_x86_64-simulator") + "iosArm64" -> { + add("ios-arm64") + add("ios-arm64_arm64e") + } + "macosArm64", "macosX64" -> { + add("macos-arm64_x86_64") + add("macos-arm64_arm64e_x86_64") + } + "tvosSimulatorArm64", "tvosX64" -> { + add("tvos-arm64_x86_64-simulator") + add("tvos-arm64_x86_64-simulator") + } + "tvosArm64" -> { + add("tvos-arm64") + add("tvos-arm64_arm64e") + } + "watchosArm32", "watchosArm64" -> { + add("watchos-arm64_arm64_32_armv7k") + add("watchos-arm64_arm64_32_arm64e_armv7k") + } + "watchosSimulatorArm64", "watchosX64" -> { + add("watchos-arm64_i386_x86_64-simulator") + add("watchos-arm64_i386_x86_64-simulator") + } + else -> emptySet() } } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt index 01737d51..719b0db4 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt @@ -1,44 +1,111 @@ package io.sentry.kotlin.multiplatform.gradle -import io.mockk.every -import io.mockk.mockk -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.junit.jupiter.api.Assertions.assertEquals +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource +import java.io.File +import java.net.URL +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import java.util.zip.ZipFile class SentryFrameworkArchitectureTest { companion object { @JvmStatic - fun architectureData(): List = listOf( - Arguments.of("iosSimulatorArm64", "ios-arm64_x86_64-simulator"), - Arguments.of("iosX64", "ios-arm64_x86_64-simulator"), - Arguments.of("iosArm64", "ios-arm64"), - Arguments.of("macosArm64", "macos-arm64_x86_64"), - Arguments.of("macosX64", "macos-arm64_x86_64"), - Arguments.of("tvosSimulatorArm64", "tvos-arm64_x86_64-simulator"), - Arguments.of("tvosX64", "tvos-arm64_x86_64-simulator"), - Arguments.of("tvosArm64", "tvos-arm64"), - Arguments.of("watchosArm32", "watchos-arm64_arm64_32_armv7k"), - Arguments.of("watchosArm64", "watchos-arm64_arm64_32_armv7k"), - Arguments.of("watchosSimulatorArm64", "watchos-arm64_i386_x86_64-simulator"), - Arguments.of("watchosX64", "watchos-arm64_i386_x86_64-simulator"), - Arguments.of("unsupportedTarget", null) + fun cocoaVersions(): List = listOf( + Arguments.of("8.37.0"), + Arguments.of("8.38.0"), + Arguments.of("latest") ) } - @ParameterizedTest(name = "Target {0} should return {1}") - @MethodSource("architectureData") - fun `toSentryFrameworkArchitecture returns correct architecture for all targets`( - targetName: String, - expectedArchitecture: String? + @ParameterizedTest(name = "Test SPM compatibility with Cocoa Version {0}") + @MethodSource("cocoaVersions") + fun `finds arch folders across different cocoa versions`( + cocoaVersion: String ) { - val target = mockk() - every { target.name } returns targetName + val project = ProjectBuilder.builder().build() + project.pluginManager.apply { + apply("org.jetbrains.kotlin.multiplatform") + apply("io.sentry.kotlin.multiplatform.gradle") + } - val result = target.toSentryFrameworkArchitecture() + val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension + kmpExtension.apply { + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + macosArm64(), + macosX64(), + watchosX64(), + watchosArm32(), + watchosSimulatorArm64(), + tvosX64(), + tvosArm64(), + tvosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "shared" + isStatic = false + } + } + } + val frameworkDir = downloadAndUnzip(cocoaVersion) + val xcFramework = File(frameworkDir, "Sentry.xcframework") - assertEquals(expectedArchitecture, result) + val downloadedArchNames = + xcFramework.listFiles()?.map { it.name } ?: throw IllegalStateException("No archs found") + + kmpExtension.appleTargets().forEach { + val mappedArchNames = it.toSentryFrameworkArchitecture() + val foundMatch = mappedArchNames.any { mappedArchName -> + downloadedArchNames.contains(mappedArchName) + } + + assert(foundMatch) { + "Expected to find one of $mappedArchNames in $xcFramework for target ${it.name}.\nFound instead: ${xcFramework.listFiles() + ?.map { file -> file.name }}" + } + } + } + + private fun downloadAndUnzip(cocoaVersion: String): File { + val tempDir = Files.createTempDirectory("sentry-cocoa-test").toFile() + tempDir.deleteOnExit() + + val targetFile = tempDir.resolve("Sentry.xcframework.zip") + val downloadLink = + if (cocoaVersion == "latest") "https://github.com/getsentry/sentry-cocoa/releases/latest/download/Sentry.xcframework.zip" else "https://github.com/getsentry/sentry-cocoa/releases/download/$cocoaVersion/Sentry.xcframework.zip" + + val url = URL(downloadLink) + url.openStream().use { input -> + Files.copy( + input, + targetFile.toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + } + + ZipFile(targetFile).use { zip -> + zip.entries().asSequence().forEach { entry -> + val entryFile = File(tempDir, entry.name) + if (entry.isDirectory) { + entryFile.mkdirs() + } else { + entryFile.parentFile?.mkdirs() + zip.getInputStream(entry).use { input -> + entryFile.outputStream().use { output -> + input.copyTo(output) + } + } + } + } + } + + targetFile.delete() + return tempDir } } From 6d6f6b230d4fd4b7bbe81658514bfcac967fea3c Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 30 Jan 2025 23:04:18 +0100 Subject: [PATCH 02/34] update test --- .../gradle/SentryFrameworkArchitectureTest.kt | 69 +++++++++++++++++-- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt index 719b0db4..795842e5 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt @@ -21,9 +21,9 @@ class SentryFrameworkArchitectureTest { ) } - @ParameterizedTest(name = "Test SPM compatibility with Cocoa Version {0}") + @ParameterizedTest(name = "Test architecture name compatibility with Cocoa Version {0} in static framework") @MethodSource("cocoaVersions") - fun `finds arch folders across different cocoa versions`( + fun `finds arch folders in static framework`( cocoaVersion: String ) { val project = ProjectBuilder.builder().build() @@ -53,7 +53,7 @@ class SentryFrameworkArchitectureTest { } } } - val frameworkDir = downloadAndUnzip(cocoaVersion) + val frameworkDir = downloadAndUnzip(cocoaVersion, isStatic = true) val xcFramework = File(frameworkDir, "Sentry.xcframework") val downloadedArchNames = @@ -72,14 +72,68 @@ class SentryFrameworkArchitectureTest { } } - private fun downloadAndUnzip(cocoaVersion: String): File { + @ParameterizedTest(name = "Test architecture name compatibility with Cocoa Version {0} in dynamic framework") + @MethodSource("cocoaVersions") + fun `finds arch folders in dynamic framework`( + cocoaVersion: String + ) { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply { + apply("org.jetbrains.kotlin.multiplatform") + apply("io.sentry.kotlin.multiplatform.gradle") + } + + val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension + kmpExtension.apply { + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + macosArm64(), + macosX64(), + watchosX64(), + watchosArm32(), + watchosSimulatorArm64(), + tvosX64(), + tvosArm64(), + tvosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "shared" + isStatic = false + } + } + } + val frameworkDir = downloadAndUnzip(cocoaVersion, isStatic = false) + val xcFramework = File(frameworkDir, "Sentry-Dynamic.xcframework") + + val downloadedArchNames = + xcFramework.listFiles()?.map { it.name } ?: throw IllegalStateException("No archs found") + + kmpExtension.appleTargets().forEach { + val mappedArchNames = it.toSentryFrameworkArchitecture() + val foundMatch = mappedArchNames.any { mappedArchName -> + downloadedArchNames.contains(mappedArchName) + } + + assert(foundMatch) { + "Expected to find one of $mappedArchNames in $xcFramework for target ${it.name}.\nFound instead: ${xcFramework.listFiles() + ?.map { file -> file.name }}" + } + } + } + + + private fun downloadAndUnzip(cocoaVersion: String, isStatic: Boolean): File { val tempDir = Files.createTempDirectory("sentry-cocoa-test").toFile() tempDir.deleteOnExit() - val targetFile = tempDir.resolve("Sentry.xcframework.zip") - val downloadLink = - if (cocoaVersion == "latest") "https://github.com/getsentry/sentry-cocoa/releases/latest/download/Sentry.xcframework.zip" else "https://github.com/getsentry/sentry-cocoa/releases/download/$cocoaVersion/Sentry.xcframework.zip" + val targetFile = tempDir.resolve("Framework.zip") + // Download + val xcFrameworkZip = if (isStatic) "Sentry.xcframework.zip" else "Sentry-Dynamic.xcframework.zip" + val downloadLink = + if (cocoaVersion == "latest") "https://github.com/getsentry/sentry-cocoa/releases/latest/download/$xcFrameworkZip" else "https://github.com/getsentry/sentry-cocoa/releases/download/$cocoaVersion/$xcFrameworkZip" val url = URL(downloadLink) url.openStream().use { input -> Files.copy( @@ -89,6 +143,7 @@ class SentryFrameworkArchitectureTest { ) } + // Unzip ZipFile(targetFile).use { zip -> zip.entries().asSequence().forEach { entry -> val entryFile = File(tempDir, entry.name) From 03b12e867e1e8e3dd502b38e30a7907120204322 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 30 Jan 2025 23:05:21 +0100 Subject: [PATCH 03/34] update comment --- .../java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt | 2 -- 1 file changed, 2 deletions(-) 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 80145386..0ce68af6 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 @@ -137,8 +137,6 @@ internal fun Project.configureLinkingOptions(linkerExtension: LinkerExtension) { } kmpExtension.appleTargets().all { target -> - // Contains a set of names where one should match the arch name in the framework - // This is needed for backwards compatibility as the arch names have changed throughout different versions of the Cocoa SDK val frameworkArchitectures = target.toSentryFrameworkArchitecture() if (frameworkArchitectures.isEmpty()) { logger.warn("Skipping target ${target.name} - unsupported architecture.") From a8dd89f8841aca65ea96e49a358edc6997541d80 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 31 Jan 2025 02:03:59 +0100 Subject: [PATCH 04/34] refactor linking to own file --- .../gradle/CocoaFrameworkLinker.kt | 273 ++++++++++++++++++ .../gradle/FrameworkPathValueSource.kt | 97 +++++++ .../multiplatform/gradle/LinkerExtension.kt | 18 ++ .../multiplatform/gradle/SentryPlugin.kt | 170 +---------- .../gradle/SentryFrameworkArchitectureTest.kt | 38 ++- .../SentryFrameworkPathResolutionTest.kt | 83 ++++++ 6 files changed, 509 insertions(+), 170 deletions(-) create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkPathResolutionTest.kt diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt new file mode 100644 index 00000000..064b1607 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -0,0 +1,273 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.logging.Logger +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer +import org.jetbrains.kotlin.gradle.plugin.mpp.Framework +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable +import org.jetbrains.kotlin.konan.target.HostManager +import java.io.File + +/** + * Configures Sentry Cocoa framework linking for Apple targets in Kotlin Multiplatform projects. + * + * Resolves framework paths and applies necessary linker options to both test and framework binaries. +*/ +class CocoaFrameworkLinker( + private val project: Project, + private val logger: Logger +) { + private val pathResolver = FrameworkPathResolver(project) + private val binaryLinker = FrameworkLinker(logger) + + fun configure(extension: LinkerExtension) { + val kmpExtension = + project.extensions.findByName("kotlin") as? KotlinMultiplatformExtension ?: run { + logger.info("Skipping Apple framework linking: Kotlin Multiplatform extension not found") + return + } + + if (!HostManager.hostIsMac) { + logger.info("Skipping Apple framework linking: Requires macOS host") + return + } + + kmpExtension.appleTargets().all { target -> + try { + processTarget(target, extension) + } catch (e: Exception) { + throw GradleException("Failed to configure ${target.name}: ${e.message}", e) + } + } + } + + private fun processTarget(target: KotlinNativeTarget, linker: LinkerExtension) { + val architectures = + target.toSentryFrameworkArchitecture().takeIf { it.isNotEmpty() } ?: run { + logger.warn("Skipping target ${target.name}: Unsupported architecture") + return + } + + val (dynamicPath, staticPath) = pathResolver.resolvePaths(linker, architectures) + binaryLinker.configureBinaries(target.binaries, dynamicPath, staticPath) + } +} + +internal class FrameworkPathResolver( + private val project: Project +) { + fun resolvePaths( + linker: LinkerExtension, + architectures: Set + ): Pair { + val customPath = linker.frameworkPath.orNull?.takeIf { it.isNotEmpty() } + val derivedData = linker.xcodeprojPath.orNull?.let(project::findDerivedDataPath) + ?: project.findDerivedDataPath(null) + + return customPath?.let { path -> + validateCustomPath(path) + when { + path.isStaticFrameworkPath() -> null to path + path.isDynamicFrameworkPath() -> path to null + else -> throw FrameworkLinkingException( + "Invalid framework name at $path - must be Sentry.xcframework or Sentry-Dynamic.xcframework" + ) + } + } ?: run { + Pair( + getFrameworkPath(FrameworkType.DYNAMIC, derivedData, architectures), + getFrameworkPath(FrameworkType.STATIC, derivedData, architectures) + ) + } + } + + private fun validateCustomPath(path: String) { + if (!File(path).exists()) { + throw FrameworkLinkingException( + "Custom framework path not found: $path\n" + + "Verify the path ends with either:\n" + + "- Sentry.xcframework (static)\n" + + "- Sentry-Dynamic.xcframework (dynamic)" + ) + } + } + + private fun getFrameworkPath( + type: FrameworkType, + derivedData: String, + architectures: Set + ) = project.providers.of(FrameworkPathValueSource::class.java) { + it.parameters.frameworkType.set(type) + it.parameters.derivedDataPath.set(derivedData) + it.parameters.frameworkArchitectures.set(architectures) + }.orNull + + private fun String.isStaticFrameworkPath() = endsWith("Sentry.xcframework") + private fun String.isDynamicFrameworkPath() = endsWith("Sentry-Dynamic.xcframework") +} + +internal class FrameworkLinker( + private val logger: Logger +) { + fun configureBinaries(binaries: KotlinNativeBinaryContainer, dynamicPath: String?, staticPath: String?) { + validatePaths(dynamicPath, staticPath) + + binaries.all { binary -> + when (binary) { + is TestExecutable -> linkTestBinary(binary, chooseTestPath(dynamicPath, staticPath)) + is Framework -> linkFrameworkBinary(binary, dynamicPath, staticPath) + else -> logger.info("Ignoring binary type: ${binary::class.java.simpleName}") + } + } + } + + private fun chooseTestPath(dynamic: String?, static: String?) = when { + dynamic != null && static != null -> { + logger.debug("Both framework types available, preferring dynamic for tests") + dynamic + } + + dynamic != null -> dynamic + static != null -> static + else -> throw FrameworkLinkingException("No valid framework path found for tests") + } + + private fun validatePaths(dynamic: String?, static: String?) { + if (dynamic == null && static == null) { + throw FrameworkLinkingException(frameworkNotFoundMessage) + } + } + + private fun linkTestBinary(binary: TestExecutable, path: String) { + binary.linkerOpts("-rpath", path, "-F$path") + logger.info("Linked Sentry Cocoa framework to test binary ${binary.name}") + } + + private fun linkFrameworkBinary(binary: Framework, dynamicPath: String?, staticPath: String?) { + val (path, type) = when { + binary.isStatic && staticPath != null -> staticPath to "static" + !binary.isStatic && dynamicPath != null -> dynamicPath to "dynamic" + else -> throw FrameworkLinkingException( + "Framework mismatch for ${binary.name}. " + + "Required ${if (binary.isStatic) "static" else "dynamic"} Sentry Cocoa framework not found." + ) + } + + binary.linkerOpts("-F$path") + logger.info("Linked $type Sentry Cocoa framework to ${binary.name}") + } + + private val frameworkNotFoundMessage = """ + Failed to find Sentry Cocoa framework. Steps to resolve: + + 1. Install via SPM or download the framework manually + 2. Verify framework exists in Xcode's DerivedData folder: + - Static: Sentry.xcframework + - Dynamic: Sentry-Dynamic.xcframework + 3. Either: + a) Set explicit path in build.gradle.kts: + sentryKmp { frameworkPath.set("path/to/framework") } + b) Configure Xcode project path in sentryKmp.xcodeprojPath + + More details: https://docs.sentry.io/platforms/apple/install/ + """.trimIndent() +} + +internal class FrameworkLinkingException( + message: String, + cause: Throwable? = null +) : GradleException(message, cause) + +/** + * Transforms a Kotlin Multiplatform target name to possible architecture names found inside + * Sentry's framework directory. + * + * Returns a set of possible architecture names because Sentry Cocoa SDK has changed folder naming + * across different versions. For example: + * - iosArm64 -> ["ios-arm64", "ios-arm64_arm64e"] + * - macosArm64 -> ["macos-arm64_x86_64", "macos-arm64_arm64e_x86_64"] + * * + * @return Set of possible architecture folder names for the given target. Returns empty set if target is not supported. + */ +internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set = buildSet { + when (name) { + "iosSimulatorArm64", "iosX64" -> add("ios-arm64_x86_64-simulator") + "iosArm64" -> { + add("ios-arm64") + add("ios-arm64_arm64e") + } + + "macosArm64", "macosX64" -> { + add("macos-arm64_x86_64") + add("macos-arm64_arm64e_x86_64") + } + + "tvosSimulatorArm64", "tvosX64" -> { + add("tvos-arm64_x86_64-simulator") + add("tvos-arm64_x86_64-simulator") + } + + "tvosArm64" -> { + add("tvos-arm64") + add("tvos-arm64_arm64e") + } + + "watchosArm32", "watchosArm64" -> { + add("watchos-arm64_arm64_32_armv7k") + add("watchos-arm64_arm64_32_arm64e_armv7k") + } + + "watchosSimulatorArm64", "watchosX64" -> { + add("watchos-arm64_i386_x86_64-simulator") + add("watchos-arm64_i386_x86_64-simulator") + } + + else -> emptySet() + } +} + +internal fun Project.findDerivedDataPath(customXcodeprojPath: String? = null): String { + val xcodeprojPath = customXcodeprojPath ?: findXcodeprojFile(rootDir)?.absolutePath + ?: throw GradleException("Xcode project file not found") + + return providers.of(DerivedDataPathValueSource::class.java) { + it.parameters.xcodeprojPath.set(xcodeprojPath) + }.get() +} + +/** + * Searches for a xcodeproj starting from the root directory. This function will only work for + * monorepos and if it is not, the user needs to provide the custom path through the + * [LinkerExtension] configuration. + */ +internal fun findXcodeprojFile(dir: File): File? { + val ignoredDirectories = listOf("build", "DerivedData") + + fun searchDirectory(directory: File): File? { + val files = directory.listFiles() ?: return null + + return files.firstNotNullOfOrNull { file -> + when { + file.name in ignoredDirectories -> null + file.extension == "xcodeproj" -> file + file.isDirectory -> searchDirectory(file) + else -> null + } + } + } + + return searchDirectory(dir) +} + +internal fun KotlinMultiplatformExtension.appleTargets() = + targets.withType(KotlinNativeTarget::class.java).matching { + it.konanTarget.family.isAppleFamily + } + +internal enum class FrameworkType { + STATIC, + DYNAMIC +} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt new file mode 100644 index 00000000..8d768b5c --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt @@ -0,0 +1,97 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters +import org.gradle.process.ExecOperations +import java.io.ByteArrayOutputStream +import java.io.File + +abstract class FrameworkPathValueSource : ValueSource { + interface Parameters : ValueSourceParameters { + val frameworkArchitectures: SetProperty + val frameworkType: Property + val derivedDataPath: Property + } + + @get:javax.inject.Inject + abstract val execOperations: ExecOperations + + override fun obtain(): String? { + val frameworkArchitectures = parameters.frameworkArchitectures.get() + val frameworkType = parameters.frameworkType.get() + val derivedDataPath = parameters.derivedDataPath.orNull + + frameworkArchitectures.forEach { architecture -> + if (derivedDataPath != null) { + val path = findFrameworkWithDerivedData( + frameworkType, + derivedDataPath = derivedDataPath, + frameworkArchitecture = architecture + ) + if (path != null) { + return path + } + } + + // if we didn't find anything with derived data, let's fallback to using the find cmd + val path = findFrameworkWithFindCommand(frameworkType, architecture) + if (path != null) { + return path + } + } + + return null + } + + /** + * Returns valid framework if exists with with the derived data path + */ + private fun findFrameworkWithDerivedData( + frameworkType: FrameworkType, + derivedDataPath: String, + frameworkArchitecture: String + ): String? { + val path = when (frameworkType) { + FrameworkType.DYNAMIC -> "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework/$frameworkArchitecture" + FrameworkType.STATIC -> "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework/$frameworkArchitecture" + } + return path.takeIf { File(it).exists() } + } + + /** + * Returns valid framework if exists with with the find command. + * If the command finds multiple frameworks, it will return the one who has been modified or used most recently. + */ + private fun findFrameworkWithFindCommand( + frameworkType: FrameworkType, + frameworkArchitecture: String + ): String? { + val output = ByteArrayOutputStream() + + val xcFrameworkName = + if (frameworkType == FrameworkType.STATIC) "Sentry.xcframework" else "Sentry-Dynamic.xcframework" + execOperations.exec { + it.commandLine( + "bash", "-c", + // Use the full path (replace ~ with $HOME) and escape quotes correctly + "find \"${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData\" " + + "-name $xcFrameworkName " + + "-exec stat -f \"%m %N\" {} \\; | " + + "sort -nr | " + + "cut -d' ' -f2-" + ) + it.standardOutput = output + it.isIgnoreExitValue = true + } + + val stringOutput = output.toString("UTF-8") + val path = stringOutput.lineSequence() + .firstOrNull()?.trim().orEmpty() + .let { basePath -> + "$basePath/$frameworkArchitecture" + } + return path.takeIf { File(it).exists() } + } +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/LinkerExtension.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/LinkerExtension.kt index 81505a7f..e8109acd 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/LinkerExtension.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/LinkerExtension.kt @@ -17,6 +17,24 @@ abstract class LinkerExtension @Inject constructor(project: Project) { /** * Path to the framework that will be linked. * Takes precedence over [xcodeprojPath] if both are set. + * + * The path must: + * 1. Point directly to the .xcframework folder + * 2. The .xcframework folder needs to be either `Sentry.xcframework` (static) or `Sentry-Dynamic.xcframework` + * + * ### Usage Example: + * ```kotlin + * sentryKmp { + * frameworkPath.set( + * "path/to/Sentry.xcframework" // Static framework + * // or + * "path/to/Sentry-Dynamic.xcframework" // Dynamic framework + * ) + * } + * ``` + * + * ### Typical Locations: + * `~/Library/Developer/Xcode/DerivedData/{PROJECT}/SourcePackages/artifacts/sentry-cocoa` */ val frameworkPath: Property = objects.property(String::class.java) } 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 0ce68af6..61bf506a 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 @@ -7,12 +7,8 @@ import org.gradle.api.plugins.ExtensionAware import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin -import org.jetbrains.kotlin.gradle.plugin.mpp.Framework -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable import org.jetbrains.kotlin.konan.target.HostManager import org.slf4j.LoggerFactory -import java.io.File internal const val SENTRY_EXTENSION_NAME = "sentryKmp" internal const val LINKER_EXTENSION_NAME = "linker" @@ -59,7 +55,7 @@ class SentryPlugin : Plugin { // If the user is not using the cocoapods plugin, linking to the framework is not // automatic so we have to configure it in the plugin. if (!hasCocoapodsPlugin) { - configureLinkingOptions(sentryExtension.linker) + CocoaFrameworkLinker(project, logger).configure(sentryExtension.linker) } } } @@ -85,9 +81,9 @@ internal fun Project.installSentryForKmp( if (unsupportedTargets.any { unsupported -> target.name.contains(unsupported) }) { throw GradleException( "Unsupported target: ${target.name}. " + - "Cannot auto install in commonMain. " + - "Please create an intermediate sourceSet with targets that the Sentry SDK " + - "supports (apple, jvm, android) and add the dependency manually." + "Cannot auto install in commonMain. " + + "Please create an intermediate sourceSet with targets that the Sentry SDK " + + "supports (apple, jvm, android) and add the dependency manually." ) } } @@ -120,161 +116,3 @@ internal fun Project.installSentryForCocoapods( } } } - -@Suppress("CyclomaticComplexMethod") -internal fun Project.configureLinkingOptions(linkerExtension: LinkerExtension) { - val kmpExtension = extensions.findByName(KOTLIN_EXTENSION_NAME) - if (kmpExtension !is KotlinMultiplatformExtension || kmpExtension.targets.isEmpty() || !HostManager.hostIsMac) { - logger.info("Skipping linker configuration.") - return - } - - var derivedDataPath = "" - val frameworkPath = linkerExtension.frameworkPath.orNull - if (frameworkPath == null) { - val customXcodeprojPath = linkerExtension.xcodeprojPath.orNull - derivedDataPath = findDerivedDataPath(customXcodeprojPath) - } - - kmpExtension.appleTargets().all { target -> - val frameworkArchitectures = target.toSentryFrameworkArchitecture() - if (frameworkArchitectures.isEmpty()) { - logger.warn("Skipping target ${target.name} - unsupported architecture.") - return@all - } - - var dynamicFrameworkPath: String? = null - var staticFrameworkPath: String? = null - - if (frameworkPath?.isNotEmpty() == true) { - dynamicFrameworkPath = frameworkPath - staticFrameworkPath = frameworkPath - } else { - frameworkArchitectures.forEach { - val dynamicPath = - "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework/$it" - logger.info("Looking for dynamic framework at $dynamicPath") - if (File(dynamicPath).exists()) { - logger.info("Found dynamic framework at $dynamicPath") - dynamicFrameworkPath = dynamicPath - return@forEach - } - - val staticPath = "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework/$it" - logger.info("Looking for dynamic framework at $dynamicPath") - if (File(staticPath).exists()) { - logger.info("Found static framework at $staticPath") - staticFrameworkPath = staticPath - return@forEach - } - } - } - - if (staticFrameworkPath == null && dynamicFrameworkPath == null) { - throw GradleException( - "Sentry Cocoa Framework not found. Make sure the Sentry Cocoa SDK is installed with SPM in your Xcode project." - ) - } - - target.binaries.all binaries@{ binary -> - if (binary is TestExecutable) { - // both dynamic and static frameworks will work for tests - val path = (dynamicFrameworkPath ?: staticFrameworkPath)!! - binary.linkerOpts("-rpath", path, "-F$path") - } - - if (binary is Framework) { - val finalFrameworkPath = when { - binary.isStatic && staticFrameworkPath != null -> staticFrameworkPath - !binary.isStatic && dynamicFrameworkPath != null -> dynamicFrameworkPath - else -> { - logger.warn("Linking to framework failed, no sentry framework found for target ${target.name}") - return@binaries - } - } - binary.linkerOpts("-F$finalFrameworkPath") - logger.info("Linked framework from $finalFrameworkPath") - } - } - } -} - -/** - * Transforms a Kotlin Multiplatform target name to possible architecture names found inside - * Sentry's framework directory. - * - * Returns a set of possible architecture names because Sentry Cocoa SDK has changed folder naming - * across different versions. For example: - * - iosArm64 -> ["ios-arm64", "ios-arm64_arm64e"] - * - macosArm64 -> ["macos-arm64_x86_64", "macos-arm64_arm64e_x86_64"] - * * - * @return Set of possible architecture folder names for the given target. Returns empty set if target is not supported. - */ -internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set = buildSet { - when (name) { - "iosSimulatorArm64", "iosX64" -> add("ios-arm64_x86_64-simulator") - "iosArm64" -> { - add("ios-arm64") - add("ios-arm64_arm64e") - } - "macosArm64", "macosX64" -> { - add("macos-arm64_x86_64") - add("macos-arm64_arm64e_x86_64") - } - "tvosSimulatorArm64", "tvosX64" -> { - add("tvos-arm64_x86_64-simulator") - add("tvos-arm64_x86_64-simulator") - } - "tvosArm64" -> { - add("tvos-arm64") - add("tvos-arm64_arm64e") - } - "watchosArm32", "watchosArm64" -> { - add("watchos-arm64_arm64_32_armv7k") - add("watchos-arm64_arm64_32_arm64e_armv7k") - } - "watchosSimulatorArm64", "watchosX64" -> { - add("watchos-arm64_i386_x86_64-simulator") - add("watchos-arm64_i386_x86_64-simulator") - } - else -> emptySet() - } -} - -private fun Project.findDerivedDataPath(customXcodeprojPath: String? = null): String { - val xcodeprojPath = customXcodeprojPath ?: findXcodeprojFile(rootDir)?.absolutePath - ?: throw GradleException("Xcode project file not found") - - return providers.of(DerivedDataPathValueSource::class.java) { - it.parameters.xcodeprojPath.set(xcodeprojPath) - }.get() -} - -/** - * Searches for a xcodeproj starting from the root directory. This function will only work for - * monorepos and if it is not, the user needs to provide the custom path through the - * [LinkerExtension] configuration. - */ -internal fun findXcodeprojFile(dir: File): File? { - val ignoredDirectories = listOf("build", "DerivedData") - - fun searchDirectory(directory: File): File? { - val files = directory.listFiles() ?: return null - - return files.firstNotNullOfOrNull { file -> - when { - file.name in ignoredDirectories -> null - file.extension == "xcodeproj" -> file - file.isDirectory -> searchDirectory(file) - else -> null - } - } - } - - return searchDirectory(dir) -} - -internal fun KotlinMultiplatformExtension.appleTargets() = - targets.withType(KotlinNativeTarget::class.java).matching { - it.konanTarget.family.isAppleFamily - } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt index 795842e5..462c5e0a 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt @@ -1,7 +1,13 @@ package io.sentry.kotlin.multiplatform.gradle +import io.mockk.every +import io.mockk.mockk +import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.konan.target.KonanTarget +import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource @@ -66,8 +72,10 @@ class SentryFrameworkArchitectureTest { } assert(foundMatch) { - "Expected to find one of $mappedArchNames in $xcFramework for target ${it.name}.\nFound instead: ${xcFramework.listFiles() - ?.map { file -> file.name }}" + "Expected to find one of $mappedArchNames in $xcFramework for target ${it.name}.\nFound instead: ${ + xcFramework.listFiles() + ?.map { file -> file.name } + }" } } } @@ -117,12 +125,28 @@ class SentryFrameworkArchitectureTest { } assert(foundMatch) { - "Expected to find one of $mappedArchNames in $xcFramework for target ${it.name}.\nFound instead: ${xcFramework.listFiles() - ?.map { file -> file.name }}" + "Expected to find one of $mappedArchNames in $xcFramework for target ${it.name}.\nFound instead: ${ + xcFramework.listFiles() + ?.map { file -> file.name } + }" } } } + @Test + fun `returns empty list if target is unsupported`() { + val unsupportedTarget = mockk() + every { unsupportedTarget.name } returns "unsupported" + every { unsupportedTarget.konanTarget } returns mockk { + every { family } returns mockk { + every { isAppleFamily } returns true + } + } + + assert(unsupportedTarget.toSentryFrameworkArchitecture().isEmpty()) { + "Expected empty list for unsupported target" + } + } private fun downloadAndUnzip(cocoaVersion: String, isStatic: Boolean): File { val tempDir = Files.createTempDirectory("sentry-cocoa-test").toFile() @@ -164,3 +188,9 @@ class SentryFrameworkArchitectureTest { return tempDir } } + +private class FakeTarget(project: Project, konanTarget: KonanTarget) : KotlinNativeTarget(project, konanTarget) { + override fun getName(): String { + return "fake" + } +} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkPathResolutionTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkPathResolutionTest.kt new file mode 100644 index 00000000..6d662bc3 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkPathResolutionTest.kt @@ -0,0 +1,83 @@ +package io.sentry.kotlin.multiplatform.gradle + +import io.mockk.every +import io.mockk.mockk +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class SentryFrameworkPathResolutionTest { + @TempDir + lateinit var tempDir: File + + @Test + fun `resolves dynamic framework path when it exists`() { + // Setup mock derived data structure + val derivedDataPath = setupMockDerivedDataStructure(isDynamic = true) + + // Create test project and target + val project = ProjectBuilder.builder().build() + val target = mockk() + every { target.name } returns "iosArm64" + every { target.konanTarget.family.isAppleFamily } returns true + + // Create framework directories + val frameworkPath = "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework/ios-arm64" + File(frameworkPath).mkdirs() + + // Test framework resolution + val result = project.findFrameworkPath( + target = target, + derivedDataPath = derivedDataPath, + frameworkPath = null + ) + + assertNotNull(result) + assertTrue(result.first.contains("ios-arm64")) + assertTrue(result.second.contains("Sentry-Dynamic.xcframework")) + } + + @Test + fun `resolves static framework path when dynamic doesn't exist`() { + // Setup mock derived data structure + val derivedDataPath = setupMockDerivedDataStructure(isDynamic = false) + + // Create test project and target + val project = ProjectBuilder.builder().build() + val target = mockk() + every { target.name } returns "iosArm64" + every { target.konanTarget.family.isAppleFamily } returns true + + // Create framework directories + val frameworkPath = "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework/ios-arm64" + File(frameworkPath).mkdirs() + + // Test framework resolution + val result = project.findFrameworkPath( + target = target, + derivedDataPath = derivedDataPath, + frameworkPath = null + ) + + assertNotNull(result) + assertTrue(result.first.contains("ios-arm64")) + assertTrue(result.second.contains("Sentry.xcframework")) + } + + private fun setupMockDerivedDataStructure(isDynamic: Boolean): String { + val derivedDataPath = tempDir.resolve("DerivedData").absolutePath + val baseDir = if (isDynamic) { + File("$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic") + } else { + File("$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry") + } + baseDir.mkdirs() + return derivedDataPath + } +} \ No newline at end of file From b2ba00fd8c10fd414a5f3ae23a2afe900be25742 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 31 Jan 2025 02:06:52 +0100 Subject: [PATCH 05/34] update --- .../gradle/CocoaFrameworkLinker.kt | 2 +- .../SentryFrameworkPathResolutionTest.kt | 83 ------------------- .../multiplatform/gradle/SentryPluginTest.kt | 2 +- 3 files changed, 2 insertions(+), 85 deletions(-) delete mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkPathResolutionTest.kt diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index 064b1607..c984fd0e 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -267,7 +267,7 @@ internal fun KotlinMultiplatformExtension.appleTargets() = it.konanTarget.family.isAppleFamily } -internal enum class FrameworkType { +enum class FrameworkType { STATIC, DYNAMIC } \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkPathResolutionTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkPathResolutionTest.kt deleted file mode 100644 index 6d662bc3..00000000 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkPathResolutionTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -package io.sentry.kotlin.multiplatform.gradle - -import io.mockk.every -import io.mockk.mockk -import org.gradle.api.Project -import org.gradle.testfixtures.ProjectBuilder -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.io.TempDir -import java.io.File -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class SentryFrameworkPathResolutionTest { - @TempDir - lateinit var tempDir: File - - @Test - fun `resolves dynamic framework path when it exists`() { - // Setup mock derived data structure - val derivedDataPath = setupMockDerivedDataStructure(isDynamic = true) - - // Create test project and target - val project = ProjectBuilder.builder().build() - val target = mockk() - every { target.name } returns "iosArm64" - every { target.konanTarget.family.isAppleFamily } returns true - - // Create framework directories - val frameworkPath = "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework/ios-arm64" - File(frameworkPath).mkdirs() - - // Test framework resolution - val result = project.findFrameworkPath( - target = target, - derivedDataPath = derivedDataPath, - frameworkPath = null - ) - - assertNotNull(result) - assertTrue(result.first.contains("ios-arm64")) - assertTrue(result.second.contains("Sentry-Dynamic.xcframework")) - } - - @Test - fun `resolves static framework path when dynamic doesn't exist`() { - // Setup mock derived data structure - val derivedDataPath = setupMockDerivedDataStructure(isDynamic = false) - - // Create test project and target - val project = ProjectBuilder.builder().build() - val target = mockk() - every { target.name } returns "iosArm64" - every { target.konanTarget.family.isAppleFamily } returns true - - // Create framework directories - val frameworkPath = "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework/ios-arm64" - File(frameworkPath).mkdirs() - - // Test framework resolution - val result = project.findFrameworkPath( - target = target, - derivedDataPath = derivedDataPath, - frameworkPath = null - ) - - assertNotNull(result) - assertTrue(result.first.contains("ios-arm64")) - assertTrue(result.second.contains("Sentry.xcframework")) - } - - private fun setupMockDerivedDataStructure(isDynamic: Boolean): String { - val derivedDataPath = tempDir.resolve("DerivedData").absolutePath - val baseDir = if (isDynamic) { - File("$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic") - } else { - File("$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry") - } - baseDir.mkdirs() - return derivedDataPath - } -} \ No newline at end of file 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 0cf1a25f..cf9d4ee4 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 @@ -221,7 +221,7 @@ class SentryPluginTest { val linkerExtension = project.extensions.getByName("linker") as LinkerExtension linkerExtension.frameworkPath.set(file.absolutePath) - project.configureLinkingOptions(linkerExtension) + CocoaFrameworkLinker(project, project.logger).configure(linkerExtension) kmpExtension.apply { val frameworks = appleTargets().map { From 6d90d3f53f0a0fbe870ef12c1e544866495472bf Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 31 Jan 2025 11:21:37 +0100 Subject: [PATCH 06/34] update --- .../kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt | 9 +++++---- .../multiplatform/gradle/FrameworkPathValueSource.kt | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index c984fd0e..66d38d43 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -87,12 +87,13 @@ internal class FrameworkPathResolver( private fun validateCustomPath(path: String) { if (!File(path).exists()) { throw FrameworkLinkingException( - "Custom framework path not found: $path\n" + - "Verify the path ends with either:\n" + - "- Sentry.xcframework (static)\n" + - "- Sentry-Dynamic.xcframework (dynamic)" + "Custom framework path not found or does not exist: $path" ) } + + if (path.isStaticFrameworkPath().not() && path.isDynamicFrameworkPath().not()) { + throw FrameworkLinkingException("Invalid framework at $path - path must end with Sentry.xcframework or Sentry-Dynamic.xcframework") + } } private fun getFrameworkPath( diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt index 8d768b5c..4cd4dbe2 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt @@ -75,7 +75,6 @@ abstract class FrameworkPathValueSource : ValueSource Date: Fri, 31 Jan 2025 11:23:16 +0100 Subject: [PATCH 07/34] update comment --- .../io/sentry/kotlin/multiplatform/gradle/LinkerExtension.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/LinkerExtension.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/LinkerExtension.kt index e8109acd..f9f0114e 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/LinkerExtension.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/LinkerExtension.kt @@ -20,7 +20,7 @@ abstract class LinkerExtension @Inject constructor(project: Project) { * * The path must: * 1. Point directly to the .xcframework folder - * 2. The .xcframework folder needs to be either `Sentry.xcframework` (static) or `Sentry-Dynamic.xcframework` + * 2. The .xcframework folder needs to be either `Sentry.xcframework` or `Sentry-Dynamic.xcframework` * * ### Usage Example: * ```kotlin From e0f2e1b02ace792d4f7761f0ab8c5ddc06807087 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 31 Jan 2025 11:51:35 +0100 Subject: [PATCH 08/34] update comment --- .../gradle/CocoaFrameworkLinker.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index 66d38d43..5ebaa63f 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -96,6 +96,9 @@ internal class FrameworkPathResolver( } } + /** + * Fallback method for fetching paths + */ private fun getFrameworkPath( type: FrameworkType, derivedData: String, @@ -164,14 +167,17 @@ internal class FrameworkLinker( private val frameworkNotFoundMessage = """ Failed to find Sentry Cocoa framework. Steps to resolve: - 1. Install via SPM or download the framework manually + 1. Install Sentry Cocoa via SPM in Xcode 2. Verify framework exists in Xcode's DerivedData folder: - Static: Sentry.xcframework - Dynamic: Sentry-Dynamic.xcframework - 3. Either: - a) Set explicit path in build.gradle.kts: - sentryKmp { frameworkPath.set("path/to/framework") } - b) Configure Xcode project path in sentryKmp.xcodeprojPath + + If problem persists consider setting explicit path in build.gradle.kts: + sentryKmp { + linker { + frameworkPath.set("path/to/framework") + } + } More details: https://docs.sentry.io/platforms/apple/install/ """.trimIndent() @@ -225,8 +231,6 @@ internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set = b add("watchos-arm64_i386_x86_64-simulator") add("watchos-arm64_i386_x86_64-simulator") } - - else -> emptySet() } } From 9c82a2a09e9fbfab7607e143966845adcf680144 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 31 Jan 2025 11:51:57 +0100 Subject: [PATCH 09/34] update message --- .../sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index 5ebaa63f..402f8547 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -178,8 +178,6 @@ internal class FrameworkLinker( frameworkPath.set("path/to/framework") } } - - More details: https://docs.sentry.io/platforms/apple/install/ """.trimIndent() } From a63d95e3eff03673fa7dc8fb02139c505a5ee817 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 4 Feb 2025 16:41:16 +0100 Subject: [PATCH 10/34] draft commit --- .../gradle/CocoaFrameworkLinker.kt | 103 +++-------- .../gradle/DerivedDataPathValueSource.kt | 17 +- .../gradle/FrameworkPathResolver.kt | 172 ++++++++++++++++++ .../gradle/FrameworkPathValueSource.kt | 96 ---------- .../ManualFrameworkPathSearchValueSource.kt | 56 ++++++ .../multiplatform/gradle/SentryPlugin.kt | 23 ++- .../gradle/CocoaFrameworkLinkerTest.kt | 115 ++++++++++++ .../gradle/DerivedDataPathTest.kt | 1 - .../gradle/FrameworkPathResolverTest.kt | 50 +++++ .../sentry/kotlin/multiplatform/gradle/Old.kt | 140 ++++++++++++++ .../gradle/SentryFrameworkArchitectureTest.kt | 8 - .../multiplatform/gradle/SentryPluginTest.kt | 2 +- 12 files changed, 585 insertions(+), 198 deletions(-) create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt delete mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/Old.kt diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index 402f8547..2ef60b39 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -8,115 +8,59 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable -import org.jetbrains.kotlin.konan.target.HostManager import java.io.File /** * Configures Sentry Cocoa framework linking for Apple targets in Kotlin Multiplatform projects. * * Resolves framework paths and applies necessary linker options to both test and framework binaries. -*/ + */ class CocoaFrameworkLinker( - private val project: Project, - private val logger: Logger + private val logger: Logger, + private val pathResolver: FrameworkPathResolver, + private val binaryLinker: FrameworkLinker, + private val hostIsMac: Boolean ) { - private val pathResolver = FrameworkPathResolver(project) - private val binaryLinker = FrameworkLinker(logger) - - fun configure(extension: LinkerExtension) { - val kmpExtension = - project.extensions.findByName("kotlin") as? KotlinMultiplatformExtension ?: run { - logger.info("Skipping Apple framework linking: Kotlin Multiplatform extension not found") - return - } - - if (!HostManager.hostIsMac) { + fun configure( + appleTargets: List, + ) { + if (!hostIsMac) { logger.info("Skipping Apple framework linking: Requires macOS host") return } - kmpExtension.appleTargets().all { target -> + appleTargets.forEach { target -> try { - processTarget(target, extension) + logger.lifecycle( + "Start resolving cocoa framework paths for target: ${target.name}" + ) + processTarget(target) } catch (e: Exception) { throw GradleException("Failed to configure ${target.name}: ${e.message}", e) } } } - private fun processTarget(target: KotlinNativeTarget, linker: LinkerExtension) { + private fun processTarget(target: KotlinNativeTarget) { val architectures = target.toSentryFrameworkArchitecture().takeIf { it.isNotEmpty() } ?: run { logger.warn("Skipping target ${target.name}: Unsupported architecture") return } - val (dynamicPath, staticPath) = pathResolver.resolvePaths(linker, architectures) + val (dynamicPath, staticPath) = pathResolver.resolvePaths(architectures) binaryLinker.configureBinaries(target.binaries, dynamicPath, staticPath) } } -internal class FrameworkPathResolver( - private val project: Project -) { - fun resolvePaths( - linker: LinkerExtension, - architectures: Set - ): Pair { - val customPath = linker.frameworkPath.orNull?.takeIf { it.isNotEmpty() } - val derivedData = linker.xcodeprojPath.orNull?.let(project::findDerivedDataPath) - ?: project.findDerivedDataPath(null) - - return customPath?.let { path -> - validateCustomPath(path) - when { - path.isStaticFrameworkPath() -> null to path - path.isDynamicFrameworkPath() -> path to null - else -> throw FrameworkLinkingException( - "Invalid framework name at $path - must be Sentry.xcframework or Sentry-Dynamic.xcframework" - ) - } - } ?: run { - Pair( - getFrameworkPath(FrameworkType.DYNAMIC, derivedData, architectures), - getFrameworkPath(FrameworkType.STATIC, derivedData, architectures) - ) - } - } - - private fun validateCustomPath(path: String) { - if (!File(path).exists()) { - throw FrameworkLinkingException( - "Custom framework path not found or does not exist: $path" - ) - } - - if (path.isStaticFrameworkPath().not() && path.isDynamicFrameworkPath().not()) { - throw FrameworkLinkingException("Invalid framework at $path - path must end with Sentry.xcframework or Sentry-Dynamic.xcframework") - } - } - - /** - * Fallback method for fetching paths - */ - private fun getFrameworkPath( - type: FrameworkType, - derivedData: String, - architectures: Set - ) = project.providers.of(FrameworkPathValueSource::class.java) { - it.parameters.frameworkType.set(type) - it.parameters.derivedDataPath.set(derivedData) - it.parameters.frameworkArchitectures.set(architectures) - }.orNull - - private fun String.isStaticFrameworkPath() = endsWith("Sentry.xcframework") - private fun String.isDynamicFrameworkPath() = endsWith("Sentry-Dynamic.xcframework") -} - -internal class FrameworkLinker( +class FrameworkLinker( private val logger: Logger ) { - fun configureBinaries(binaries: KotlinNativeBinaryContainer, dynamicPath: String?, staticPath: String?) { + fun configureBinaries( + binaries: KotlinNativeBinaryContainer, + dynamicPath: String?, + staticPath: String? + ) { validatePaths(dynamicPath, staticPath) binaries.all { binary -> @@ -146,6 +90,7 @@ internal class FrameworkLinker( } private fun linkTestBinary(binary: TestExecutable, path: String) { + // Linking in test binaries works with both dynamic and static framework binary.linkerOpts("-rpath", path, "-F$path") logger.info("Linked Sentry Cocoa framework to test binary ${binary.name}") } @@ -232,7 +177,7 @@ internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set = b } } -internal fun Project.findDerivedDataPath(customXcodeprojPath: String? = null): String { +internal fun Project.findDerivedDataPath(customXcodeprojPath: String? = null): String? { val xcodeprojPath = customXcodeprojPath ?: findXcodeprojFile(rootDir)?.absolutePath ?: throw GradleException("Xcode project file not found") diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathValueSource.kt index 12dbeafb..a28f97bb 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathValueSource.kt @@ -1,6 +1,5 @@ package io.sentry.kotlin.multiplatform.gradle -import org.gradle.api.GradleException import org.gradle.api.provider.Property import org.gradle.api.provider.ValueSource import org.gradle.api.provider.ValueSourceParameters @@ -9,8 +8,13 @@ import org.gradle.process.ExecOperations import java.io.ByteArrayOutputStream import javax.inject.Inject -internal abstract class DerivedDataPathValueSource : - ValueSource { +/** + * Provides the derived data path for an Xcode project using the xcodebuild command. + * + * e.g /Users/theusername/Library/Developer/Xcode/DerivedData/iosApp-ddefikekigqzzgcnpfkkdallksmlfpln/ + */ +abstract class DerivedDataPathValueSource : + ValueSource { interface Parameters : ValueSourceParameters { @get:Input val xcodeprojPath: Property @@ -23,7 +27,7 @@ internal abstract class DerivedDataPathValueSource : private val buildDirRegex = Regex("BUILD_DIR = (.+)") } - override fun obtain(): String { + override fun obtain(): String? { val buildDirOutput = ByteArrayOutputStream() execOperations.exec { it.commandLine = listOf( @@ -37,7 +41,10 @@ internal abstract class DerivedDataPathValueSource : val buildSettings = buildDirOutput.toString("UTF-8") val buildDirMatch = buildDirRegex.find(buildSettings) val buildDir = buildDirMatch?.groupValues?.get(1) - ?: throw GradleException("BUILD_DIR not found in xcodebuild output") + ?: return null + if (buildDir.contains("DerivedData").not()) { + return null + } return buildDir.replace("/Build/Products", "") } } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt new file mode 100644 index 00000000..d1b9a463 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -0,0 +1,172 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.api.Project +import java.io.File + +data class FrameworkPaths( + val dynamic: String? = null, + val static: String? = null +) { + companion object { + val NONE = FrameworkPaths(null, null) + + fun createValidated( + dynamicBasePath: String? = null, + staticBasePath: String? = null, + architectures: Set, + pathExists: (String) -> Boolean = { path -> File(path).exists() } + ): FrameworkPaths { + // Find first valid dynamic path + val dynamicPath = dynamicBasePath?.let { basePath -> + architectures.firstNotNullOfOrNull { arch -> + val path = "$basePath/$arch" + path.takeIf { pathExists(it) } + } + } + + // Find first valid static path + val staticPath = staticBasePath?.let { basePath -> + architectures.firstNotNullOfOrNull { arch -> + val path = "$basePath/$arch" + path.takeIf { pathExists(it) } + } + } + + return when { + dynamicPath != null && staticPath != null -> + FrameworkPaths(dynamic = dynamicPath, static = staticPath) + + dynamicPath != null -> + FrameworkPaths(dynamic = dynamicPath) + + staticPath != null -> + FrameworkPaths(static = staticPath) + + else -> + NONE + } + } + } +} + +sealed interface FrameworkResolutionStrategy { + fun resolvePaths( + architectures: Set, + ): FrameworkPaths +} + +/** + * Handles the custom framework paths set by the user. This should generally be executed first. + */ +class CustomPathStrategy( + private val project: Project, +) : FrameworkResolutionStrategy { + private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) + + // In this function we don't really distinguish between static and dynamic framework + // We trust that the user knows the distinction if they purposefully override the framework path + override fun resolvePaths(architectures: Set): FrameworkPaths { + return linker.frameworkPath.orNull?.takeIf { it.isNotEmpty() }?.let { basePath -> + when { + basePath.endsWith("Sentry.xcframework") -> FrameworkPaths.createValidated( + staticBasePath = basePath, + architectures = architectures + ) + + basePath.endsWith("Sentry-Dynamic.xcframework") -> FrameworkPaths.createValidated( + dynamicBasePath = basePath, + architectures = architectures + ) + + else -> null + } + } ?: FrameworkPaths.NONE + } +} + +class DerivedDataStrategy( + private val project: Project, +) : FrameworkResolutionStrategy { + private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) + + override fun resolvePaths(architectures: Set): FrameworkPaths { + val xcodeprojPath = linker.xcodeprojPath.orNull + val derivedDataPath = xcodeprojPath?.let { path -> + project.providers.of(DerivedDataPathValueSource::class.java) { + it.parameters.xcodeprojPath.set(path) + }.get() + } ?: return FrameworkPaths.NONE + val dynamicBasePath = + "${derivedDataPath}/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework" + val staticBasePath = + "${derivedDataPath}/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework" + + return FrameworkPaths.createValidated( + dynamicBasePath = dynamicBasePath, + staticBasePath = staticBasePath, + architectures = architectures + ) + } +} + +class ManualSearchStrategy( + private val project: Project, +) : FrameworkResolutionStrategy { + override fun resolvePaths(architectures: Set): FrameworkPaths { + val dynamicValueSource = + project.providers.of(ManualFrameworkPathSearchValueSource::class.java) { + it.parameters.frameworkType.set(FrameworkType.DYNAMIC) + it.parameters.frameworkArchitectures.set(architectures) + } + val staticValueSource = + project.providers.of(ManualFrameworkPathSearchValueSource::class.java) { + it.parameters.frameworkType.set(FrameworkType.STATIC) + it.parameters.frameworkArchitectures.set(architectures) + } + + return FrameworkPaths.createValidated( + dynamicBasePath = dynamicValueSource.orNull, + staticBasePath = staticValueSource.orNull, + architectures = architectures + ) + } +} + +class FrameworkPathResolver( + private val project: Project, + private val strategies: List = defaultStrategies(project), +) { + fun resolvePaths( + architectures: Set + ): FrameworkPaths { + return strategies.firstNotNullOfOrNull { strategy -> + try { + strategy.resolvePaths(architectures) + } catch (e: Exception) { + project.logger.debug( + "Path resolution strategy ${strategy::class.simpleName} failed", + e + ) + null + } + } + ?: throw FrameworkLinkingException("All framework resolution strategies failed. Could not find Sentry Cocoa framework path") + } + + companion object { + /** + * Resolution strategies for finding the framework path + * + * The order of resolution strategies matters, as the framework path will be resolved by the first successful strategy + * Specifically here Custom Path will be checked first, if that fails then it is followed by the Derived Data strategy etc... + */ + fun defaultStrategies(project: Project): List { + return listOf( + CustomPathStrategy(project), + DerivedDataStrategy(project), + ManualSearchStrategy(project) + ) + } + } + +} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt deleted file mode 100644 index 4cd4dbe2..00000000 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathValueSource.kt +++ /dev/null @@ -1,96 +0,0 @@ -package io.sentry.kotlin.multiplatform.gradle - -import org.gradle.api.provider.Property -import org.gradle.api.provider.SetProperty -import org.gradle.api.provider.ValueSource -import org.gradle.api.provider.ValueSourceParameters -import org.gradle.process.ExecOperations -import java.io.ByteArrayOutputStream -import java.io.File - -abstract class FrameworkPathValueSource : ValueSource { - interface Parameters : ValueSourceParameters { - val frameworkArchitectures: SetProperty - val frameworkType: Property - val derivedDataPath: Property - } - - @get:javax.inject.Inject - abstract val execOperations: ExecOperations - - override fun obtain(): String? { - val frameworkArchitectures = parameters.frameworkArchitectures.get() - val frameworkType = parameters.frameworkType.get() - val derivedDataPath = parameters.derivedDataPath.orNull - - frameworkArchitectures.forEach { architecture -> - if (derivedDataPath != null) { - val path = findFrameworkWithDerivedData( - frameworkType, - derivedDataPath = derivedDataPath, - frameworkArchitecture = architecture - ) - if (path != null) { - return path - } - } - - // if we didn't find anything with derived data, let's fallback to using the find cmd - val path = findFrameworkWithFindCommand(frameworkType, architecture) - if (path != null) { - return path - } - } - - return null - } - - /** - * Returns valid framework if exists with with the derived data path - */ - private fun findFrameworkWithDerivedData( - frameworkType: FrameworkType, - derivedDataPath: String, - frameworkArchitecture: String - ): String? { - val path = when (frameworkType) { - FrameworkType.DYNAMIC -> "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework/$frameworkArchitecture" - FrameworkType.STATIC -> "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework/$frameworkArchitecture" - } - return path.takeIf { File(it).exists() } - } - - /** - * Returns valid framework if exists with with the find command. - * If the command finds multiple frameworks, it will return the one who has been modified or used most recently. - */ - private fun findFrameworkWithFindCommand( - frameworkType: FrameworkType, - frameworkArchitecture: String - ): String? { - val output = ByteArrayOutputStream() - - val xcFrameworkName = - if (frameworkType == FrameworkType.STATIC) "Sentry.xcframework" else "Sentry-Dynamic.xcframework" - execOperations.exec { - it.commandLine( - "bash", "-c", - "find \"${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData\" " + - "-name $xcFrameworkName " + - "-exec stat -f \"%m %N\" {} \\; | " + - "sort -nr | " + - "cut -d' ' -f2-" - ) - it.standardOutput = output - it.isIgnoreExitValue = true - } - - val stringOutput = output.toString("UTF-8") - val path = stringOutput.lineSequence() - .firstOrNull()?.trim().orEmpty() - .let { basePath -> - "$basePath/$frameworkArchitecture" - } - return path.takeIf { File(it).exists() } - } -} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt new file mode 100644 index 00000000..ae72a2b5 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt @@ -0,0 +1,56 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters +import org.gradle.process.ExecOperations +import java.io.ByteArrayOutputStream + +abstract class ManualFrameworkPathSearchValueSource : ValueSource { + interface Parameters : ValueSourceParameters { + val frameworkArchitectures: SetProperty + val frameworkType: Property + } + + @get:javax.inject.Inject + abstract val execOperations: ExecOperations + + override fun obtain(): String? { + val frameworkArchitectures = parameters.frameworkArchitectures.get() + val frameworkType = parameters.frameworkType.get() + + return frameworkArchitectures.firstNotNullOfOrNull { architecture -> + findFrameworkWithFindCommand(frameworkType, architecture) + } + } + + /** + * Returns valid framework if exists with with the find command. + * If the command finds multiple frameworks, it will return the one who has been modified or used most recently. + */ + private fun findFrameworkWithFindCommand( + frameworkType: FrameworkType, + frameworkArchitecture: String + ): String? { + val output = ByteArrayOutputStream() + + val xcFrameworkName = + if (frameworkType == FrameworkType.STATIC) "Sentry.xcframework" else "Sentry-Dynamic.xcframework" + execOperations.exec { + it.commandLine( + "bash", "-c", + "find \"${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData\" " + + "-name $xcFrameworkName " + + "-exec stat -f \"%m %N\" {} \\; | " + + "sort -nr | " + + "cut -d' ' -f2-" + ) + it.standardOutput = output + it.isIgnoreExitValue = true + } + + val stringOutput = output.toString("UTF-8") + return stringOutput.lineSequence().firstOrNull() + } +} 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 61bf506a..4bdc8bef 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 @@ -8,7 +8,6 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin import org.jetbrains.kotlin.konan.target.HostManager -import org.slf4j.LoggerFactory internal const val SENTRY_EXTENSION_NAME = "sentryKmp" internal const val LINKER_EXTENSION_NAME = "linker" @@ -55,16 +54,24 @@ class SentryPlugin : Plugin { // If the user is not using the cocoapods plugin, linking to the framework is not // automatic so we have to configure it in the plugin. if (!hasCocoapodsPlugin) { - CocoaFrameworkLinker(project, logger).configure(sentryExtension.linker) + logger.info("Cocoapods plugin not found. Attempting to link Sentry Cocoa framework.") + + val kmpExtension = + extensions.findByName(KOTLIN_EXTENSION_NAME) as? KotlinMultiplatformExtension + val appleTargets = kmpExtension?.appleTargets()?.toList() + ?: throw GradleException("Error fetching Apple targets from Kotlin Multiplatform plugin.") + + CocoaFrameworkLinker( + logger = logger, + pathResolver = FrameworkPathResolver(project), + binaryLinker = FrameworkLinker(logger), + HostManager.hostIsMac + ).configure( + appleTargets, + ) } } } - - companion object { - internal val logger by lazy { - LoggerFactory.getLogger(SentryPlugin::class.java) - } - } } internal fun Project.installSentryForKmp( diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt new file mode 100644 index 00000000..615b151a --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt @@ -0,0 +1,115 @@ +import io.mockk.* +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.sentry.kotlin.multiplatform.gradle.* +import org.gradle.api.logging.Logger +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.mpp.Framework +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +class CocoaFrameworkLinkerTest { + @MockK + lateinit var mockLogger: Logger + + @MockK + lateinit var mockPathResolver: FrameworkPathResolver + + @MockK + lateinit var mockFrameworkLinker: FrameworkLinker + + @MockK + lateinit var mockLinkerExtension: LinkerExtension + + private lateinit var linker: CocoaFrameworkLinker + + @BeforeEach + fun setup() { + every { mockLogger.info(any()) } just Runs + linker = CocoaFrameworkLinker(mockLogger, mockPathResolver, mockFrameworkLinker) + } + + @Test + fun `configure should skip when not on macOS host`() { + linker.configure(mockLinkerExtension, emptyList(), hostIsMac = false) + + verify { mockLogger.info("Skipping Apple framework linking: Requires macOS host") } + verify { mockPathResolver wasNot Called } + verify { mockFrameworkLinker wasNot Called } + } + + @Test + fun `integration test`() { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply { + apply("org.jetbrains.kotlin.multiplatform") + apply("io.sentry.kotlin.multiplatform.gradle") + } + + val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension + kmpExtension.apply { + listOf( + iosArm64(), + ).forEach { + it.binaries.framework { + baseName = "shared" + isStatic = false + } + } + } + + val linker2 = project.extensions.getByName("linker") as LinkerExtension + CocoaFrameworkLinker( + project.logger, + FrameworkPathResolver2(), + FrameworkLinker(project.logger) + ).configure(linker2, kmpExtension.appleTargets().toList(), hostIsMac = true) + + kmpExtension.iosArm64().binaries.filterIsInstance().forEach { + println(it.linkerOpts) + } + } + + @Test + fun `configure should process valid Apple targets`() { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply { + apply("org.jetbrains.kotlin.multiplatform") + apply("io.sentry.kotlin.multiplatform.gradle") + } + + val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension + kmpExtension.apply { + listOf( + iosArm64(), + ).forEach { + it.binaries.framework { + baseName = "shared" + isStatic = false + } + } + } + + every { mockPathResolver.resolvePaths(any(), any()) } returns ("dynamic/path" to "static/path") + justRun { mockFrameworkLinker.configureBinaries(any(), any(), any()) } + + linker.configure(mockLinkerExtension, kmpExtension.appleTargets().toList(), hostIsMac = true) + + verifyAll { + mockPathResolver.resolvePaths(mockLinkerExtension, setOf("ios-arm64", "ios-arm64_arm64e")) + mockFrameworkLinker.configureBinaries(any(), "dynamic/path", "static/path") + } + } +} + +private class FakeDerivedDataPathFinder: DerivedDataPathFinder { + override fun findDerivedDataPath(xcodeprojPath: String): String? { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt index 7e6007e9..3348c6ab 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt @@ -14,7 +14,6 @@ import org.junit.jupiter.api.assertThrows import java.io.ByteArrayOutputStream class DerivedDataPathTest { - private lateinit var valueSource: DerivedDataPathValueSource private lateinit var execOperations: ExecOperations private lateinit var parameters: DerivedDataPathValueSource.Parameters diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt new file mode 100644 index 00000000..f6c7914d --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt @@ -0,0 +1,50 @@ +package io.sentry.kotlin.multiplatform.gradle + +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import org.gradle.api.Project +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.io.TempDir +import java.io.File + +@ExtendWith(MockKExtension::class) +class FrameworkPathResolverTest { + @TempDir + lateinit var tempDir: File + + @MockK + lateinit var mockLinkerExtension: LinkerExtension + + lateinit var mockProject: Project + + lateinit var resolver: FrameworkPathResolver + + @BeforeEach + fun setup() { + mockProject = mockk { + every { providers } returns mockk { + every { of(any>(), any()) } returns mockk { + every { get() } returns tempDir.resolve("DerivedData").absolutePath + } + } + } + resolver = FrameworkPathResolver(mockProject) + } + + @Test + fun `resolves valid static custom path`() { + val frameworkDir = File(tempDir, "Sentry.xcframework").apply { mkdir() } + + val (dynamic, static) = resolver.resolvePaths( + mockLinkerExtension, + architectures = setOf("ios-arm64") + ) + + assertEquals(null to frameworkDir.absolutePath, dynamic to static) + } +} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/Old.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/Old.kt new file mode 100644 index 00000000..b034b686 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/Old.kt @@ -0,0 +1,140 @@ +package io.sentry.kotlin.multiplatform.gradle + +// +//import io.mockk.Runs +//import io.mockk.every +//import io.mockk.impl.annotations.MockK +//import io.mockk.junit5.MockKExtension +//import io.mockk.just +//import io.mockk.verify +//import org.gradle.api.Project +//import org.gradle.api.logging.Logger +//import org.gradle.testfixtures.ProjectBuilder +//import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +//import org.jetbrains.kotlin.gradle.plugin.mpp.Framework +//import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +//import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable +//import org.junit.jupiter.api.BeforeEach +//import org.junit.jupiter.api.Test +//import org.junit.jupiter.api.extension.ExtendWith +//import java.io.File +// +//@ExtendWith(MockKExtension::class) +//class CocoaFrameworkLinkerTest { +// @MockK +// private lateinit var mockProject: Project +// +// @MockK +// private lateinit var mockLogger: Logger +// +// @MockK +// private lateinit var mockKmpExtension: KotlinMultiplatformExtension +// +// @MockK +// private lateinit var mockTarget: KotlinNativeTarget +// +// @BeforeEach +// fun setup() { +// every { mockLogger.info(any()) } just Runs +// } +// +// @Test +// fun `configure should skip when Kotlin Multiplatform extension is missing`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// val linker = CocoaFrameworkLinker(project, mockLogger, hostIsMac = true) +// val linkerExtension = project.extensions.getByName("linker") as LinkerExtension +// linker.configure(linkerExtension) +// +// verify { +// mockLogger.info("Skipping Apple framework linking: Kotlin Multiplatform extension not found") +// } +// } +// +// @Test +// fun `configure should skip when not on macOS host`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// val linker = CocoaFrameworkLinker(project, mockLogger, hostIsMac = false) +// val linkerExtension = project.extensions.getByName("linker") as LinkerExtension +// linker.configure(linkerExtension) +// +// verify { +// mockLogger.info("Skipping Apple framework linking: Requires macOS host") +// } +// } +// +// @Test +// fun `configure should process valid Apple targets with custom framework path`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// val kmpExtension = project.extensions.findByName("kotlin") as KotlinMultiplatformExtension +// kmpExtension.apply { +// iosArm64 { +// binaries { +// framework("testFramework") { +// isStatic = true +// } +// test("testExecutable") +// } +// } +// } +// +// val linker = CocoaFrameworkLinker(project, mockLogger, hostIsMac = true) +// val linkerExtension = project.extensions.getByName("linker") as LinkerExtension +// +// val frameworkDir = File(File(System.getProperty("java.io.tmpdir")), "Sentry.xcframework").apply { +// mkdirs() +// deleteOnExit() +// File(this, "ios-arm64").mkdirs() +// } +// linkerExtension.frameworkPath.set(frameworkDir.absolutePath) +// +// linker.configure(linkerExtension) +// +// val target = kmpExtension.targets.getByName("iosArm64") as KotlinNativeTarget +// +// target.binaries.forEach { binary -> +// when (binary) { +// is Framework -> assert(binary.linkerOpts.any { it == "-F${frameworkDir.absolutePath}" }) +// is TestExecutable -> { +// assert(binary.linkerOpts.any { it == "-rpath" }) +// assert(binary.linkerOpts.any { it == frameworkDir.absolutePath }) +// assert(binary.linkerOpts.any { it == "-F${frameworkDir.absolutePath}" }) +// } +// else -> {} +// } +// } +// } +//// +//// @Test +//// fun `configure should wrap exceptions with GradleException`() { +//// every { mockProject.extensions.findByName("kotlin") } returns mockKmpExtension +//// every { mockKmpExtension.appleTargets() } returns mockk { +//// every { all(any()) } throws RuntimeException("Target processing failed") +//// } +//// +//// val exception = assertThrows { +//// linker.configure(LinkerExtension()) +//// } +//// +//// assert(exception.message!!.contains("Failed to configure")) +//// } +//// +//// @Test +//// fun `processTarget should skip unsupported architectures`() { +//// every { mockTarget.toSentryFrameworkArchitecture() } returns emptySet() +//// every { mockTarget.name } returns "unsupportedTarget" +//// +//// linker.processTarget(mockTarget, LinkerExtension()) +//// +//// verify { +//// mockLogger.warn("Skipping target unsupportedTarget: Unsupported architecture") +//// } +//// } +//} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt index 462c5e0a..d36c0bc0 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt @@ -2,11 +2,9 @@ package io.sentry.kotlin.multiplatform.gradle import io.mockk.every import io.mockk.mockk -import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.konan.target.KonanTarget import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -188,9 +186,3 @@ class SentryFrameworkArchitectureTest { return tempDir } } - -private class FakeTarget(project: Project, konanTarget: KonanTarget) : KotlinNativeTarget(project, konanTarget) { - override fun getName(): String { - return "fake" - } -} \ No newline at end of file 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 cf9d4ee4..804f4c19 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 @@ -221,7 +221,7 @@ class SentryPluginTest { val linkerExtension = project.extensions.getByName("linker") as LinkerExtension linkerExtension.frameworkPath.set(file.absolutePath) - CocoaFrameworkLinker(project, project.logger).configure(linkerExtension) +// CocoaFrameworkLinker(project, project.logger, hostIsMac = true).configure(linkerExtension) kmpExtension.apply { val frameworks = appleTargets().map { From 40dbe8a3d47915bbea3e0c208610a9c96181410d Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 4 Feb 2025 17:08:10 +0100 Subject: [PATCH 11/34] better logging --- .../gradle/CocoaFrameworkLinker.kt | 10 --- .../gradle/FrameworkPathResolver.kt | 89 +++++++++++++++---- 2 files changed, 72 insertions(+), 27 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index 2ef60b39..5c3bd0a1 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -1,7 +1,6 @@ package io.sentry.kotlin.multiplatform.gradle import org.gradle.api.GradleException -import org.gradle.api.Project import org.gradle.api.logging.Logger import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer @@ -177,15 +176,6 @@ internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set = b } } -internal fun Project.findDerivedDataPath(customXcodeprojPath: String? = null): String? { - val xcodeprojPath = customXcodeprojPath ?: findXcodeprojFile(rootDir)?.absolutePath - ?: throw GradleException("Xcode project file not found") - - return providers.of(DerivedDataPathValueSource::class.java) { - it.parameters.xcodeprojPath.set(xcodeprojPath) - }.get() -} - /** * Searches for a xcodeproj starting from the root directory. This function will only work for * monorepos and if it is not, the user needs to provide the custom path through the diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index d1b9a463..b9b6cb87 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -67,6 +67,12 @@ class CustomPathStrategy( // We trust that the user knows the distinction if they purposefully override the framework path override fun resolvePaths(architectures: Set): FrameworkPaths { return linker.frameworkPath.orNull?.takeIf { it.isNotEmpty() }?.let { basePath -> + project.logger.lifecycle( + "Resolving Sentry Cocoa framework paths using custom path: {} and looking for Sentry architectures: {} within the custom path", + basePath, + architectures + ) + when { basePath.endsWith("Sentry.xcframework") -> FrameworkPaths.createValidated( staticBasePath = basePath, @@ -78,7 +84,7 @@ class CustomPathStrategy( architectures = architectures ) - else -> null + else -> FrameworkPaths.NONE } } ?: FrameworkPaths.NONE } @@ -90,12 +96,19 @@ class DerivedDataStrategy( private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) override fun resolvePaths(architectures: Set): FrameworkPaths { - val xcodeprojPath = linker.xcodeprojPath.orNull - val derivedDataPath = xcodeprojPath?.let { path -> + project.logger.lifecycle("Resolving Sentry Cocoa framework paths using derived data path") + + // First priority: User-specified xcodeproj path + val xcodeprojSetByUser = linker.xcodeprojPath.orNull?.takeIf { it.isNotEmpty() } + // Second priority: Auto-discovered xcodeproj in project root + val foundXcodeproj = xcodeprojSetByUser ?: findXcodeprojFile2(project.rootDir)?.absolutePath + + val derivedDataPath = foundXcodeproj?.let { path -> project.providers.of(DerivedDataPathValueSource::class.java) { it.parameters.xcodeprojPath.set(path) - }.get() - } ?: return FrameworkPaths.NONE + }.orNull + } ?: FrameworkPaths.NONE + val dynamicBasePath = "${derivedDataPath}/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework" val staticBasePath = @@ -107,6 +120,30 @@ class DerivedDataStrategy( architectures = architectures ) } + + /** + * Searches for a xcodeproj starting from the root directory. This function will only work for + * monorepos and if it is not, the user needs to provide the custom path through the + * [LinkerExtension] configuration. + */ + internal fun findXcodeprojFile2(dir: File): File? { + val ignoredDirectories = listOf("build", "DerivedData") + + fun searchDirectory(directory: File): File? { + val files = directory.listFiles() ?: return null + + return files.firstNotNullOfOrNull { file -> + when { + file.name in ignoredDirectories -> null + file.extension == "xcodeproj" -> file + file.isDirectory -> searchDirectory(file) + else -> null + } + } + } + + return searchDirectory(dir) + } } class ManualSearchStrategy( @@ -139,18 +176,34 @@ class FrameworkPathResolver( fun resolvePaths( architectures: Set ): FrameworkPaths { - return strategies.firstNotNullOfOrNull { strategy -> - try { - strategy.resolvePaths(architectures) - } catch (e: Exception) { - project.logger.debug( - "Path resolution strategy ${strategy::class.simpleName} failed", - e - ) - null + return strategies.asSequence() + .mapNotNull { strategy -> + try { + val result = strategy.resolvePaths(architectures) + when { + result == FrameworkPaths.NONE -> { + project.logger.debug("Strategy ${strategy::class.simpleName} returned no paths") + null + } + + else -> { + project.logger.lifecycle("✅ Found Sentry Cocoa framework paths using ${strategy::class.simpleName}") + result + } + } + } catch (e: Exception) { + project.logger.debug( + "Strategy ${strategy::class.simpleName} failed: ${e.message}" + ) + null + } } - } - ?: throw FrameworkLinkingException("All framework resolution strategies failed. Could not find Sentry Cocoa framework path") + .firstOrNull() + ?: throw FrameworkLinkingException( + "No valid framework paths found. Attempted strategies: ${ + strategies.joinToString { it::class.simpleName!! } + }" + ) } companion object { @@ -164,7 +217,9 @@ class FrameworkPathResolver( return listOf( CustomPathStrategy(project), DerivedDataStrategy(project), - ManualSearchStrategy(project) + ManualSearchStrategy(project), + // TODO: add DownloadStrategy -> downloads the framework and stores it in build dir + // this is especially useful for users who dont have a monorepo setup ) } } From 2f5ea500b72c3881af30bdb0ce75c2a27561f9f8 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 4 Feb 2025 17:19:56 +0100 Subject: [PATCH 12/34] add docs per strategy --- .../gradle/FrameworkPathResolver.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index b9b6cb87..3e1cf425 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -56,7 +56,7 @@ sealed interface FrameworkResolutionStrategy { } /** - * Handles the custom framework paths set by the user. This should generally be executed first. + * Finds the framework path based on the custom framework paths set by the user. This should generally be executed first. */ class CustomPathStrategy( private val project: Project, @@ -90,6 +90,13 @@ class CustomPathStrategy( } } +/** + * Finds framework paths based on the derived data path. + * + * This strategy prioritizes: + * 1. A user-specified Xcode project path via [LinkerExtension]. + * 2. An auto-found Xcode project in the root directory. (mainly works for mono repo) + */ class DerivedDataStrategy( private val project: Project, ) : FrameworkResolutionStrategy { @@ -98,9 +105,7 @@ class DerivedDataStrategy( override fun resolvePaths(architectures: Set): FrameworkPaths { project.logger.lifecycle("Resolving Sentry Cocoa framework paths using derived data path") - // First priority: User-specified xcodeproj path val xcodeprojSetByUser = linker.xcodeprojPath.orNull?.takeIf { it.isNotEmpty() } - // Second priority: Auto-discovered xcodeproj in project root val foundXcodeproj = xcodeprojSetByUser ?: findXcodeprojFile2(project.rootDir)?.absolutePath val derivedDataPath = foundXcodeproj?.let { path -> @@ -146,6 +151,16 @@ class DerivedDataStrategy( } } +/** + * Performs a manual search for Sentry Cocoa frameworks using system tools. + * + * This strategy: + * - Searches the DerivedData for valid framework paths + * - Returns first validated paths found for either static or dynamic frameworks + * + * If multiple paths were found for a single framework type, the most recently used is chosen. + * See [ManualFrameworkPathSearchValueSource] for details. + */ class ManualSearchStrategy( private val project: Project, ) : FrameworkResolutionStrategy { @@ -223,5 +238,4 @@ class FrameworkPathResolver( ) } } - } \ No newline at end of file From 38db872354349edb21fbe66eefc065761702c4f2 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 4 Feb 2025 17:20:50 +0100 Subject: [PATCH 13/34] docs --- .../sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index 3e1cf425..9b44199d 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -223,7 +223,7 @@ class FrameworkPathResolver( companion object { /** - * Resolution strategies for finding the framework path + * Default resolution strategies for finding the Sentry Cocoa framework path. * * The order of resolution strategies matters, as the framework path will be resolved by the first successful strategy * Specifically here Custom Path will be checked first, if that fails then it is followed by the Derived Data strategy etc... From 3be1bcc773145c532e7b5913c99fa89d63efdb6a Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 5 Feb 2025 12:44:47 +0100 Subject: [PATCH 14/34] add FrameworkPathResolver test --- .../gradle/CocoaFrameworkLinker.kt | 57 +---------- .../gradle/FrameworkPathResolver.kt | 78 ++++++++------- .../gradle/FrameworkPathResolverTest.kt | 96 +++++++++++++------ 3 files changed, 108 insertions(+), 123 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index 5c3bd0a1..417e2e85 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -7,7 +7,6 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable -import java.io.File /** * Configures Sentry Cocoa framework linking for Apple targets in Kotlin Multiplatform projects. @@ -31,7 +30,7 @@ class CocoaFrameworkLinker( appleTargets.forEach { target -> try { logger.lifecycle( - "Start resolving cocoa framework paths for target: ${target.name}" + "Start resolving Sentry Cocoa framework paths for target: ${target.name}" ) processTarget(target) } catch (e: Exception) { @@ -46,7 +45,6 @@ class CocoaFrameworkLinker( logger.warn("Skipping target ${target.name}: Unsupported architecture") return } - val (dynamicPath, staticPath) = pathResolver.resolvePaths(architectures) binaryLinker.configureBinaries(target.binaries, dynamicPath, staticPath) } @@ -60,8 +58,6 @@ class FrameworkLinker( dynamicPath: String?, staticPath: String? ) { - validatePaths(dynamicPath, staticPath) - binaries.all { binary -> when (binary) { is TestExecutable -> linkTestBinary(binary, chooseTestPath(dynamicPath, staticPath)) @@ -82,12 +78,6 @@ class FrameworkLinker( else -> throw FrameworkLinkingException("No valid framework path found for tests") } - private fun validatePaths(dynamic: String?, static: String?) { - if (dynamic == null && static == null) { - throw FrameworkLinkingException(frameworkNotFoundMessage) - } - } - private fun linkTestBinary(binary: TestExecutable, path: String) { // Linking in test binaries works with both dynamic and static framework binary.linkerOpts("-rpath", path, "-F$path") @@ -107,22 +97,6 @@ class FrameworkLinker( binary.linkerOpts("-F$path") logger.info("Linked $type Sentry Cocoa framework to ${binary.name}") } - - private val frameworkNotFoundMessage = """ - Failed to find Sentry Cocoa framework. Steps to resolve: - - 1. Install Sentry Cocoa via SPM in Xcode - 2. Verify framework exists in Xcode's DerivedData folder: - - Static: Sentry.xcframework - - Dynamic: Sentry-Dynamic.xcframework - - If problem persists consider setting explicit path in build.gradle.kts: - sentryKmp { - linker { - frameworkPath.set("path/to/framework") - } - } - """.trimIndent() } internal class FrameworkLinkingException( @@ -176,36 +150,7 @@ internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set = b } } -/** - * Searches for a xcodeproj starting from the root directory. This function will only work for - * monorepos and if it is not, the user needs to provide the custom path through the - * [LinkerExtension] configuration. - */ -internal fun findXcodeprojFile(dir: File): File? { - val ignoredDirectories = listOf("build", "DerivedData") - - fun searchDirectory(directory: File): File? { - val files = directory.listFiles() ?: return null - - return files.firstNotNullOfOrNull { file -> - when { - file.name in ignoredDirectories -> null - file.extension == "xcodeproj" -> file - file.isDirectory -> searchDirectory(file) - else -> null - } - } - } - - return searchDirectory(dir) -} - internal fun KotlinMultiplatformExtension.appleTargets() = targets.withType(KotlinNativeTarget::class.java).matching { it.konanTarget.family.isAppleFamily } - -enum class FrameworkType { - STATIC, - DYNAMIC -} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index 9b44199d..35dede59 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -3,6 +3,11 @@ package io.sentry.kotlin.multiplatform.gradle import org.gradle.api.Project import java.io.File +enum class FrameworkType { + STATIC, + DYNAMIC +} + data class FrameworkPaths( val dynamic: String? = null, val static: String? = null @@ -49,7 +54,7 @@ data class FrameworkPaths( } } -sealed interface FrameworkResolutionStrategy { +interface FrameworkResolutionStrategy { fun resolvePaths( architectures: Set, ): FrameworkPaths @@ -67,12 +72,6 @@ class CustomPathStrategy( // We trust that the user knows the distinction if they purposefully override the framework path override fun resolvePaths(architectures: Set): FrameworkPaths { return linker.frameworkPath.orNull?.takeIf { it.isNotEmpty() }?.let { basePath -> - project.logger.lifecycle( - "Resolving Sentry Cocoa framework paths using custom path: {} and looking for Sentry architectures: {} within the custom path", - basePath, - architectures - ) - when { basePath.endsWith("Sentry.xcframework") -> FrameworkPaths.createValidated( staticBasePath = basePath, @@ -103,10 +102,8 @@ class DerivedDataStrategy( private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) override fun resolvePaths(architectures: Set): FrameworkPaths { - project.logger.lifecycle("Resolving Sentry Cocoa framework paths using derived data path") - val xcodeprojSetByUser = linker.xcodeprojPath.orNull?.takeIf { it.isNotEmpty() } - val foundXcodeproj = xcodeprojSetByUser ?: findXcodeprojFile2(project.rootDir)?.absolutePath + val foundXcodeproj = xcodeprojSetByUser ?: findXcodeprojFile(project.rootDir)?.absolutePath val derivedDataPath = foundXcodeproj?.let { path -> project.providers.of(DerivedDataPathValueSource::class.java) { @@ -131,7 +128,7 @@ class DerivedDataStrategy( * monorepos and if it is not, the user needs to provide the custom path through the * [LinkerExtension] configuration. */ - internal fun findXcodeprojFile2(dir: File): File? { + private fun findXcodeprojFile(dir: File): File? { val ignoredDirectories = listOf("build", "DerivedData") fun searchDirectory(directory: File): File? { @@ -191,36 +188,43 @@ class FrameworkPathResolver( fun resolvePaths( architectures: Set ): FrameworkPaths { - return strategies.asSequence() - .mapNotNull { strategy -> - try { - val result = strategy.resolvePaths(architectures) - when { - result == FrameworkPaths.NONE -> { - project.logger.debug("Strategy ${strategy::class.simpleName} returned no paths") - null - } - - else -> { - project.logger.lifecycle("✅ Found Sentry Cocoa framework paths using ${strategy::class.simpleName}") - result - } - } - } catch (e: Exception) { - project.logger.debug( - "Strategy ${strategy::class.simpleName} failed: ${e.message}" - ) - null + strategies.forEach { strategy -> + try { + project.logger.lifecycle("Attempt to resolve Sentry Cocoa framework paths using ${strategy::class.simpleName}") + val result = strategy.resolvePaths(architectures) + if (result != FrameworkPaths.NONE) { + project.logger.lifecycle("Found Sentry Cocoa framework paths using ${strategy::class.simpleName}") + return result + } else { + project.logger.debug("Strategy ${strategy::class.simpleName} did not find valid paths") } + } catch (e: Exception) { + project.logger.warn( + "Strategy ${strategy::class.simpleName} failed due to error: ${e.message}" + ) } - .firstOrNull() - ?: throw FrameworkLinkingException( - "No valid framework paths found. Attempted strategies: ${ - strategies.joinToString { it::class.simpleName!! } - }" - ) + } + + // If at this point we didn't find a path to the framework, we cannot proceed + throw FrameworkLinkingException(frameworkNotFoundMessage) } + private val frameworkNotFoundMessage = """ + Failed to find Sentry Cocoa framework. Steps to resolve: + + 1. Install Sentry Cocoa via SPM in Xcode + 2. Verify framework exists in Xcode's DerivedData folder: + - If static: Sentry.xcframework + - If dynamic: Sentry-Dynamic.xcframework + + If problem persists consider setting explicit path in build.gradle.kts: + sentryKmp { + linker { + frameworkPath.set("path/to/Sentry.xcframework") + } + } + """.trimIndent() + companion object { /** * Default resolution strategies for finding the Sentry Cocoa framework path. diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt index f6c7914d..7b4e89a7 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt @@ -1,50 +1,86 @@ package io.sentry.kotlin.multiplatform.gradle import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.junit5.MockKExtension import io.mockk.mockk -import org.gradle.api.Project -import org.junit.jupiter.api.Assertions.assertEquals +import io.mockk.verify +import io.mockk.verifyOrder +import org.gradle.internal.impldep.org.junit.Assert.assertEquals +import org.gradle.testfixtures.ProjectBuilder import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.io.TempDir -import java.io.File +import org.junit.jupiter.api.assertThrows -@ExtendWith(MockKExtension::class) class FrameworkPathResolverTest { - @TempDir - lateinit var tempDir: File + private val mockStrategy1 = mockk() + private val mockStrategy2 = mockk() + private val mockStrategy3 = mockk() + private lateinit var fixture: Fixture - @MockK - lateinit var mockLinkerExtension: LinkerExtension + @BeforeEach + fun setUp() { + fixture = Fixture() + } - lateinit var mockProject: Project + @Test + fun `does not execute subsequent strategies after first success`() { + val strategy1 = mockk { + every { resolvePaths(any()) } returns FrameworkPaths(dynamic = "dyn") + } + val strategy2 = mockk() - lateinit var resolver: FrameworkPathResolver + val sut = fixture.getSut(listOf(strategy1, strategy2)) + sut.resolvePaths(setOf("arch")) - @BeforeEach - fun setup() { - mockProject = mockk { - every { providers } returns mockk { - every { of(any>(), any()) } returns mockk { - every { get() } returns tempDir.resolve("DerivedData").absolutePath - } - } + verify(exactly = 0) { strategy2.resolvePaths(any()) } + } + + @Test + fun `proceeds past multiple failing strategies and returns first success`() { + every { mockStrategy1.resolvePaths(any()) } throws RuntimeException() + every { mockStrategy2.resolvePaths(any()) } returns FrameworkPaths.NONE + every { mockStrategy3.resolvePaths(any()) } returns FrameworkPaths(static = "valid") + + val sut = fixture.getSut(listOf(mockStrategy1, mockStrategy2, mockStrategy3)) + val result = sut.resolvePaths(setOf("arch")) + + assertEquals("valid", result.static) + } + + @Test + fun `throws if no framework paths are resolved by strategies`() { + every { mockStrategy1.resolvePaths(any()) } returns FrameworkPaths.NONE + every { mockStrategy2.resolvePaths(any()) } returns FrameworkPaths.NONE + + val sut = fixture.getSut(listOf(mockStrategy1, mockStrategy2)) + assertThrows { + sut.resolvePaths(setOf("arch")) + } + + verifyOrder { + mockStrategy1.resolvePaths(any()) + mockStrategy2.resolvePaths(any()) } - resolver = FrameworkPathResolver(mockProject) } @Test - fun `resolves valid static custom path`() { - val frameworkDir = File(tempDir, "Sentry.xcframework").apply { mkdir() } + fun `throws when no strategies provided`() { + val sut = fixture.getSut(emptyList()) - val (dynamic, static) = resolver.resolvePaths( - mockLinkerExtension, - architectures = setOf("ios-arm64") - ) + assertThrows { + sut.resolvePaths(setOf("arch")) + } + } +} + +private class Fixture { + fun getSut(strategies: List): FrameworkPathResolver { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply { + apply("org.jetbrains.kotlin.multiplatform") + apply("io.sentry.kotlin.multiplatform.gradle") + } - assertEquals(null to frameworkDir.absolutePath, dynamic to static) + return FrameworkPathResolver(project, strategies) } } \ No newline at end of file From c58f714bd35a0fd994a0cc563c889ca56814bf6e Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 5 Feb 2025 14:40:49 +0100 Subject: [PATCH 15/34] update tests --- .../gradle/CocoaFrameworkLinker.kt | 65 +- .../gradle/FrameworkPathResolver.kt | 46 +- .../ManualFrameworkPathSearchValueSource.kt | 10 +- .../gradle/CocoaFrameworkLinkerTest.kt | 229 ++++--- .../gradle/CustomPathStrategyTest.kt | 104 ++++ .../gradle/FrameworkPathResolverTest.kt | 28 +- .../multiplatform/gradle/SentryPluginTest.kt | 576 +++++++++--------- 7 files changed, 572 insertions(+), 486 deletions(-) create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index 417e2e85..c2e88995 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -7,6 +7,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable +import java.awt.Frame /** * Configures Sentry Cocoa framework linking for Apple targets in Kotlin Multiplatform projects. @@ -34,7 +35,7 @@ class CocoaFrameworkLinker( ) processTarget(target) } catch (e: Exception) { - throw GradleException("Failed to configure ${target.name}: ${e.message}", e) + throw FrameworkLinkingException("Failed to configure ${target.name}: ${e.message}", e) } } } @@ -45,8 +46,10 @@ class CocoaFrameworkLinker( logger.warn("Skipping target ${target.name}: Unsupported architecture") return } - val (dynamicPath, staticPath) = pathResolver.resolvePaths(architectures) - binaryLinker.configureBinaries(target.binaries, dynamicPath, staticPath) + val paths: FrameworkPaths = architectures.firstNotNullOf { arch -> + pathResolver.resolvePaths(arch).takeIf { it != FrameworkPaths.NONE } + } + binaryLinker.configureBinaries(target.binaries, paths.dynamic, paths.static) } } @@ -110,46 +113,36 @@ internal class FrameworkLinkingException( * * Returns a set of possible architecture names because Sentry Cocoa SDK has changed folder naming * across different versions. For example: - * - iosArm64 -> ["ios-arm64", "ios-arm64_arm64e"] - * - macosArm64 -> ["macos-arm64_x86_64", "macos-arm64_arm64e_x86_64"] - * * + * - iosArm64 -> [SentryCocoaFrameworkArchitectures.IOS_ARM64] + * - macosArm64 -> [SentryCocoaFrameworkArchitectures.MACOS_ARM64_AND_X64] + * * @return Set of possible architecture folder names for the given target. Returns empty set if target is not supported. */ internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set = buildSet { when (name) { - "iosSimulatorArm64", "iosX64" -> add("ios-arm64_x86_64-simulator") - "iosArm64" -> { - add("ios-arm64") - add("ios-arm64_arm64e") - } - - "macosArm64", "macosX64" -> { - add("macos-arm64_x86_64") - add("macos-arm64_arm64e_x86_64") - } - - "tvosSimulatorArm64", "tvosX64" -> { - add("tvos-arm64_x86_64-simulator") - add("tvos-arm64_x86_64-simulator") - } - - "tvosArm64" -> { - add("tvos-arm64") - add("tvos-arm64_arm64e") - } - - "watchosArm32", "watchosArm64" -> { - add("watchos-arm64_arm64_32_armv7k") - add("watchos-arm64_arm64_32_arm64e_armv7k") - } - - "watchosSimulatorArm64", "watchosX64" -> { - add("watchos-arm64_i386_x86_64-simulator") - add("watchos-arm64_i386_x86_64-simulator") - } + "iosSimulatorArm64", "iosX64" -> addAll(SentryCocoaFrameworkArchitectures.IOS_SIMULATOR_AND_X64) + "iosArm64" -> addAll(SentryCocoaFrameworkArchitectures.IOS_ARM64) + "macosArm64", "macosX64" -> addAll(SentryCocoaFrameworkArchitectures.MACOS_ARM64_AND_X64) + "tvosSimulatorArm64", "tvosX64" -> addAll(SentryCocoaFrameworkArchitectures.TVOS_SIMULATOR_AND_X64) + "tvosArm64" -> addAll(SentryCocoaFrameworkArchitectures.TVOS_ARM64) + "watchosArm32", "watchosArm64" -> addAll(SentryCocoaFrameworkArchitectures.WATCHOS_ARM) + "watchosSimulatorArm64", "watchosX64" -> addAll(SentryCocoaFrameworkArchitectures.WATCHOS_SIMULATOR_AND_X64) } } +internal object SentryCocoaFrameworkArchitectures { + val IOS_SIMULATOR_AND_X64 = setOf("ios-arm64_x86_64-simulator") + val IOS_ARM64 = setOf("ios-arm64", "ios-arm64_arm64e") + val MACOS_ARM64_AND_X64 = setOf("macos-arm64_x86_64", "macos-arm64_arm64e_x86_64") + val TVOS_SIMULATOR_AND_X64 = setOf("tvos-arm64_x86_64-simulator") + val TVOS_ARM64 = setOf("tvos-arm64", "tvos-arm64_arm64e") + val WATCHOS_ARM = setOf("watchos-arm64_arm64_32_armv7k", "watchos-arm64_arm64_32_arm64e_armv7k") + val WATCHOS_SIMULATOR_AND_X64 = setOf("watchos-arm64_i386_x86_64-simulator") + + // Used for tests + val all = setOf(IOS_SIMULATOR_AND_X64, IOS_ARM64, MACOS_ARM64_AND_X64, TVOS_SIMULATOR_AND_X64, TVOS_ARM64, WATCHOS_ARM, WATCHOS_SIMULATOR_AND_X64) +} + internal fun KotlinMultiplatformExtension.appleTargets() = targets.withType(KotlinNativeTarget::class.java).matching { it.konanTarget.family.isAppleFamily diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index 35dede59..bbf82966 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -18,23 +18,17 @@ data class FrameworkPaths( fun createValidated( dynamicBasePath: String? = null, staticBasePath: String? = null, - architectures: Set, + architecture: String, pathExists: (String) -> Boolean = { path -> File(path).exists() } ): FrameworkPaths { - // Find first valid dynamic path val dynamicPath = dynamicBasePath?.let { basePath -> - architectures.firstNotNullOfOrNull { arch -> - val path = "$basePath/$arch" - path.takeIf { pathExists(it) } - } + val path = "$basePath/$architecture" + path.takeIf { pathExists(it) } } - // Find first valid static path val staticPath = staticBasePath?.let { basePath -> - architectures.firstNotNullOfOrNull { arch -> - val path = "$basePath/$arch" - path.takeIf { pathExists(it) } - } + val path = "$basePath/$architecture" + path.takeIf { pathExists(it) } } return when { @@ -55,32 +49,30 @@ data class FrameworkPaths( } interface FrameworkResolutionStrategy { - fun resolvePaths( - architectures: Set, - ): FrameworkPaths + fun resolvePaths(architecture: String): FrameworkPaths } /** * Finds the framework path based on the custom framework paths set by the user. This should generally be executed first. */ class CustomPathStrategy( - private val project: Project, + project: Project, ) : FrameworkResolutionStrategy { private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) - // In this function we don't really distinguish between static and dynamic framework + // In this function we don't distinguish between static and dynamic frameworks // We trust that the user knows the distinction if they purposefully override the framework path - override fun resolvePaths(architectures: Set): FrameworkPaths { + override fun resolvePaths(architecture: String): FrameworkPaths { return linker.frameworkPath.orNull?.takeIf { it.isNotEmpty() }?.let { basePath -> when { basePath.endsWith("Sentry.xcframework") -> FrameworkPaths.createValidated( staticBasePath = basePath, - architectures = architectures + architecture = architecture ) basePath.endsWith("Sentry-Dynamic.xcframework") -> FrameworkPaths.createValidated( dynamicBasePath = basePath, - architectures = architectures + architecture = architecture ) else -> FrameworkPaths.NONE @@ -101,7 +93,7 @@ class DerivedDataStrategy( ) : FrameworkResolutionStrategy { private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) - override fun resolvePaths(architectures: Set): FrameworkPaths { + override fun resolvePaths(architecture: String): FrameworkPaths { val xcodeprojSetByUser = linker.xcodeprojPath.orNull?.takeIf { it.isNotEmpty() } val foundXcodeproj = xcodeprojSetByUser ?: findXcodeprojFile(project.rootDir)?.absolutePath @@ -119,7 +111,7 @@ class DerivedDataStrategy( return FrameworkPaths.createValidated( dynamicBasePath = dynamicBasePath, staticBasePath = staticBasePath, - architectures = architectures + architecture = architecture ) } @@ -161,22 +153,22 @@ class DerivedDataStrategy( class ManualSearchStrategy( private val project: Project, ) : FrameworkResolutionStrategy { - override fun resolvePaths(architectures: Set): FrameworkPaths { + override fun resolvePaths(architecture: String): FrameworkPaths { val dynamicValueSource = project.providers.of(ManualFrameworkPathSearchValueSource::class.java) { it.parameters.frameworkType.set(FrameworkType.DYNAMIC) - it.parameters.frameworkArchitectures.set(architectures) + it.parameters.frameworkArchitecture.set(architecture) } val staticValueSource = project.providers.of(ManualFrameworkPathSearchValueSource::class.java) { it.parameters.frameworkType.set(FrameworkType.STATIC) - it.parameters.frameworkArchitectures.set(architectures) + it.parameters.frameworkArchitecture.set(architecture) } return FrameworkPaths.createValidated( dynamicBasePath = dynamicValueSource.orNull, staticBasePath = staticValueSource.orNull, - architectures = architectures + architecture = architecture ) } } @@ -186,12 +178,12 @@ class FrameworkPathResolver( private val strategies: List = defaultStrategies(project), ) { fun resolvePaths( - architectures: Set + architecture: String ): FrameworkPaths { strategies.forEach { strategy -> try { project.logger.lifecycle("Attempt to resolve Sentry Cocoa framework paths using ${strategy::class.simpleName}") - val result = strategy.resolvePaths(architectures) + val result = strategy.resolvePaths(architecture) if (result != FrameworkPaths.NONE) { project.logger.lifecycle("Found Sentry Cocoa framework paths using ${strategy::class.simpleName}") return result diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt index ae72a2b5..ffccc712 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt @@ -9,7 +9,7 @@ import java.io.ByteArrayOutputStream abstract class ManualFrameworkPathSearchValueSource : ValueSource { interface Parameters : ValueSourceParameters { - val frameworkArchitectures: SetProperty + val frameworkArchitecture: Property val frameworkType: Property } @@ -17,16 +17,14 @@ abstract class ManualFrameworkPathSearchValueSource : ValueSource - findFrameworkWithFindCommand(frameworkType, architecture) - } + return findFrameworkWithFindCommand(frameworkType, frameworkArchitectures) } /** - * Returns valid framework if exists with with the find command. + * Returns valid framework if exists with the find command. * If the command finds multiple frameworks, it will return the one who has been modified or used most recently. */ private fun findFrameworkWithFindCommand( diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt index 615b151a..37f6bdc3 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt @@ -1,115 +1,114 @@ -import io.mockk.* -import io.mockk.impl.annotations.MockK -import io.mockk.junit5.MockKExtension -import io.sentry.kotlin.multiplatform.gradle.* -import org.gradle.api.logging.Logger -import org.gradle.testfixtures.ProjectBuilder -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.plugin.mpp.Framework -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(MockKExtension::class) -class CocoaFrameworkLinkerTest { - @MockK - lateinit var mockLogger: Logger - - @MockK - lateinit var mockPathResolver: FrameworkPathResolver - - @MockK - lateinit var mockFrameworkLinker: FrameworkLinker - - @MockK - lateinit var mockLinkerExtension: LinkerExtension - - private lateinit var linker: CocoaFrameworkLinker - - @BeforeEach - fun setup() { - every { mockLogger.info(any()) } just Runs - linker = CocoaFrameworkLinker(mockLogger, mockPathResolver, mockFrameworkLinker) - } - - @Test - fun `configure should skip when not on macOS host`() { - linker.configure(mockLinkerExtension, emptyList(), hostIsMac = false) - - verify { mockLogger.info("Skipping Apple framework linking: Requires macOS host") } - verify { mockPathResolver wasNot Called } - verify { mockFrameworkLinker wasNot Called } - } - - @Test - fun `integration test`() { - val project = ProjectBuilder.builder().build() - - project.pluginManager.apply { - apply("org.jetbrains.kotlin.multiplatform") - apply("io.sentry.kotlin.multiplatform.gradle") - } - - val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension - kmpExtension.apply { - listOf( - iosArm64(), - ).forEach { - it.binaries.framework { - baseName = "shared" - isStatic = false - } - } - } - - val linker2 = project.extensions.getByName("linker") as LinkerExtension - CocoaFrameworkLinker( - project.logger, - FrameworkPathResolver2(), - FrameworkLinker(project.logger) - ).configure(linker2, kmpExtension.appleTargets().toList(), hostIsMac = true) - - kmpExtension.iosArm64().binaries.filterIsInstance().forEach { - println(it.linkerOpts) - } - } - - @Test - fun `configure should process valid Apple targets`() { - val project = ProjectBuilder.builder().build() - - project.pluginManager.apply { - apply("org.jetbrains.kotlin.multiplatform") - apply("io.sentry.kotlin.multiplatform.gradle") - } - - val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension - kmpExtension.apply { - listOf( - iosArm64(), - ).forEach { - it.binaries.framework { - baseName = "shared" - isStatic = false - } - } - } - - every { mockPathResolver.resolvePaths(any(), any()) } returns ("dynamic/path" to "static/path") - justRun { mockFrameworkLinker.configureBinaries(any(), any(), any()) } - - linker.configure(mockLinkerExtension, kmpExtension.appleTargets().toList(), hostIsMac = true) - - verifyAll { - mockPathResolver.resolvePaths(mockLinkerExtension, setOf("ios-arm64", "ios-arm64_arm64e")) - mockFrameworkLinker.configureBinaries(any(), "dynamic/path", "static/path") - } - } -} - -private class FakeDerivedDataPathFinder: DerivedDataPathFinder { - override fun findDerivedDataPath(xcodeprojPath: String): String? { - TODO("Not yet implemented") - } - -} \ No newline at end of file +//import io.mockk.* +//import io.mockk.impl.annotations.MockK +//import io.mockk.junit5.MockKExtension +//import io.sentry.kotlin.multiplatform.gradle.* +//import org.gradle.api.logging.Logger +//import org.gradle.testfixtures.ProjectBuilder +//import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +//import org.jetbrains.kotlin.gradle.plugin.mpp.Framework +//import org.junit.jupiter.api.BeforeEach +//import org.junit.jupiter.api.Test +//import org.junit.jupiter.api.extension.ExtendWith +// +//@ExtendWith(MockKExtension::class) +//class CocoaFrameworkLinkerTest { +// @MockK +// lateinit var mockLogger: Logger +// +// @MockK +// lateinit var mockPathResolver: FrameworkPathResolver +// +// @MockK +// lateinit var mockFrameworkLinker: FrameworkLinker +// +// @MockK +// lateinit var mockLinkerExtension: LinkerExtension +// +// private lateinit var linker: CocoaFrameworkLinker +// +// @BeforeEach +// fun setup() { +// every { mockLogger.info(any()) } just Runs +// linker = CocoaFrameworkLinker(mockLogger, mockPathResolver, mockFrameworkLinker) +// } +// +// @Test +// fun `configure should skip when not on macOS host`() { +// linker.configure(mockLinkerExtension, emptyList(), hostIsMac = false) +// +// verify { mockLogger.info("Skipping Apple framework linking: Requires macOS host") } +// verify { mockPathResolver wasNot Called } +// verify { mockFrameworkLinker wasNot Called } +// } +// +// @Test +// fun `integration test`() { +// val project = ProjectBuilder.builder().build() +// +// project.pluginManager.apply { +// apply("org.jetbrains.kotlin.multiplatform") +// apply("io.sentry.kotlin.multiplatform.gradle") +// } +// +// val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension +// kmpExtension.apply { +// listOf( +// iosArm64(), +// ).forEach { +// it.binaries.framework { +// baseName = "shared" +// isStatic = false +// } +// } +// } +// +// val linker2 = project.extensions.getByName("linker") as LinkerExtension +// CocoaFrameworkLinker( +// project.logger, +// FrameworkPathResolver2(), +// FrameworkLinker(project.logger) +// ).configure(linker2, kmpExtension.appleTargets().toList(), hostIsMac = true) +// +// kmpExtension.iosArm64().binaries.filterIsInstance().forEach { +// println(it.linkerOpts) +// } +// } +// +// @Test +// fun `configure should process valid Apple targets`() { +// val project = ProjectBuilder.builder().build() +// +// project.pluginManager.apply { +// apply("org.jetbrains.kotlin.multiplatform") +// apply("io.sentry.kotlin.multiplatform.gradle") +// } +// +// val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension +// kmpExtension.apply { +// listOf( +// iosArm64(), +// ).forEach { +// it.binaries.framework { +// baseName = "shared" +// isStatic = false +// } +// } +// } +// +// every { mockPathResolver.resolvePaths(any(), any()) } returns ("dynamic/path" to "static/path") +// justRun { mockFrameworkLinker.configureBinaries(any(), any(), any()) } +// +// linker.configure(mockLinkerExtension, kmpExtension.appleTargets().toList(), hostIsMac = true) +// +// verifyAll { +// mockPathResolver.resolvePaths(mockLinkerExtension, setOf("ios-arm64", "ios-arm64_arm64e")) +// mockFrameworkLinker.configureBinaries(any(), "dynamic/path", "static/path") +// } +// } +//} +// +//private class Fixture { +// fun getSut(): CocoaFrameworkLinker { +// return CocoaFrameworkLinker() +// } +//} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt new file mode 100644 index 00000000..bbbdb42f --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt @@ -0,0 +1,104 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +class CustomPathStrategyTest { + private lateinit var fixture: Fixture + + @BeforeEach + fun setUp() { + fixture = Fixture() + } + + @ParameterizedTest(name = "should return static path for architecture {0}") + @MethodSource("architectureMappingProvider") + fun `should return static path when framework is Sentry xcframework`( + expectedArchitecture: String, @TempDir dir: Path + ) { + val xcframeworkPath = dir.resolve("Sentry.xcframework") + Files.createDirectory(xcframeworkPath) + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture)) + + val sut = fixture.getSut(xcframeworkPath.absolutePathString()) + val paths = sut.resolvePaths(expectedArchitecture) + + assertEquals(archDirectory.absolutePathString(), paths.static) + assertNull(paths.dynamic) + } + + @ParameterizedTest(name = "should return dynamic path for architecture {0}") + @MethodSource("architectureMappingProvider") + fun `should return dynamic path when framework is Sentry xcframework`( + expectedArchitecture: String, @TempDir dir: Path + ) { + val xcframeworkPath = dir.resolve("Sentry-Dynamic.xcframework") + Files.createDirectory(xcframeworkPath) + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture)) + + val sut = fixture.getSut(xcframeworkPath.absolutePathString()) + val paths = sut.resolvePaths(expectedArchitecture) + + assertEquals(archDirectory.absolutePathString(), paths.dynamic) + assertNull(paths.static) + } + + @Test + fun `returns NONE when frameworkPath is null`() { + val sut = fixture.getSut(null) + val result = sut.resolvePaths("doesnt matter") + + assertEquals(FrameworkPaths.NONE, result) + } + + @Test + fun `returns NONE when frameworkPath is empty`() { + val sut = fixture.getSut("") + val result = sut.resolvePaths("doesnt matter") + + assertEquals(FrameworkPaths.NONE, result) + } + + @Test + fun `should return NONE when framework has invalid name`(@TempDir dir: Path) { + val xcframeworkPath = dir.resolve("Invalid.xcframework") + val sut = fixture.getSut(xcframeworkPath.absolutePathString()) + + val paths = sut.resolvePaths("doesnt matter") + + assertEquals(FrameworkPaths.NONE, paths) + } + + companion object { + @JvmStatic + fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all.flatten() + .map { Arguments.of(it) } + .toList() + } + + private class Fixture { + fun getSut(frameworkPath: String?): CustomPathStrategy { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply { + apply("org.jetbrains.kotlin.multiplatform") + apply("io.sentry.kotlin.multiplatform.gradle") + } + + val linker = project.extensions.getByType(LinkerExtension::class.java) + linker.frameworkPath.set(frameworkPath) + + return CustomPathStrategy(project) + } + } +} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt index 7b4e89a7..0f95eef0 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt @@ -29,7 +29,7 @@ class FrameworkPathResolverTest { val strategy2 = mockk() val sut = fixture.getSut(listOf(strategy1, strategy2)) - sut.resolvePaths(setOf("arch")) + sut.resolvePaths("test") verify(exactly = 0) { strategy2.resolvePaths(any()) } } @@ -41,7 +41,7 @@ class FrameworkPathResolverTest { every { mockStrategy3.resolvePaths(any()) } returns FrameworkPaths(static = "valid") val sut = fixture.getSut(listOf(mockStrategy1, mockStrategy2, mockStrategy3)) - val result = sut.resolvePaths(setOf("arch")) + val result = sut.resolvePaths("test") assertEquals("valid", result.static) } @@ -53,7 +53,7 @@ class FrameworkPathResolverTest { val sut = fixture.getSut(listOf(mockStrategy1, mockStrategy2)) assertThrows { - sut.resolvePaths(setOf("arch")) + sut.resolvePaths("test") } verifyOrder { @@ -67,20 +67,20 @@ class FrameworkPathResolverTest { val sut = fixture.getSut(emptyList()) assertThrows { - sut.resolvePaths(setOf("arch")) + sut.resolvePaths("test") } } -} -private class Fixture { - fun getSut(strategies: List): FrameworkPathResolver { - val project = ProjectBuilder.builder().build() + private class Fixture { + fun getSut(strategies: List): FrameworkPathResolver { + val project = ProjectBuilder.builder().build() - project.pluginManager.apply { - apply("org.jetbrains.kotlin.multiplatform") - apply("io.sentry.kotlin.multiplatform.gradle") - } + project.pluginManager.apply { + apply("org.jetbrains.kotlin.multiplatform") + apply("io.sentry.kotlin.multiplatform.gradle") + } - return FrameworkPathResolver(project, strategies) + return FrameworkPathResolver(project, strategies) + } } -} \ No newline at end of file +} 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 804f4c19..b68c7e63 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,288 +1,288 @@ -package io.sentry.kotlin.multiplatform.gradle - -import io.sentry.BuildConfig -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.gradle.plugin.mpp.NativeBuildType -import org.junit.jupiter.api.Assertions.assertEquals -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.Test -import org.junit.jupiter.api.io.TempDir -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import java.io.File - -class SentryPluginTest { - @Test - fun `plugin is applied correctly to the project`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - assert(project.plugins.hasPlugin(SentryPlugin::class.java)) - } - - @Test - fun `extension sentry is created correctly`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - assertNotNull(project.extensions.getByName("sentryKmp")) - } - - @Test - fun `extension linker is created correctly`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - assertNotNull(project.extensions.getByName("linker")) - } - - @Test - fun `extension autoInstall is created correctly`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - assertNotNull(project.extensions.getByName("autoInstall")) - } - - @Test - fun `extension cocoapods is created correctly`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - assertNotNull(project.extensions.getByName("cocoapods")) - } - - @Test - fun `extension commonMain is created correctly`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - assertNotNull(project.extensions.getByName("commonMain")) - } - - @Test - fun `plugin applies extensions correctly`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - assertNotNull(project.extensions.getByName("sentryKmp")) - assertNotNull(project.extensions.getByName("linker")) - assertNotNull(project.extensions.getByName("autoInstall")) - assertNotNull(project.extensions.getByName("cocoapods")) - assertNotNull(project.extensions.getByName("commonMain")) - } - - @Test - fun `default kmp version is set in commonMain extension`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - val sourceSetAutoInstallExtension = project.extensions.getByName("commonMain") as SourceSetAutoInstallExtension - assertEquals(BuildConfig.SentryKmpVersion, sourceSetAutoInstallExtension.sentryKmpVersion.get()) - } - - @Test - fun `custom kmp version overrides default in commonMain extension`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension - autoInstallExtension.commonMain.sentryKmpVersion.set("1.2.3") - - assertEquals("1.2.3", autoInstallExtension.commonMain.sentryKmpVersion.get()) - } - - @ParameterizedTest - @ValueSource(strings = ["1.0.0", "2.3.4-SNAPSHOT", "latest.release"]) - fun `sentryKmpVersion accepts various version formats in commonMain extension`(version: String) { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension - autoInstallExtension.commonMain.sentryKmpVersion.set(version) - - assertEquals(version, autoInstallExtension.commonMain.sentryKmpVersion.get()) - } - - @Test - fun `when autoInstall is disabled, no installations are performed`() { - val project = ProjectBuilder.builder().build() - - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension - autoInstallExtension.enabled.set(false) - - project.afterEvaluate { - val commonMainConfiguration = - project.configurations.find { it.name.contains("commonMain", ignoreCase = true) } - assertNull(commonMainConfiguration) - - val cocoapodsExtension = project.extensions.getByName("cocoapods") as CocoapodsExtension - val sentryPod = cocoapodsExtension.pods.findByName("Sentry") - assertNull(sentryPod) - } - } - - @Test - fun `installSentryForKmp adds dependency to commonMain`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - - project.installSentryForKmp(project.extensions.getByName("commonMain") as SourceSetAutoInstallExtension) - - val sentryDependencies = project.configurations - .flatMap { it.dependencies } - .filter { it.group == "io.sentry" && it.name == "sentry-kotlin-multiplatform" } - .toList() - - assertTrue(sentryDependencies.isNotEmpty()) - - val sentryDependency = sentryDependencies.first() - assertEquals("io.sentry", sentryDependency.group) - assertEquals("sentry-kotlin-multiplatform", sentryDependency.name) - - val commonMainConfiguration = - project.configurations.find { it.name.contains("commonMain", ignoreCase = true) } - assertNotNull(commonMainConfiguration) - assertTrue(commonMainConfiguration!!.dependencies.contains(sentryDependency)) - } - - @Test - fun `install Sentry pod if not already exists`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") - - project.installSentryForCocoapods(project.extensions.getByName("cocoapods") as CocoapodsAutoInstallExtension) - - project.afterEvaluate { - val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) - val sentryPod = cocoapodsExtension?.pods?.findByName("Sentry") - assertTrue(sentryPod != null) - assertTrue(sentryPod!!.linkOnly) - } - } - - @Test - fun `do not install Sentry pod when cocoapods plugin when Sentry cocoapods configuration exists`() { - val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") - project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") - project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") - - val kmpExtension = project.extensions.findByName("kotlin") - (kmpExtension as ExtensionAware).extensions.configure(CocoapodsExtension::class.java) { cocoapods -> - cocoapods.pod("Sentry") { version = "custom version" } - } - - // plugin does not configure sentry pod if there is already an existing configuration - project.afterEvaluate { - val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) - val pod = cocoapodsExtension?.pods?.findByName("Sentry") - assertEquals(pod?.version, "custom version") - } - } - - @Test - fun `configureLinkingOptions sets up linker options for apple targets`(@TempDir tempDir: File) { - val project = ProjectBuilder.builder().build() - - project.pluginManager.apply { - apply("org.jetbrains.kotlin.multiplatform") - apply("io.sentry.kotlin.multiplatform.gradle") - } - - val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension - kmpExtension.apply { - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "shared" - isStatic = false - } - } - } - - val file = - tempDir.resolve("test/path") - file.mkdirs() - - val linkerExtension = project.extensions.getByName("linker") as LinkerExtension - linkerExtension.frameworkPath.set(file.absolutePath) - -// CocoaFrameworkLinker(project, project.logger, hostIsMac = true).configure(linkerExtension) - - kmpExtension.apply { - val frameworks = appleTargets().map { - it.binaries.findFramework(NativeBuildType.DEBUG) - } - frameworks.forEach { - assertTrue(it?.linkerOpts?.contains("-F${file.absolutePath}") ?: false) - } - } - } - - @Test - fun `findXcodeprojFile returns xcodeproj file when it exists`(@TempDir tempDir: File) { - val xcodeprojFile = File(tempDir, "TestProject.xcodeproj") - xcodeprojFile.mkdir() - - val result = findXcodeprojFile(tempDir) - - assertEquals(xcodeprojFile, result) - } - - @Test - fun `findXcodeprojFile returns null when no xcodeproj file exists`(@TempDir tempDir: File) { - val result = findXcodeprojFile(tempDir) - - assertNull(result) - } - - @Test - fun `findXcodeprojFile ignores build and DerivedData directories`(@TempDir tempDir: File) { - File(tempDir, "build").mkdir() - File(tempDir, "DerivedData").mkdir() - val xcodeprojFile = File(tempDir, "TestProject.xcodeproj") - xcodeprojFile.mkdir() - - val result = findXcodeprojFile(tempDir) - - assertEquals(xcodeprojFile, result) - } - - @Test - fun `findXcodeprojFile searches subdirectories`(@TempDir tempDir: File) { - val subDir = File(tempDir, "subdir") - subDir.mkdir() - val xcodeprojFile = File(subDir, "TestProject.xcodeproj") - xcodeprojFile.mkdir() - - val result = findXcodeprojFile(tempDir) - - assertEquals(xcodeprojFile, result) - } - - @Test - fun `findXcodeprojFile returns first xcodeproj file found`(@TempDir tempDir: File) { - val xcodeprojFile1 = File(tempDir, "TestProject1.xcodeproj") - xcodeprojFile1.mkdir() - val xcodeprojFile2 = File(tempDir, "TestProject2.xcodeproj") - xcodeprojFile2.mkdir() - - val result = findXcodeprojFile(tempDir) - - assertEquals(xcodeprojFile1, result) - } -} +//package io.sentry.kotlin.multiplatform.gradle +// +//import io.sentry.BuildConfig +//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.gradle.plugin.mpp.NativeBuildType +//import org.junit.jupiter.api.Assertions.assertEquals +//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.Test +//import org.junit.jupiter.api.io.TempDir +//import org.junit.jupiter.params.ParameterizedTest +//import org.junit.jupiter.params.provider.ValueSource +//import java.io.File +// +//class SentryPluginTest { +// @Test +// fun `plugin is applied correctly to the project`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// assert(project.plugins.hasPlugin(SentryPlugin::class.java)) +// } +// +// @Test +// fun `extension sentry is created correctly`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// assertNotNull(project.extensions.getByName("sentryKmp")) +// } +// +// @Test +// fun `extension linker is created correctly`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// assertNotNull(project.extensions.getByName("linker")) +// } +// +// @Test +// fun `extension autoInstall is created correctly`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// assertNotNull(project.extensions.getByName("autoInstall")) +// } +// +// @Test +// fun `extension cocoapods is created correctly`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// assertNotNull(project.extensions.getByName("cocoapods")) +// } +// +// @Test +// fun `extension commonMain is created correctly`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// assertNotNull(project.extensions.getByName("commonMain")) +// } +// +// @Test +// fun `plugin applies extensions correctly`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// assertNotNull(project.extensions.getByName("sentryKmp")) +// assertNotNull(project.extensions.getByName("linker")) +// assertNotNull(project.extensions.getByName("autoInstall")) +// assertNotNull(project.extensions.getByName("cocoapods")) +// assertNotNull(project.extensions.getByName("commonMain")) +// } +// +// @Test +// fun `default kmp version is set in commonMain extension`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// val sourceSetAutoInstallExtension = project.extensions.getByName("commonMain") as SourceSetAutoInstallExtension +// assertEquals(BuildConfig.SentryKmpVersion, sourceSetAutoInstallExtension.sentryKmpVersion.get()) +// } +// +// @Test +// fun `custom kmp version overrides default in commonMain extension`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension +// autoInstallExtension.commonMain.sentryKmpVersion.set("1.2.3") +// +// assertEquals("1.2.3", autoInstallExtension.commonMain.sentryKmpVersion.get()) +// } +// +// @ParameterizedTest +// @ValueSource(strings = ["1.0.0", "2.3.4-SNAPSHOT", "latest.release"]) +// fun `sentryKmpVersion accepts various version formats in commonMain extension`(version: String) { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension +// autoInstallExtension.commonMain.sentryKmpVersion.set(version) +// +// assertEquals(version, autoInstallExtension.commonMain.sentryKmpVersion.get()) +// } +// +// @Test +// fun `when autoInstall is disabled, no installations are performed`() { +// val project = ProjectBuilder.builder().build() +// +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension +// autoInstallExtension.enabled.set(false) +// +// project.afterEvaluate { +// val commonMainConfiguration = +// project.configurations.find { it.name.contains("commonMain", ignoreCase = true) } +// assertNull(commonMainConfiguration) +// +// val cocoapodsExtension = project.extensions.getByName("cocoapods") as CocoapodsExtension +// val sentryPod = cocoapodsExtension.pods.findByName("Sentry") +// assertNull(sentryPod) +// } +// } +// +// @Test +// fun `installSentryForKmp adds dependency to commonMain`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// +// project.installSentryForKmp(project.extensions.getByName("commonMain") as SourceSetAutoInstallExtension) +// +// val sentryDependencies = project.configurations +// .flatMap { it.dependencies } +// .filter { it.group == "io.sentry" && it.name == "sentry-kotlin-multiplatform" } +// .toList() +// +// assertTrue(sentryDependencies.isNotEmpty()) +// +// val sentryDependency = sentryDependencies.first() +// assertEquals("io.sentry", sentryDependency.group) +// assertEquals("sentry-kotlin-multiplatform", sentryDependency.name) +// +// val commonMainConfiguration = +// project.configurations.find { it.name.contains("commonMain", ignoreCase = true) } +// assertNotNull(commonMainConfiguration) +// assertTrue(commonMainConfiguration!!.dependencies.contains(sentryDependency)) +// } +// +// @Test +// fun `install Sentry pod if not already exists`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") +// +// project.installSentryForCocoapods(project.extensions.getByName("cocoapods") as CocoapodsAutoInstallExtension) +// +// project.afterEvaluate { +// val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) +// val sentryPod = cocoapodsExtension?.pods?.findByName("Sentry") +// assertTrue(sentryPod != null) +// assertTrue(sentryPod!!.linkOnly) +// } +// } +// +// @Test +// fun `do not install Sentry pod when cocoapods plugin when Sentry cocoapods configuration exists`() { +// val project = ProjectBuilder.builder().build() +// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") +// project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") +// project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") +// +// val kmpExtension = project.extensions.findByName("kotlin") +// (kmpExtension as ExtensionAware).extensions.configure(CocoapodsExtension::class.java) { cocoapods -> +// cocoapods.pod("Sentry") { version = "custom version" } +// } +// +// // plugin does not configure sentry pod if there is already an existing configuration +// project.afterEvaluate { +// val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) +// val pod = cocoapodsExtension?.pods?.findByName("Sentry") +// assertEquals(pod?.version, "custom version") +// } +// } +// +// @Test +// fun `configureLinkingOptions sets up linker options for apple targets`(@TempDir tempDir: File) { +// val project = ProjectBuilder.builder().build() +// +// project.pluginManager.apply { +// apply("org.jetbrains.kotlin.multiplatform") +// apply("io.sentry.kotlin.multiplatform.gradle") +// } +// +// val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension +// kmpExtension.apply { +// listOf( +// iosX64(), +// iosArm64(), +// iosSimulatorArm64() +// ).forEach { +// it.binaries.framework { +// baseName = "shared" +// isStatic = false +// } +// } +// } +// +// val file = +// tempDir.resolve("test/path") +// file.mkdirs() +// +// val linkerExtension = project.extensions.getByName("linker") as LinkerExtension +// linkerExtension.frameworkPath.set(file.absolutePath) +// +//// CocoaFrameworkLinker(project, project.logger, hostIsMac = true).configure(linkerExtension) +// +// kmpExtension.apply { +// val frameworks = appleTargets().map { +// it.binaries.findFramework(NativeBuildType.DEBUG) +// } +// frameworks.forEach { +// assertTrue(it?.linkerOpts?.contains("-F${file.absolutePath}") ?: false) +// } +// } +// } +// +// @Test +// fun `findXcodeprojFile returns xcodeproj file when it exists`(@TempDir tempDir: File) { +// val xcodeprojFile = File(tempDir, "TestProject.xcodeproj") +// xcodeprojFile.mkdir() +// +// val result = findXcodeprojFile(tempDir) +// +// assertEquals(xcodeprojFile, result) +// } +// +// @Test +// fun `findXcodeprojFile returns null when no xcodeproj file exists`(@TempDir tempDir: File) { +// val result = findXcodeprojFile(tempDir) +// +// assertNull(result) +// } +// +// @Test +// fun `findXcodeprojFile ignores build and DerivedData directories`(@TempDir tempDir: File) { +// File(tempDir, "build").mkdir() +// File(tempDir, "DerivedData").mkdir() +// val xcodeprojFile = File(tempDir, "TestProject.xcodeproj") +// xcodeprojFile.mkdir() +// +// val result = findXcodeprojFile(tempDir) +// +// assertEquals(xcodeprojFile, result) +// } +// +// @Test +// fun `findXcodeprojFile searches subdirectories`(@TempDir tempDir: File) { +// val subDir = File(tempDir, "subdir") +// subDir.mkdir() +// val xcodeprojFile = File(subDir, "TestProject.xcodeproj") +// xcodeprojFile.mkdir() +// +// val result = findXcodeprojFile(tempDir) +// +// assertEquals(xcodeprojFile, result) +// } +// +// @Test +// fun `findXcodeprojFile returns first xcodeproj file found`(@TempDir tempDir: File) { +// val xcodeprojFile1 = File(tempDir, "TestProject1.xcodeproj") +// xcodeprojFile1.mkdir() +// val xcodeprojFile2 = File(tempDir, "TestProject2.xcodeproj") +// xcodeprojFile2.mkdir() +// +// val result = findXcodeprojFile(tempDir) +// +// assertEquals(xcodeprojFile1, result) +// } +//} From a74e90a37a2ff2c8eb0f12273d8fa21a002eceb1 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 5 Feb 2025 18:22:22 +0100 Subject: [PATCH 16/34] update tests --- .../gradle/CocoaFrameworkLinker.kt | 3 +- .../gradle/FrameworkPathResolver.kt | 33 ++- .../ManualFrameworkPathSearchValueSource.kt | 18 +- .../gradle/CocoaFrameworkLinkerTest.kt | 271 ++++++++++-------- .../gradle/DerivedDataStrategyTest.kt | 116 ++++++++ .../gradle/ManualSearchStrategyTest.kt | 104 +++++++ .../sentry/kotlin/multiplatform/gradle/Old.kt | 140 --------- 7 files changed, 411 insertions(+), 274 deletions(-) create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt delete mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/Old.kt diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index c2e88995..3ccfe2a3 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -24,8 +24,7 @@ class CocoaFrameworkLinker( appleTargets: List, ) { if (!hostIsMac) { - logger.info("Skipping Apple framework linking: Requires macOS host") - return + throw FrameworkLinkingException("Sentry Cocoa framework linking requires a macOS host") } appleTargets.forEach { target -> diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index bbf82966..c0957ac2 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -90,18 +90,22 @@ class CustomPathStrategy( */ class DerivedDataStrategy( private val project: Project, + private val derivedDataProvider: (String) -> String? = { xcodeprojPath -> + project.providers.of(DerivedDataPathValueSource::class.java) { + it.parameters.xcodeprojPath.set(xcodeprojPath) + }.orNull + }, ) : FrameworkResolutionStrategy { private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) override fun resolvePaths(architecture: String): FrameworkPaths { val xcodeprojSetByUser = linker.xcodeprojPath.orNull?.takeIf { it.isNotEmpty() } val foundXcodeproj = xcodeprojSetByUser ?: findXcodeprojFile(project.rootDir)?.absolutePath + if (foundXcodeproj == null) { + return FrameworkPaths.NONE + } - val derivedDataPath = foundXcodeproj?.let { path -> - project.providers.of(DerivedDataPathValueSource::class.java) { - it.parameters.xcodeprojPath.set(path) - }.orNull - } ?: FrameworkPaths.NONE + val derivedDataPath = derivedDataProvider(foundXcodeproj) ?: return FrameworkPaths.NONE val dynamicBasePath = "${derivedDataPath}/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework" @@ -117,10 +121,9 @@ class DerivedDataStrategy( /** * Searches for a xcodeproj starting from the root directory. This function will only work for - * monorepos and if it is not, the user needs to provide the custom path through the - * [LinkerExtension] configuration. + * monorepos and if it is not, the user needs to provide the [LinkerExtension.xcodeprojPath]. */ - private fun findXcodeprojFile(dir: File): File? { + private fun findXcodeprojFile(startingDir: File): File? { val ignoredDirectories = listOf("build", "DerivedData") fun searchDirectory(directory: File): File? { @@ -136,7 +139,7 @@ class DerivedDataStrategy( } } - return searchDirectory(dir) + return searchDirectory(startingDir) } } @@ -152,19 +155,27 @@ class DerivedDataStrategy( */ class ManualSearchStrategy( private val project: Project, + private val basePathToSearch: String? = null ) : FrameworkResolutionStrategy { override fun resolvePaths(architecture: String): FrameworkPaths { val dynamicValueSource = project.providers.of(ManualFrameworkPathSearchValueSource::class.java) { it.parameters.frameworkType.set(FrameworkType.DYNAMIC) - it.parameters.frameworkArchitecture.set(architecture) + if (basePathToSearch != null) { + it.parameters.basePathToSearch.set(basePathToSearch) + } } val staticValueSource = project.providers.of(ManualFrameworkPathSearchValueSource::class.java) { it.parameters.frameworkType.set(FrameworkType.STATIC) - it.parameters.frameworkArchitecture.set(architecture) + if (basePathToSearch != null) { + it.parameters.basePathToSearch.set(basePathToSearch) + } } + println("static: ${staticValueSource.orNull}") + println("dynamic: ${dynamicValueSource.orNull}") + return FrameworkPaths.createValidated( dynamicBasePath = dynamicValueSource.orNull, staticBasePath = staticValueSource.orNull, diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt index ffccc712..479aa84f 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt @@ -7,20 +7,23 @@ import org.gradle.api.provider.ValueSourceParameters import org.gradle.process.ExecOperations import java.io.ByteArrayOutputStream -abstract class ManualFrameworkPathSearchValueSource : ValueSource { +abstract class ManualFrameworkPathSearchValueSource : + ValueSource { interface Parameters : ValueSourceParameters { - val frameworkArchitecture: Property val frameworkType: Property + val basePathToSearch: Property } @get:javax.inject.Inject abstract val execOperations: ExecOperations override fun obtain(): String? { - val frameworkArchitectures = parameters.frameworkArchitecture.get() val frameworkType = parameters.frameworkType.get() + val basePathToSearch = + parameters.basePathToSearch.convention("\"${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData\"") + .get() - return findFrameworkWithFindCommand(frameworkType, frameworkArchitectures) + return findFrameworkWithFindCommand(frameworkType, basePathToSearch) } /** @@ -29,7 +32,7 @@ abstract class ManualFrameworkPathSearchValueSource : ValueSource().forEach { -// println(it.linkerOpts) -// } -// } -// -// @Test -// fun `configure should process valid Apple targets`() { -// val project = ProjectBuilder.builder().build() -// -// project.pluginManager.apply { -// apply("org.jetbrains.kotlin.multiplatform") -// apply("io.sentry.kotlin.multiplatform.gradle") -// } -// -// val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension -// kmpExtension.apply { -// listOf( -// iosArm64(), -// ).forEach { -// it.binaries.framework { -// baseName = "shared" -// isStatic = false -// } -// } -// } -// -// every { mockPathResolver.resolvePaths(any(), any()) } returns ("dynamic/path" to "static/path") -// justRun { mockFrameworkLinker.configureBinaries(any(), any(), any()) } -// -// linker.configure(mockLinkerExtension, kmpExtension.appleTargets().toList(), hostIsMac = true) -// -// verifyAll { -// mockPathResolver.resolvePaths(mockLinkerExtension, setOf("ios-arm64", "ios-arm64_arm64e")) -// mockFrameworkLinker.configureBinaries(any(), "dynamic/path", "static/path") -// } -// } -//} -// -//private class Fixture { -// fun getSut(): CocoaFrameworkLinker { -// return CocoaFrameworkLinker() -// } -//} \ No newline at end of file +import io.sentry.kotlin.multiplatform.gradle.* +import io.sentry.kotlin.multiplatform.gradle.FrameworkLinkingException +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class CocoaFrameworkLinkerTest { + private lateinit var fixture: Fixture + + @BeforeEach + fun setUp() { + fixture = Fixture() + } + + @Test + fun `throws if host is not macOS`() { + val sut = fixture.getSut(false) + + assertThrows { sut.configure(emptyList()) } + } + + @Test + fun `framework linking succeeds for static Framework binary`() { + val kmpExtension = fixture.project.extensions.getByType(KotlinMultiplatformExtension::class.java) + val appleTargets = listOf( + kmpExtension.iosSimulatorArm64(), + kmpExtension.iosArm64(), + kmpExtension.watchosArm32(), + kmpExtension.watchosSimulatorArm64(), + kmpExtension.watchosX64(), + kmpExtension.macosArm64(), + kmpExtension.macosX64(), + kmpExtension.tvosArm64(), + kmpExtension.tvosSimulatorArm64(), + kmpExtension.tvosX64() + ) + appleTargets.forEach { + it.binaries.framework { + baseName = "MyFramework" + isStatic = true + } + } + + val sut = fixture.getSut(true) + sut.configure(appleTargets) + + appleTargets.forEach { target -> + val binary = target.binaries.find { it.baseName == "MyFramework" }!! + assertTrue(binary.linkerOpts.size == 1) + assertEquals(binary.linkerOpts.first(), "-F$staticPath") + } + } + + @Test + fun `framework linking succeeds for dynamic Framework binary`() { + val kmpExtension = fixture.project.extensions.getByType(KotlinMultiplatformExtension::class.java) + val appleTargets = listOf( + kmpExtension.iosSimulatorArm64(), + kmpExtension.iosArm64(), + kmpExtension.watchosArm32(), + kmpExtension.watchosSimulatorArm64(), + kmpExtension.watchosX64(), + kmpExtension.macosArm64(), + kmpExtension.macosX64(), + kmpExtension.tvosArm64(), + kmpExtension.tvosSimulatorArm64(), + kmpExtension.tvosX64() + ) + appleTargets.forEach { + it.binaries.framework { + baseName = "MyFramework" + isStatic = false + } + } + + val sut = fixture.getSut(true) + sut.configure(appleTargets) + + appleTargets.forEach { target -> + val binary = target.binaries.find { it.baseName == "MyFramework" }!! + assertTrue(binary.linkerOpts.size == 1) + assertEquals(binary.linkerOpts.first(), "-F$dynamicPath") + } + } + + @Test + fun `framework linking succeeds for TestExecutable binary`() { + val kmpExtension = fixture.project.extensions.getByType(KotlinMultiplatformExtension::class.java) + val appleTargets = listOf( + kmpExtension.iosSimulatorArm64(), + kmpExtension.iosArm64(), + kmpExtension.watchosArm32(), + kmpExtension.watchosSimulatorArm64(), + kmpExtension.watchosX64(), + kmpExtension.macosArm64(), + kmpExtension.macosX64(), + kmpExtension.tvosArm64(), + kmpExtension.tvosSimulatorArm64(), + kmpExtension.tvosX64() + ) + appleTargets.forEach { + it.binaries.framework { + baseName = "MyFramework" + isStatic = true + } + } + + val sut = fixture.getSut(true) + sut.configure(appleTargets) + + // both dynamic and static frameworks can be used for linkin in test executables + appleTargets.forEach { target -> + val binary = target.binaries.find { it is TestExecutable }!! + assertTrue(binary.linkerOpts.size == 3) + assertEquals(binary.linkerOpts.first(), "-rpath") + assertTrue(binary.linkerOpts[1] == staticPath || binary.linkerOpts[1] == dynamicPath) + assertTrue(binary.linkerOpts[2] == "-F$staticPath" || binary.linkerOpts[2] == "-F$dynamicPath") + } + } + + private class Fixture { + val project: Project = ProjectBuilder.builder().build() + + init { + project.pluginManager.apply { + apply("org.jetbrains.kotlin.multiplatform") + apply("io.sentry.kotlin.multiplatform.gradle") + } + } + + fun getSut(hostIsMac: Boolean): CocoaFrameworkLinker { + return CocoaFrameworkLinker( + project.logger, + FrameworkPathResolver(project, strategies = listOf(FakeStrategy())), + FrameworkLinker(project.logger), + hostIsMac + ) + } + } +} + +// We don't really care what the strategy exactly does in this test +// The strategies themselves are tested independently +private class FakeStrategy : FrameworkResolutionStrategy { + override fun resolvePaths(architecture: String): FrameworkPaths { + return FrameworkPaths(static = staticPath, dynamic = dynamicPath) + } +} + +val staticPath = "/path/to/static/Sentry.xcframework" +val dynamicPath = "/path/to/dynamic/Sentry-Dynamic.xcframework" \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt new file mode 100644 index 00000000..351cd653 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt @@ -0,0 +1,116 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +class DerivedDataStrategyTest { + private lateinit var fixture: Fixture + + @BeforeEach + fun setUp() { + fixture = Fixture() + } + + @ParameterizedTest(name = "should return static path for architecture {0}") + @MethodSource("architectureMappingProvider") + fun `if xcodeproj is null and find xcode project successfully then resolve static path`( + expectedArchitecture: String, @TempDir dir: Path + ) { + Files.createDirectory(dir.resolve("project.xcodeproj")) + val xcframeworkPath = dir.resolve("SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework") + val xcframeworkDirectory = Files.createDirectories(xcframeworkPath) + val archDirectory = Files.createDirectory(xcframeworkDirectory.resolve(expectedArchitecture)) + + val sut = fixture.getSut(null, rootDirPath = dir.toFile().absolutePath) { _: String -> + dir.toFile().absolutePath + } + + val paths = sut.resolvePaths(expectedArchitecture) + assertEquals(archDirectory.absolutePathString(), paths.static) + assertNull(paths.dynamic) + } + + @ParameterizedTest(name = "should return dynamic path for architecture {0}") + @MethodSource("architectureMappingProvider") + fun `if xcodeproj is null and find xcode project successfully then resolve dynamic path`( + expectedArchitecture: String, @TempDir dir: Path + ) { + Files.createDirectory(dir.resolve("project.xcodeproj")) + val xcframeworkPath = dir.resolve("SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework") + val xcframeworkDirectory = Files.createDirectories(xcframeworkPath) + val archDirectory = Files.createDirectory(xcframeworkDirectory.resolve(expectedArchitecture)) + + val sut = fixture.getSut(null, rootDirPath = dir.toFile().absolutePath) { _: String -> + dir.toFile().absolutePath + } + + val paths = sut.resolvePaths(expectedArchitecture) + assertEquals(archDirectory.absolutePathString(), paths.dynamic) + assertNull(paths.static) + } + + @ParameterizedTest(name = "should return dynamic path for architecture {0}") + @MethodSource("architectureMappingProvider") + fun `if xcodeproj is null and find xcode project is not successful then return NONE`( + expectedArchitecture: String, @TempDir dir: Path + ) { + val sut = fixture.getSut(null, rootDirPath = dir.toFile().absolutePath) { _: String -> + dir.toFile().absolutePath + } + + val paths = sut.resolvePaths(expectedArchitecture) + assertEquals(FrameworkPaths.NONE, paths) + } + + @ParameterizedTest(name = "should return dynamic path for architecture {0}") + @MethodSource("architectureMappingProvider") + fun `if xcodeproj is not null and find xcode project is not successful then return NONE`( + expectedArchitecture: String, @TempDir dir: Path + ) { + val sut = fixture.getSut("some invalid path", rootDirPath = dir.toFile().absolutePath) { _: String -> + dir.toFile().absolutePath + } + + val paths = sut.resolvePaths(expectedArchitecture) + assertEquals(FrameworkPaths.NONE, paths) + } + + companion object { + @JvmStatic + fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all.flatten() + .map { Arguments.of(it) } + .toList() + } + + private class Fixture { + fun getSut( + xcodeprojPath: String?, + rootDirPath: String, + derivedDataProvider: (String) -> String? + ): DerivedDataStrategy { + val project = ProjectBuilder.builder() + .withProjectDir(File(rootDirPath)) + .build() + + project.pluginManager.apply { + apply("org.jetbrains.kotlin.multiplatform") + apply("io.sentry.kotlin.multiplatform.gradle") + } + + val linker = project.extensions.getByType(LinkerExtension::class.java) + linker.xcodeprojPath.set(xcodeprojPath) + + return DerivedDataStrategy(project, derivedDataProvider) + } + } +} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt new file mode 100644 index 00000000..2fc9c47a --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt @@ -0,0 +1,104 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.attribute.FileTime +import java.time.Instant +import kotlin.io.path.absolutePathString +import kotlin.io.path.createDirectories + +class ManualSearchStrategyTest { + private lateinit var fixture: Fixture + + @BeforeEach + fun setUp() { + fixture = Fixture() + } + + @ParameterizedTest(name = "should return static path for architecture {0}") + @MethodSource("architectureMappingProvider") + fun `should return static path when framework exists`( + expectedArchitecture: String, @TempDir dir: Path + ) { + val xcframeworkPath = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture)) + + val sut = fixture.getSut(dir.absolutePathString()) + val paths = sut.resolvePaths(expectedArchitecture) + + assertEquals(archDirectory.absolutePathString(), paths.static) + assertNull(paths.dynamic) + } + + @ParameterizedTest(name = "should return dynamic path for architecture {0}") + @MethodSource("architectureMappingProvider") + fun `should return dynamic path when framework exists`( + expectedArchitecture: String, @TempDir dir: Path + ) { + val xcframeworkPath = dir.resolve("somewhere/hidden/Sentry-Dynamic.xcframework").createDirectories() + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture)) + + val sut = fixture.getSut(dir.absolutePathString()) + val paths = sut.resolvePaths(expectedArchitecture) + + assertEquals(archDirectory.absolutePathString(), paths.dynamic) + assertNull(paths.static) + } + + @ParameterizedTest(name = "should return most recently used path for architecture {0}") + @MethodSource("architectureMappingProvider") + fun `should return most recently used path when multiple framework exists`( + expectedArchitecture: String, @TempDir dir: Path + ) { + val temp = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() + // Modifying this path so it's modified longer ago than the second path + val xcframeworkPath1 = Files.setLastModifiedTime(temp, FileTime.from(Instant.now().minusSeconds(5))) + Files.createDirectory(xcframeworkPath1.resolve(expectedArchitecture)) + + val xcframeworkPath2 = dir.resolve("more/recent/Sentry.xcframework").createDirectories() + val archDirectory2 = Files.createDirectory(xcframeworkPath2.resolve(expectedArchitecture)) + + val sut = fixture.getSut(dir.absolutePathString()) + val paths = sut.resolvePaths(expectedArchitecture) + + assertEquals(archDirectory2.absolutePathString(), paths.static) + assertNull(paths.dynamic) + } + + @Test + fun `returns NONE when no framework found`() { + val sut = fixture.getSut("some/random/path") + val result = sut.resolvePaths("doesnt matter") + + assertEquals(FrameworkPaths.NONE, result) + } + + companion object { + @JvmStatic + fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all.flatten() + .map { Arguments.of(it) } + .toList() + } + + private class Fixture { + fun getSut(basePathToSearch: String): ManualSearchStrategy { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply { + apply("org.jetbrains.kotlin.multiplatform") + apply("io.sentry.kotlin.multiplatform.gradle") + } + + return ManualSearchStrategy(project, basePathToSearch) + } + } +} \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/Old.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/Old.kt deleted file mode 100644 index b034b686..00000000 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/Old.kt +++ /dev/null @@ -1,140 +0,0 @@ -package io.sentry.kotlin.multiplatform.gradle - -// -//import io.mockk.Runs -//import io.mockk.every -//import io.mockk.impl.annotations.MockK -//import io.mockk.junit5.MockKExtension -//import io.mockk.just -//import io.mockk.verify -//import org.gradle.api.Project -//import org.gradle.api.logging.Logger -//import org.gradle.testfixtures.ProjectBuilder -//import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -//import org.jetbrains.kotlin.gradle.plugin.mpp.Framework -//import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -//import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable -//import org.junit.jupiter.api.BeforeEach -//import org.junit.jupiter.api.Test -//import org.junit.jupiter.api.extension.ExtendWith -//import java.io.File -// -//@ExtendWith(MockKExtension::class) -//class CocoaFrameworkLinkerTest { -// @MockK -// private lateinit var mockProject: Project -// -// @MockK -// private lateinit var mockLogger: Logger -// -// @MockK -// private lateinit var mockKmpExtension: KotlinMultiplatformExtension -// -// @MockK -// private lateinit var mockTarget: KotlinNativeTarget -// -// @BeforeEach -// fun setup() { -// every { mockLogger.info(any()) } just Runs -// } -// -// @Test -// fun `configure should skip when Kotlin Multiplatform extension is missing`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// val linker = CocoaFrameworkLinker(project, mockLogger, hostIsMac = true) -// val linkerExtension = project.extensions.getByName("linker") as LinkerExtension -// linker.configure(linkerExtension) -// -// verify { -// mockLogger.info("Skipping Apple framework linking: Kotlin Multiplatform extension not found") -// } -// } -// -// @Test -// fun `configure should skip when not on macOS host`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// val linker = CocoaFrameworkLinker(project, mockLogger, hostIsMac = false) -// val linkerExtension = project.extensions.getByName("linker") as LinkerExtension -// linker.configure(linkerExtension) -// -// verify { -// mockLogger.info("Skipping Apple framework linking: Requires macOS host") -// } -// } -// -// @Test -// fun `configure should process valid Apple targets with custom framework path`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// val kmpExtension = project.extensions.findByName("kotlin") as KotlinMultiplatformExtension -// kmpExtension.apply { -// iosArm64 { -// binaries { -// framework("testFramework") { -// isStatic = true -// } -// test("testExecutable") -// } -// } -// } -// -// val linker = CocoaFrameworkLinker(project, mockLogger, hostIsMac = true) -// val linkerExtension = project.extensions.getByName("linker") as LinkerExtension -// -// val frameworkDir = File(File(System.getProperty("java.io.tmpdir")), "Sentry.xcframework").apply { -// mkdirs() -// deleteOnExit() -// File(this, "ios-arm64").mkdirs() -// } -// linkerExtension.frameworkPath.set(frameworkDir.absolutePath) -// -// linker.configure(linkerExtension) -// -// val target = kmpExtension.targets.getByName("iosArm64") as KotlinNativeTarget -// -// target.binaries.forEach { binary -> -// when (binary) { -// is Framework -> assert(binary.linkerOpts.any { it == "-F${frameworkDir.absolutePath}" }) -// is TestExecutable -> { -// assert(binary.linkerOpts.any { it == "-rpath" }) -// assert(binary.linkerOpts.any { it == frameworkDir.absolutePath }) -// assert(binary.linkerOpts.any { it == "-F${frameworkDir.absolutePath}" }) -// } -// else -> {} -// } -// } -// } -//// -//// @Test -//// fun `configure should wrap exceptions with GradleException`() { -//// every { mockProject.extensions.findByName("kotlin") } returns mockKmpExtension -//// every { mockKmpExtension.appleTargets() } returns mockk { -//// every { all(any()) } throws RuntimeException("Target processing failed") -//// } -//// -//// val exception = assertThrows { -//// linker.configure(LinkerExtension()) -//// } -//// -//// assert(exception.message!!.contains("Failed to configure")) -//// } -//// -//// @Test -//// fun `processTarget should skip unsupported architectures`() { -//// every { mockTarget.toSentryFrameworkArchitecture() } returns emptySet() -//// every { mockTarget.name } returns "unsupportedTarget" -//// -//// linker.processTarget(mockTarget, LinkerExtension()) -//// -//// verify { -//// mockLogger.warn("Skipping target unsupportedTarget: Unsupported architecture") -//// } -//// } -//} \ No newline at end of file From ca5cacc12f7c71eea2ef64569b4ef463876d5d68 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 5 Feb 2025 18:25:28 +0100 Subject: [PATCH 17/34] update tests --- .../multiplatform/gradle/SentryPluginTest.kt | 477 +++++++----------- 1 file changed, 189 insertions(+), 288 deletions(-) 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 b68c7e63..472d6725 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,288 +1,189 @@ -//package io.sentry.kotlin.multiplatform.gradle -// -//import io.sentry.BuildConfig -//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.gradle.plugin.mpp.NativeBuildType -//import org.junit.jupiter.api.Assertions.assertEquals -//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.Test -//import org.junit.jupiter.api.io.TempDir -//import org.junit.jupiter.params.ParameterizedTest -//import org.junit.jupiter.params.provider.ValueSource -//import java.io.File -// -//class SentryPluginTest { -// @Test -// fun `plugin is applied correctly to the project`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// assert(project.plugins.hasPlugin(SentryPlugin::class.java)) -// } -// -// @Test -// fun `extension sentry is created correctly`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// assertNotNull(project.extensions.getByName("sentryKmp")) -// } -// -// @Test -// fun `extension linker is created correctly`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// assertNotNull(project.extensions.getByName("linker")) -// } -// -// @Test -// fun `extension autoInstall is created correctly`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// assertNotNull(project.extensions.getByName("autoInstall")) -// } -// -// @Test -// fun `extension cocoapods is created correctly`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// assertNotNull(project.extensions.getByName("cocoapods")) -// } -// -// @Test -// fun `extension commonMain is created correctly`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// assertNotNull(project.extensions.getByName("commonMain")) -// } -// -// @Test -// fun `plugin applies extensions correctly`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// assertNotNull(project.extensions.getByName("sentryKmp")) -// assertNotNull(project.extensions.getByName("linker")) -// assertNotNull(project.extensions.getByName("autoInstall")) -// assertNotNull(project.extensions.getByName("cocoapods")) -// assertNotNull(project.extensions.getByName("commonMain")) -// } -// -// @Test -// fun `default kmp version is set in commonMain extension`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// val sourceSetAutoInstallExtension = project.extensions.getByName("commonMain") as SourceSetAutoInstallExtension -// assertEquals(BuildConfig.SentryKmpVersion, sourceSetAutoInstallExtension.sentryKmpVersion.get()) -// } -// -// @Test -// fun `custom kmp version overrides default in commonMain extension`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension -// autoInstallExtension.commonMain.sentryKmpVersion.set("1.2.3") -// -// assertEquals("1.2.3", autoInstallExtension.commonMain.sentryKmpVersion.get()) -// } -// -// @ParameterizedTest -// @ValueSource(strings = ["1.0.0", "2.3.4-SNAPSHOT", "latest.release"]) -// fun `sentryKmpVersion accepts various version formats in commonMain extension`(version: String) { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension -// autoInstallExtension.commonMain.sentryKmpVersion.set(version) -// -// assertEquals(version, autoInstallExtension.commonMain.sentryKmpVersion.get()) -// } -// -// @Test -// fun `when autoInstall is disabled, no installations are performed`() { -// val project = ProjectBuilder.builder().build() -// -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension -// autoInstallExtension.enabled.set(false) -// -// project.afterEvaluate { -// val commonMainConfiguration = -// project.configurations.find { it.name.contains("commonMain", ignoreCase = true) } -// assertNull(commonMainConfiguration) -// -// val cocoapodsExtension = project.extensions.getByName("cocoapods") as CocoapodsExtension -// val sentryPod = cocoapodsExtension.pods.findByName("Sentry") -// assertNull(sentryPod) -// } -// } -// -// @Test -// fun `installSentryForKmp adds dependency to commonMain`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// -// project.installSentryForKmp(project.extensions.getByName("commonMain") as SourceSetAutoInstallExtension) -// -// val sentryDependencies = project.configurations -// .flatMap { it.dependencies } -// .filter { it.group == "io.sentry" && it.name == "sentry-kotlin-multiplatform" } -// .toList() -// -// assertTrue(sentryDependencies.isNotEmpty()) -// -// val sentryDependency = sentryDependencies.first() -// assertEquals("io.sentry", sentryDependency.group) -// assertEquals("sentry-kotlin-multiplatform", sentryDependency.name) -// -// val commonMainConfiguration = -// project.configurations.find { it.name.contains("commonMain", ignoreCase = true) } -// assertNotNull(commonMainConfiguration) -// assertTrue(commonMainConfiguration!!.dependencies.contains(sentryDependency)) -// } -// -// @Test -// fun `install Sentry pod if not already exists`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") -// -// project.installSentryForCocoapods(project.extensions.getByName("cocoapods") as CocoapodsAutoInstallExtension) -// -// project.afterEvaluate { -// val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) -// val sentryPod = cocoapodsExtension?.pods?.findByName("Sentry") -// assertTrue(sentryPod != null) -// assertTrue(sentryPod!!.linkOnly) -// } -// } -// -// @Test -// fun `do not install Sentry pod when cocoapods plugin when Sentry cocoapods configuration exists`() { -// val project = ProjectBuilder.builder().build() -// project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") -// project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") -// project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") -// -// val kmpExtension = project.extensions.findByName("kotlin") -// (kmpExtension as ExtensionAware).extensions.configure(CocoapodsExtension::class.java) { cocoapods -> -// cocoapods.pod("Sentry") { version = "custom version" } -// } -// -// // plugin does not configure sentry pod if there is already an existing configuration -// project.afterEvaluate { -// val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) -// val pod = cocoapodsExtension?.pods?.findByName("Sentry") -// assertEquals(pod?.version, "custom version") -// } -// } -// -// @Test -// fun `configureLinkingOptions sets up linker options for apple targets`(@TempDir tempDir: File) { -// val project = ProjectBuilder.builder().build() -// -// project.pluginManager.apply { -// apply("org.jetbrains.kotlin.multiplatform") -// apply("io.sentry.kotlin.multiplatform.gradle") -// } -// -// val kmpExtension = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension -// kmpExtension.apply { -// listOf( -// iosX64(), -// iosArm64(), -// iosSimulatorArm64() -// ).forEach { -// it.binaries.framework { -// baseName = "shared" -// isStatic = false -// } -// } -// } -// -// val file = -// tempDir.resolve("test/path") -// file.mkdirs() -// -// val linkerExtension = project.extensions.getByName("linker") as LinkerExtension -// linkerExtension.frameworkPath.set(file.absolutePath) -// -//// CocoaFrameworkLinker(project, project.logger, hostIsMac = true).configure(linkerExtension) -// -// kmpExtension.apply { -// val frameworks = appleTargets().map { -// it.binaries.findFramework(NativeBuildType.DEBUG) -// } -// frameworks.forEach { -// assertTrue(it?.linkerOpts?.contains("-F${file.absolutePath}") ?: false) -// } -// } -// } -// -// @Test -// fun `findXcodeprojFile returns xcodeproj file when it exists`(@TempDir tempDir: File) { -// val xcodeprojFile = File(tempDir, "TestProject.xcodeproj") -// xcodeprojFile.mkdir() -// -// val result = findXcodeprojFile(tempDir) -// -// assertEquals(xcodeprojFile, result) -// } -// -// @Test -// fun `findXcodeprojFile returns null when no xcodeproj file exists`(@TempDir tempDir: File) { -// val result = findXcodeprojFile(tempDir) -// -// assertNull(result) -// } -// -// @Test -// fun `findXcodeprojFile ignores build and DerivedData directories`(@TempDir tempDir: File) { -// File(tempDir, "build").mkdir() -// File(tempDir, "DerivedData").mkdir() -// val xcodeprojFile = File(tempDir, "TestProject.xcodeproj") -// xcodeprojFile.mkdir() -// -// val result = findXcodeprojFile(tempDir) -// -// assertEquals(xcodeprojFile, result) -// } -// -// @Test -// fun `findXcodeprojFile searches subdirectories`(@TempDir tempDir: File) { -// val subDir = File(tempDir, "subdir") -// subDir.mkdir() -// val xcodeprojFile = File(subDir, "TestProject.xcodeproj") -// xcodeprojFile.mkdir() -// -// val result = findXcodeprojFile(tempDir) -// -// assertEquals(xcodeprojFile, result) -// } -// -// @Test -// fun `findXcodeprojFile returns first xcodeproj file found`(@TempDir tempDir: File) { -// val xcodeprojFile1 = File(tempDir, "TestProject1.xcodeproj") -// xcodeprojFile1.mkdir() -// val xcodeprojFile2 = File(tempDir, "TestProject2.xcodeproj") -// xcodeprojFile2.mkdir() -// -// val result = findXcodeprojFile(tempDir) -// -// assertEquals(xcodeprojFile1, result) -// } -//} +package io.sentry.kotlin.multiplatform.gradle + +import io.sentry.BuildConfig +import org.gradle.api.plugins.ExtensionAware +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension +import org.junit.jupiter.api.Assertions.assertEquals +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.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class SentryPluginTest { + @Test + fun `plugin is applied correctly to the project`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assert(project.plugins.hasPlugin(SentryPlugin::class.java)) + } + + @Test + fun `extension sentry is created correctly`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assertNotNull(project.extensions.getByName("sentryKmp")) + } + + @Test + fun `extension linker is created correctly`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assertNotNull(project.extensions.getByName("linker")) + } + + @Test + fun `extension autoInstall is created correctly`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assertNotNull(project.extensions.getByName("autoInstall")) + } + + @Test + fun `extension cocoapods is created correctly`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assertNotNull(project.extensions.getByName("cocoapods")) + } + + @Test + fun `extension commonMain is created correctly`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assertNotNull(project.extensions.getByName("commonMain")) + } + + @Test + fun `plugin applies extensions correctly`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + assertNotNull(project.extensions.getByName("sentryKmp")) + assertNotNull(project.extensions.getByName("linker")) + assertNotNull(project.extensions.getByName("autoInstall")) + assertNotNull(project.extensions.getByName("cocoapods")) + assertNotNull(project.extensions.getByName("commonMain")) + } + + @Test + fun `default kmp version is set in commonMain extension`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val sourceSetAutoInstallExtension = project.extensions.getByName("commonMain") as SourceSetAutoInstallExtension + assertEquals(BuildConfig.SentryKmpVersion, sourceSetAutoInstallExtension.sentryKmpVersion.get()) + } + + @Test + fun `custom kmp version overrides default in commonMain extension`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension + autoInstallExtension.commonMain.sentryKmpVersion.set("1.2.3") + + assertEquals("1.2.3", autoInstallExtension.commonMain.sentryKmpVersion.get()) + } + + @ParameterizedTest + @ValueSource(strings = ["1.0.0", "2.3.4-SNAPSHOT", "latest.release"]) + fun `sentryKmpVersion accepts various version formats in commonMain extension`(version: String) { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension + autoInstallExtension.commonMain.sentryKmpVersion.set(version) + + assertEquals(version, autoInstallExtension.commonMain.sentryKmpVersion.get()) + } + + @Test + fun `when autoInstall is disabled, no installations are performed`() { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val autoInstallExtension = project.extensions.getByName("autoInstall") as AutoInstallExtension + autoInstallExtension.enabled.set(false) + + project.afterEvaluate { + val commonMainConfiguration = + project.configurations.find { it.name.contains("commonMain", ignoreCase = true) } + assertNull(commonMainConfiguration) + + val cocoapodsExtension = project.extensions.getByName("cocoapods") as CocoapodsExtension + val sentryPod = cocoapodsExtension.pods.findByName("Sentry") + assertNull(sentryPod) + } + } + + @Test + fun `installSentryForKmp adds dependency to commonMain`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + project.installSentryForKmp(project.extensions.getByName("commonMain") as SourceSetAutoInstallExtension) + + val sentryDependencies = project.configurations + .flatMap { it.dependencies } + .filter { it.group == "io.sentry" && it.name == "sentry-kotlin-multiplatform" } + .toList() + + assertTrue(sentryDependencies.isNotEmpty()) + + val sentryDependency = sentryDependencies.first() + assertEquals("io.sentry", sentryDependency.group) + assertEquals("sentry-kotlin-multiplatform", sentryDependency.name) + + val commonMainConfiguration = + project.configurations.find { it.name.contains("commonMain", ignoreCase = true) } + assertNotNull(commonMainConfiguration) + assertTrue(commonMainConfiguration!!.dependencies.contains(sentryDependency)) + } + + @Test + fun `install Sentry pod if not already exists`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") + + project.installSentryForCocoapods(project.extensions.getByName("cocoapods") as CocoapodsAutoInstallExtension) + + project.afterEvaluate { + val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) + val sentryPod = cocoapodsExtension?.pods?.findByName("Sentry") + assertTrue(sentryPod != null) + assertTrue(sentryPod!!.linkOnly) + } + } + + @Test + fun `do not install Sentry pod when cocoapods plugin when Sentry cocoapods configuration exists`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") + + val kmpExtension = project.extensions.findByName("kotlin") + (kmpExtension as ExtensionAware).extensions.configure(CocoapodsExtension::class.java) { cocoapods -> + cocoapods.pod("Sentry") { version = "custom version" } + } + + // plugin does not configure sentry pod if there is already an existing configuration + project.afterEvaluate { + val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) + val pod = cocoapodsExtension?.pods?.findByName("Sentry") + assertEquals(pod?.version, "custom version") + } + } +} From c9bdd3846f045c90ef5f92fe5f9beaa37c99c9db Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 5 Feb 2025 18:28:49 +0100 Subject: [PATCH 18/34] formatting --- .../gradle/CocoaFrameworkLinker.kt | 8 +++----- .../gradle/FrameworkPathResolver.kt | 14 ++++++------- .../ManualFrameworkPathSearchValueSource.kt | 20 +++++++++++-------- .../multiplatform/gradle/SentryPlugin.kt | 8 ++++---- .../gradle/CocoaFrameworkLinkerTest.kt | 8 ++++++-- .../gradle/CustomPathStrategyTest.kt | 8 +++++--- .../gradle/DerivedDataStrategyTest.kt | 14 ++++++++----- .../gradle/ManualSearchStrategyTest.kt | 11 ++++++---- .../gradle/SentryFrameworkArchitectureTest.kt | 8 ++++---- 9 files changed, 57 insertions(+), 42 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index 3ccfe2a3..f37987f6 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -7,7 +7,6 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable -import java.awt.Frame /** * Configures Sentry Cocoa framework linking for Apple targets in Kotlin Multiplatform projects. @@ -21,7 +20,7 @@ class CocoaFrameworkLinker( private val hostIsMac: Boolean ) { fun configure( - appleTargets: List, + appleTargets: List ) { if (!hostIsMac) { throw FrameworkLinkingException("Sentry Cocoa framework linking requires a macOS host") @@ -92,7 +91,7 @@ class FrameworkLinker( !binary.isStatic && dynamicPath != null -> dynamicPath to "dynamic" else -> throw FrameworkLinkingException( "Framework mismatch for ${binary.name}. " + - "Required ${if (binary.isStatic) "static" else "dynamic"} Sentry Cocoa framework not found." + "Required ${if (binary.isStatic) "static" else "dynamic"} Sentry Cocoa framework not found." ) } @@ -114,8 +113,7 @@ internal class FrameworkLinkingException( * across different versions. For example: * - iosArm64 -> [SentryCocoaFrameworkArchitectures.IOS_ARM64] * - macosArm64 -> [SentryCocoaFrameworkArchitectures.MACOS_ARM64_AND_X64] - * - * @return Set of possible architecture folder names for the given target. Returns empty set if target is not supported. + * * @return Set of possible architecture folder names for the given target. Returns empty set if target is not supported. */ internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set = buildSet { when (name) { diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index c0957ac2..90a7a4c2 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -56,7 +56,7 @@ interface FrameworkResolutionStrategy { * Finds the framework path based on the custom framework paths set by the user. This should generally be executed first. */ class CustomPathStrategy( - project: Project, + project: Project ) : FrameworkResolutionStrategy { private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) @@ -94,7 +94,7 @@ class DerivedDataStrategy( project.providers.of(DerivedDataPathValueSource::class.java) { it.parameters.xcodeprojPath.set(xcodeprojPath) }.orNull - }, + } ) : FrameworkResolutionStrategy { private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) @@ -108,9 +108,9 @@ class DerivedDataStrategy( val derivedDataPath = derivedDataProvider(foundXcodeproj) ?: return FrameworkPaths.NONE val dynamicBasePath = - "${derivedDataPath}/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework" + "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework" val staticBasePath = - "${derivedDataPath}/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework" + "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework" return FrameworkPaths.createValidated( dynamicBasePath = dynamicBasePath, @@ -186,7 +186,7 @@ class ManualSearchStrategy( class FrameworkPathResolver( private val project: Project, - private val strategies: List = defaultStrategies(project), + private val strategies: List = defaultStrategies(project) ) { fun resolvePaths( architecture: String @@ -239,10 +239,10 @@ class FrameworkPathResolver( return listOf( CustomPathStrategy(project), DerivedDataStrategy(project), - ManualSearchStrategy(project), + ManualSearchStrategy(project) // TODO: add DownloadStrategy -> downloads the framework and stores it in build dir // this is especially useful for users who dont have a monorepo setup ) } } -} \ No newline at end of file +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt index 479aa84f..5c9e9ed4 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt @@ -1,7 +1,6 @@ package io.sentry.kotlin.multiplatform.gradle import org.gradle.api.provider.Property -import org.gradle.api.provider.SetProperty import org.gradle.api.provider.ValueSource import org.gradle.api.provider.ValueSourceParameters import org.gradle.process.ExecOperations @@ -40,19 +39,24 @@ abstract class ManualFrameworkPathSearchValueSource : if (frameworkType == FrameworkType.STATIC) "Sentry.xcframework" else "Sentry-Dynamic.xcframework" execOperations.exec { it.commandLine( - "bash", "-c", + "bash", + "-c", "find $basePathToSearch " + - "-name $xcFrameworkName " + - "-exec stat -f \"%m %N\" {} \\; | " + - "sort -nr | " + - "cut -d' ' -f2-" + "-name $xcFrameworkName " + + "-exec stat -f \"%m %N\" {} \\; | " + + "sort -nr | " + + "cut -d' ' -f2-" ) it.standardOutput = output it.isIgnoreExitValue = true } val stringOutput = output.toString("UTF-8") - return if (stringOutput.lineSequence().firstOrNull().isNullOrEmpty()) null else stringOutput.lineSequence() - .first() + return if (stringOutput.lineSequence().firstOrNull().isNullOrEmpty()) { + null + } else { + stringOutput.lineSequence() + .first() + } } } 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 4bdc8bef..72e590b6 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 @@ -67,7 +67,7 @@ class SentryPlugin : Plugin { binaryLinker = FrameworkLinker(logger), HostManager.hostIsMac ).configure( - appleTargets, + appleTargets ) } } @@ -88,9 +88,9 @@ internal fun Project.installSentryForKmp( if (unsupportedTargets.any { unsupported -> target.name.contains(unsupported) }) { throw GradleException( "Unsupported target: ${target.name}. " + - "Cannot auto install in commonMain. " + - "Please create an intermediate sourceSet with targets that the Sentry SDK " + - "supports (apple, jvm, android) and add the dependency manually." + "Cannot auto install in commonMain. " + + "Please create an intermediate sourceSet with targets that the Sentry SDK " + + "supports (apple, jvm, android) and add the dependency manually." ) } } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt index 45e7cfcc..720b39c1 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt @@ -1,5 +1,9 @@ -import io.sentry.kotlin.multiplatform.gradle.* +import io.sentry.kotlin.multiplatform.gradle.CocoaFrameworkLinker +import io.sentry.kotlin.multiplatform.gradle.FrameworkLinker import io.sentry.kotlin.multiplatform.gradle.FrameworkLinkingException +import io.sentry.kotlin.multiplatform.gradle.FrameworkPathResolver +import io.sentry.kotlin.multiplatform.gradle.FrameworkPaths +import io.sentry.kotlin.multiplatform.gradle.FrameworkResolutionStrategy import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension @@ -154,4 +158,4 @@ private class FakeStrategy : FrameworkResolutionStrategy { } val staticPath = "/path/to/static/Sentry.xcframework" -val dynamicPath = "/path/to/dynamic/Sentry-Dynamic.xcframework" \ No newline at end of file +val dynamicPath = "/path/to/dynamic/Sentry-Dynamic.xcframework" diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt index bbbdb42f..0318dc06 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt @@ -24,7 +24,8 @@ class CustomPathStrategyTest { @ParameterizedTest(name = "should return static path for architecture {0}") @MethodSource("architectureMappingProvider") fun `should return static path when framework is Sentry xcframework`( - expectedArchitecture: String, @TempDir dir: Path + expectedArchitecture: String, + @TempDir dir: Path ) { val xcframeworkPath = dir.resolve("Sentry.xcframework") Files.createDirectory(xcframeworkPath) @@ -40,7 +41,8 @@ class CustomPathStrategyTest { @ParameterizedTest(name = "should return dynamic path for architecture {0}") @MethodSource("architectureMappingProvider") fun `should return dynamic path when framework is Sentry xcframework`( - expectedArchitecture: String, @TempDir dir: Path + expectedArchitecture: String, + @TempDir dir: Path ) { val xcframeworkPath = dir.resolve("Sentry-Dynamic.xcframework") Files.createDirectory(xcframeworkPath) @@ -101,4 +103,4 @@ class CustomPathStrategyTest { return CustomPathStrategy(project) } } -} \ No newline at end of file +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt index 351cd653..764e843d 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt @@ -24,7 +24,8 @@ class DerivedDataStrategyTest { @ParameterizedTest(name = "should return static path for architecture {0}") @MethodSource("architectureMappingProvider") fun `if xcodeproj is null and find xcode project successfully then resolve static path`( - expectedArchitecture: String, @TempDir dir: Path + expectedArchitecture: String, + @TempDir dir: Path ) { Files.createDirectory(dir.resolve("project.xcodeproj")) val xcframeworkPath = dir.resolve("SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework") @@ -43,7 +44,8 @@ class DerivedDataStrategyTest { @ParameterizedTest(name = "should return dynamic path for architecture {0}") @MethodSource("architectureMappingProvider") fun `if xcodeproj is null and find xcode project successfully then resolve dynamic path`( - expectedArchitecture: String, @TempDir dir: Path + expectedArchitecture: String, + @TempDir dir: Path ) { Files.createDirectory(dir.resolve("project.xcodeproj")) val xcframeworkPath = dir.resolve("SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework") @@ -62,7 +64,8 @@ class DerivedDataStrategyTest { @ParameterizedTest(name = "should return dynamic path for architecture {0}") @MethodSource("architectureMappingProvider") fun `if xcodeproj is null and find xcode project is not successful then return NONE`( - expectedArchitecture: String, @TempDir dir: Path + expectedArchitecture: String, + @TempDir dir: Path ) { val sut = fixture.getSut(null, rootDirPath = dir.toFile().absolutePath) { _: String -> dir.toFile().absolutePath @@ -75,7 +78,8 @@ class DerivedDataStrategyTest { @ParameterizedTest(name = "should return dynamic path for architecture {0}") @MethodSource("architectureMappingProvider") fun `if xcodeproj is not null and find xcode project is not successful then return NONE`( - expectedArchitecture: String, @TempDir dir: Path + expectedArchitecture: String, + @TempDir dir: Path ) { val sut = fixture.getSut("some invalid path", rootDirPath = dir.toFile().absolutePath) { _: String -> dir.toFile().absolutePath @@ -113,4 +117,4 @@ class DerivedDataStrategyTest { return DerivedDataStrategy(project, derivedDataProvider) } } -} \ No newline at end of file +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt index 2fc9c47a..3c273bfd 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt @@ -27,7 +27,8 @@ class ManualSearchStrategyTest { @ParameterizedTest(name = "should return static path for architecture {0}") @MethodSource("architectureMappingProvider") fun `should return static path when framework exists`( - expectedArchitecture: String, @TempDir dir: Path + expectedArchitecture: String, + @TempDir dir: Path ) { val xcframeworkPath = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture)) @@ -42,7 +43,8 @@ class ManualSearchStrategyTest { @ParameterizedTest(name = "should return dynamic path for architecture {0}") @MethodSource("architectureMappingProvider") fun `should return dynamic path when framework exists`( - expectedArchitecture: String, @TempDir dir: Path + expectedArchitecture: String, + @TempDir dir: Path ) { val xcframeworkPath = dir.resolve("somewhere/hidden/Sentry-Dynamic.xcframework").createDirectories() val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture)) @@ -57,7 +59,8 @@ class ManualSearchStrategyTest { @ParameterizedTest(name = "should return most recently used path for architecture {0}") @MethodSource("architectureMappingProvider") fun `should return most recently used path when multiple framework exists`( - expectedArchitecture: String, @TempDir dir: Path + expectedArchitecture: String, + @TempDir dir: Path ) { val temp = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() // Modifying this path so it's modified longer ago than the second path @@ -101,4 +104,4 @@ class ManualSearchStrategyTest { return ManualSearchStrategy(project, basePathToSearch) } } -} \ No newline at end of file +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt index d36c0bc0..b88b4fdb 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt @@ -71,8 +71,8 @@ class SentryFrameworkArchitectureTest { assert(foundMatch) { "Expected to find one of $mappedArchNames in $xcFramework for target ${it.name}.\nFound instead: ${ - xcFramework.listFiles() - ?.map { file -> file.name } + xcFramework.listFiles() + ?.map { file -> file.name } }" } } @@ -124,8 +124,8 @@ class SentryFrameworkArchitectureTest { assert(foundMatch) { "Expected to find one of $mappedArchNames in $xcFramework for target ${it.name}.\nFound instead: ${ - xcFramework.listFiles() - ?.map { file -> file.name } + xcFramework.listFiles() + ?.map { file -> file.name } }" } } From ae0e0ac65658d99a03a75c64677c3670de50285b Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 5 Feb 2025 18:33:48 +0100 Subject: [PATCH 19/34] update tests --- .../gradle/DerivedDataStrategyTest.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt index 764e843d..dec1b32c 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt @@ -4,6 +4,7 @@ import org.gradle.testfixtures.ProjectBuilder import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -21,7 +22,7 @@ class DerivedDataStrategyTest { fixture = Fixture() } - @ParameterizedTest(name = "should return static path for architecture {0}") + @ParameterizedTest(name = "resolve static path for architecture {0}") @MethodSource("architectureMappingProvider") fun `if xcodeproj is null and find xcode project successfully then resolve static path`( expectedArchitecture: String, @@ -48,7 +49,8 @@ class DerivedDataStrategyTest { @TempDir dir: Path ) { Files.createDirectory(dir.resolve("project.xcodeproj")) - val xcframeworkPath = dir.resolve("SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework") + val xcframeworkPath = + dir.resolve("SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework") val xcframeworkDirectory = Files.createDirectories(xcframeworkPath) val archDirectory = Files.createDirectory(xcframeworkDirectory.resolve(expectedArchitecture)) @@ -61,31 +63,27 @@ class DerivedDataStrategyTest { assertNull(paths.static) } - @ParameterizedTest(name = "should return dynamic path for architecture {0}") - @MethodSource("architectureMappingProvider") + @Test fun `if xcodeproj is null and find xcode project is not successful then return NONE`( - expectedArchitecture: String, @TempDir dir: Path ) { val sut = fixture.getSut(null, rootDirPath = dir.toFile().absolutePath) { _: String -> dir.toFile().absolutePath } - val paths = sut.resolvePaths(expectedArchitecture) + val paths = sut.resolvePaths("doesnt matter") assertEquals(FrameworkPaths.NONE, paths) } - @ParameterizedTest(name = "should return dynamic path for architecture {0}") - @MethodSource("architectureMappingProvider") + @Test fun `if xcodeproj is not null and find xcode project is not successful then return NONE`( - expectedArchitecture: String, @TempDir dir: Path ) { val sut = fixture.getSut("some invalid path", rootDirPath = dir.toFile().absolutePath) { _: String -> dir.toFile().absolutePath } - val paths = sut.resolvePaths(expectedArchitecture) + val paths = sut.resolvePaths("doesnt matter") assertEquals(FrameworkPaths.NONE, paths) } From e551c09cb17c8d35c89ec66d2c715a4bc4bd10c4 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 5 Feb 2025 19:06:03 +0100 Subject: [PATCH 20/34] fix test --- .../kotlin/multiplatform/gradle/FrameworkPathResolver.kt | 3 --- .../multiplatform/gradle/ManualSearchStrategyTest.kt | 8 +++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index 90a7a4c2..28a10b30 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -173,9 +173,6 @@ class ManualSearchStrategy( } } - println("static: ${staticValueSource.orNull}") - println("dynamic: ${dynamicValueSource.orNull}") - return FrameworkPaths.createValidated( dynamicBasePath = dynamicValueSource.orNull, staticBasePath = staticValueSource.orNull, diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt index 3c273bfd..81b0c2e9 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt @@ -9,12 +9,14 @@ import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource +import java.lang.Thread.sleep import java.nio.file.Files import java.nio.file.Path import java.nio.file.attribute.FileTime import java.time.Instant import kotlin.io.path.absolutePathString import kotlin.io.path.createDirectories +import kotlin.io.path.setLastModifiedTime class ManualSearchStrategyTest { private lateinit var fixture: Fixture @@ -62,11 +64,11 @@ class ManualSearchStrategyTest { expectedArchitecture: String, @TempDir dir: Path ) { - val temp = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() - // Modifying this path so it's modified longer ago than the second path - val xcframeworkPath1 = Files.setLastModifiedTime(temp, FileTime.from(Instant.now().minusSeconds(5))) + val xcframeworkPath1 = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() Files.createDirectory(xcframeworkPath1.resolve(expectedArchitecture)) + sleep(1000) + val xcframeworkPath2 = dir.resolve("more/recent/Sentry.xcframework").createDirectories() val archDirectory2 = Files.createDirectory(xcframeworkPath2.resolve(expectedArchitecture)) From 7beec81ffb13d836c8f49e67839be5022b69a3c6 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 5 Feb 2025 19:06:40 +0100 Subject: [PATCH 21/34] add comment to test --- .../kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt index 81b0c2e9..163f31b4 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt @@ -67,6 +67,8 @@ class ManualSearchStrategyTest { val xcframeworkPath1 = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() Files.createDirectory(xcframeworkPath1.resolve(expectedArchitecture)) + // sleep so both directories have different timestamps. + // This needs to be in seconds since the captured timestamps are not precise enough sleep(1000) val xcframeworkPath2 = dir.resolve("more/recent/Sentry.xcframework").createDirectories() From 909fa959d1ce3426ca350e69eb6ba28cefb77671 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 5 Feb 2025 19:10:47 +0100 Subject: [PATCH 22/34] dont throw if mac is not host --- .../gradle/CocoaFrameworkLinker.kt | 9 +---- .../multiplatform/gradle/SentryPlugin.kt | 39 ++++++++++--------- .../gradle/CocoaFrameworkLinkerTest.kt | 16 ++------ 3 files changed, 25 insertions(+), 39 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index f37987f6..2d668ef1 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -17,15 +17,8 @@ class CocoaFrameworkLinker( private val logger: Logger, private val pathResolver: FrameworkPathResolver, private val binaryLinker: FrameworkLinker, - private val hostIsMac: Boolean ) { - fun configure( - appleTargets: List - ) { - if (!hostIsMac) { - throw FrameworkLinkingException("Sentry Cocoa framework linking requires a macOS host") - } - + fun configure(appleTargets: List) { appleTargets.forEach { target -> try { logger.lifecycle( 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 72e590b6..0c889862 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,24 +51,25 @@ class SentryPlugin : Plugin { } } - // If the user is not using the cocoapods plugin, linking to the framework is not - // automatic so we have to configure it in the plugin. - if (!hasCocoapodsPlugin) { - logger.info("Cocoapods plugin not found. Attempting to link Sentry Cocoa framework.") + if (HostManager.hostIsMac) { + // If the user is not using the cocoapods plugin, linking to the framework is not + // automatic so we have to configure it in the plugin. + if (!hasCocoapodsPlugin) { + logger.info("Cocoapods plugin not found. Attempting to link Sentry Cocoa framework.") - val kmpExtension = - extensions.findByName(KOTLIN_EXTENSION_NAME) as? KotlinMultiplatformExtension - val appleTargets = kmpExtension?.appleTargets()?.toList() - ?: throw GradleException("Error fetching Apple targets from Kotlin Multiplatform plugin.") + val kmpExtension = + extensions.findByName(KOTLIN_EXTENSION_NAME) as? KotlinMultiplatformExtension + val appleTargets = kmpExtension?.appleTargets()?.toList() + ?: throw GradleException("Error fetching Apple targets from Kotlin Multiplatform plugin.") - CocoaFrameworkLinker( - logger = logger, - pathResolver = FrameworkPathResolver(project), - binaryLinker = FrameworkLinker(logger), - HostManager.hostIsMac - ).configure( - appleTargets - ) + CocoaFrameworkLinker( + logger = logger, + pathResolver = FrameworkPathResolver(project), + binaryLinker = FrameworkLinker(logger), + ).configure( + appleTargets + ) + } } } } @@ -88,9 +89,9 @@ internal fun Project.installSentryForKmp( if (unsupportedTargets.any { unsupported -> target.name.contains(unsupported) }) { throw GradleException( "Unsupported target: ${target.name}. " + - "Cannot auto install in commonMain. " + - "Please create an intermediate sourceSet with targets that the Sentry SDK " + - "supports (apple, jvm, android) and add the dependency manually." + "Cannot auto install in commonMain. " + + "Please create an intermediate sourceSet with targets that the Sentry SDK " + + "supports (apple, jvm, android) and add the dependency manually." ) } } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt index 720b39c1..d1030aa8 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt @@ -22,13 +22,6 @@ class CocoaFrameworkLinkerTest { fixture = Fixture() } - @Test - fun `throws if host is not macOS`() { - val sut = fixture.getSut(false) - - assertThrows { sut.configure(emptyList()) } - } - @Test fun `framework linking succeeds for static Framework binary`() { val kmpExtension = fixture.project.extensions.getByType(KotlinMultiplatformExtension::class.java) @@ -51,7 +44,7 @@ class CocoaFrameworkLinkerTest { } } - val sut = fixture.getSut(true) + val sut = fixture.getSut() sut.configure(appleTargets) appleTargets.forEach { target -> @@ -83,7 +76,7 @@ class CocoaFrameworkLinkerTest { } } - val sut = fixture.getSut(true) + val sut = fixture.getSut() sut.configure(appleTargets) appleTargets.forEach { target -> @@ -115,7 +108,7 @@ class CocoaFrameworkLinkerTest { } } - val sut = fixture.getSut(true) + val sut = fixture.getSut() sut.configure(appleTargets) // both dynamic and static frameworks can be used for linkin in test executables @@ -138,12 +131,11 @@ class CocoaFrameworkLinkerTest { } } - fun getSut(hostIsMac: Boolean): CocoaFrameworkLinker { + fun getSut(): CocoaFrameworkLinker { return CocoaFrameworkLinker( project.logger, FrameworkPathResolver(project, strategies = listOf(FakeStrategy())), FrameworkLinker(project.logger), - hostIsMac ) } } From 0cf0e41aad367db568b323df3d5bfe93f310e118 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 5 Feb 2025 19:11:49 +0100 Subject: [PATCH 23/34] fix test --- .../kotlin/multiplatform/gradle/DerivedDataPathTest.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt index 3348c6ab..dcb9413d 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt @@ -8,6 +8,7 @@ import org.gradle.process.ExecOperations import org.gradle.process.ExecResult import org.gradle.process.ExecSpec import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -71,7 +72,7 @@ class DerivedDataPathTest { } @Test - fun `obtain should throw GradleException when BUILD_DIR is not found`() { + fun `obtain should return null when BUILD_DIR is not found`() { val xcodebuildOutput = "Some output without BUILD_DIR" every { parameters.xcodeprojPath } returns mockk { @@ -101,8 +102,6 @@ class DerivedDataPathTest { } } - assertThrows { - valueSource.obtain() - } + assertNull(valueSource.obtain()) } } From 90dd2c038f568115d452a5830b731cfec97577c1 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 6 Feb 2025 12:06:09 +0100 Subject: [PATCH 24/34] improve coverage --- .../gradle/CocoaFrameworkLinker.kt | 2 +- .../multiplatform/gradle/SentryPlugin.kt | 71 ++++++------ .../gradle/CocoaFrameworkLinkerTest.kt | 4 +- .../gradle/DerivedDataPathTest.kt | 2 - .../gradle/ManualSearchStrategyTest.kt | 3 - .../multiplatform/gradle/SentryPluginTest.kt | 101 +++++++++++++++--- 6 files changed, 126 insertions(+), 57 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index 2d668ef1..cb8edbc0 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -16,7 +16,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable class CocoaFrameworkLinker( private val logger: Logger, private val pathResolver: FrameworkPathResolver, - private val binaryLinker: FrameworkLinker, + private val binaryLinker: FrameworkLinker ) { fun configure(appleTargets: List) { appleTargets.forEach { target -> 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 0c889862..552a72b9 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 @@ -38,41 +38,40 @@ class SentryPlugin : Plugin { ) afterEvaluate { - val hasCocoapodsPlugin = - project.plugins.findPlugin(KotlinCocoapodsPlugin::class.java) != null - - if (sentryExtension.autoInstall.enabled.get()) { - if (sentryExtension.autoInstall.commonMain.enabled.get()) { - installSentryForKmp(sentryExtension.autoInstall.commonMain) - } - - if (hasCocoapodsPlugin && sentryExtension.autoInstall.cocoapods.enabled.get()) { - installSentryForCocoapods(sentryExtension.autoInstall.cocoapods) - } - } - - if (HostManager.hostIsMac) { - // If the user is not using the cocoapods plugin, linking to the framework is not - // automatic so we have to configure it in the plugin. - if (!hasCocoapodsPlugin) { - logger.info("Cocoapods plugin not found. Attempting to link Sentry Cocoa framework.") - - val kmpExtension = - extensions.findByName(KOTLIN_EXTENSION_NAME) as? KotlinMultiplatformExtension - val appleTargets = kmpExtension?.appleTargets()?.toList() - ?: throw GradleException("Error fetching Apple targets from Kotlin Multiplatform plugin.") - - CocoaFrameworkLinker( - logger = logger, - pathResolver = FrameworkPathResolver(project), - binaryLinker = FrameworkLinker(logger), - ).configure( - appleTargets - ) - } - } + executeConfiguration(project) } } + + internal fun executeConfiguration(project: Project, hostIsMac: Boolean = HostManager.hostIsMac) { + val sentryExtension = project.extensions.getByType(SentryExtension::class.java) + val hasCocoapodsPlugin = project.plugins.findPlugin(KotlinCocoapodsPlugin::class.java) != null + + if (sentryExtension.autoInstall.enabled.get()) { + val autoInstall = sentryExtension.autoInstall + + if (autoInstall.commonMain.enabled.get()) { + project.installSentryForKmp(autoInstall.commonMain) + } + + if (hasCocoapodsPlugin && autoInstall.cocoapods.enabled.get() && hostIsMac) { + project.installSentryForCocoapods(autoInstall.cocoapods) + } + } + + if (hostIsMac && !hasCocoapodsPlugin) { + project.logger.info("Cocoapods plugin not found. Attempting to link Sentry Cocoa framework.") + + val kmpExtension = project.extensions.findByName(KOTLIN_EXTENSION_NAME) as? KotlinMultiplatformExtension + val appleTargets = kmpExtension?.appleTargets()?.toList() + ?: throw GradleException("Error fetching Apple targets from Kotlin Multiplatform plugin.") + + CocoaFrameworkLinker( + logger = project.logger, + pathResolver = FrameworkPathResolver(project), + binaryLinker = FrameworkLinker(project.logger) + ).configure(appleTargets) + } + } } internal fun Project.installSentryForKmp( @@ -89,9 +88,9 @@ internal fun Project.installSentryForKmp( if (unsupportedTargets.any { unsupported -> target.name.contains(unsupported) }) { throw GradleException( "Unsupported target: ${target.name}. " + - "Cannot auto install in commonMain. " + - "Please create an intermediate sourceSet with targets that the Sentry SDK " + - "supports (apple, jvm, android) and add the dependency manually." + "Cannot auto install in commonMain. " + + "Please create an intermediate sourceSet with targets that the Sentry SDK " + + "supports (apple, jvm, android) and add the dependency manually." ) } } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt index d1030aa8..9686a7f2 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt @@ -1,6 +1,5 @@ import io.sentry.kotlin.multiplatform.gradle.CocoaFrameworkLinker import io.sentry.kotlin.multiplatform.gradle.FrameworkLinker -import io.sentry.kotlin.multiplatform.gradle.FrameworkLinkingException import io.sentry.kotlin.multiplatform.gradle.FrameworkPathResolver import io.sentry.kotlin.multiplatform.gradle.FrameworkPaths import io.sentry.kotlin.multiplatform.gradle.FrameworkResolutionStrategy @@ -12,7 +11,6 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows class CocoaFrameworkLinkerTest { private lateinit var fixture: Fixture @@ -135,7 +133,7 @@ class CocoaFrameworkLinkerTest { return CocoaFrameworkLinker( project.logger, FrameworkPathResolver(project, strategies = listOf(FakeStrategy())), - FrameworkLinker(project.logger), + FrameworkLinker(project.logger) ) } } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt index dcb9413d..99c2cab5 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt @@ -3,7 +3,6 @@ package io.sentry.kotlin.multiplatform.gradle import io.mockk.every import io.mockk.mockk import org.gradle.api.Action -import org.gradle.api.GradleException import org.gradle.process.ExecOperations import org.gradle.process.ExecResult import org.gradle.process.ExecSpec @@ -11,7 +10,6 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows import java.io.ByteArrayOutputStream class DerivedDataPathTest { diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt index 163f31b4..b6f2ae7c 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt @@ -12,11 +12,8 @@ import org.junit.jupiter.params.provider.MethodSource import java.lang.Thread.sleep import java.nio.file.Files import java.nio.file.Path -import java.nio.file.attribute.FileTime -import java.time.Instant import kotlin.io.path.absolutePathString import kotlin.io.path.createDirectories -import kotlin.io.path.setLastModifiedTime class ManualSearchStrategyTest { private lateinit var fixture: Fixture 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 472d6725..772b0c24 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,14 +1,18 @@ package io.sentry.kotlin.multiplatform.gradle import io.sentry.BuildConfig +import org.gradle.api.GradleException 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.gradle.targets.js.dsl.ExperimentalWasmDsl import org.junit.jupiter.api.Assertions.assertEquals 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.Test +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource @@ -150,6 +154,30 @@ class SentryPluginTest { assertTrue(commonMainConfiguration!!.dependencies.contains(sentryDependency)) } + @OptIn(ExperimentalWasmDsl::class) + @ParameterizedTest(name = "installSentryForKmp throws if build contains unsupported target {0}") + @ValueSource(strings = ["wasm", "js", "mingw", "linux", "androidNative"]) + fun `installSentryForKmp throws if build contains any unsupported target`(unsupportedTarget: String) { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kmpExtension.apply { + when (unsupportedTarget) { + "wasm" -> wasmJs() + "js" -> js() + "mingw" -> mingwX64() + "linux" -> linuxX64() + "androidNative" -> androidNativeArm64() + } + } + + assertThrows { + project.installSentryForKmp(project.extensions.getByName("commonMain") as SourceSetAutoInstallExtension) + } + } + @Test fun `install Sentry pod if not already exists`() { val project = ProjectBuilder.builder().build() @@ -157,14 +185,43 @@ class SentryPluginTest { project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") - project.installSentryForCocoapods(project.extensions.getByName("cocoapods") as CocoapodsAutoInstallExtension) + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) as ExtensionAware + val cocoapodsExtension = kmpExtension.extensions.getByType(CocoapodsExtension::class.java) + val sentryPod = cocoapodsExtension.pods.findByName("Sentry") - project.afterEvaluate { - val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) - val sentryPod = cocoapodsExtension?.pods?.findByName("Sentry") - assertTrue(sentryPod != null) - assertTrue(sentryPod!!.linkOnly) - } + // Check that it does not exist + assertNull(sentryPod) + + val plugin = project.plugins.getPlugin(SentryPlugin::class.java) + plugin.executeConfiguration(project) + + // Check that it now exists + val cocoapodsAutoInstallExtension = project.extensions.getByType(CocoapodsAutoInstallExtension::class.java) + assertEquals(cocoapodsExtension.pods.getByName("Sentry").version, cocoapodsAutoInstallExtension.sentryCocoaVersion.get()) + } + + @Test + fun `install Sentry pod and prioritize user set version for cocoapods installation`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") + + val cocoapodsAutoInstallExtension = project.extensions.getByType(CocoapodsAutoInstallExtension::class.java) + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) as ExtensionAware + val cocoapodsExtension = kmpExtension.extensions.getByType(CocoapodsExtension::class.java) + val sentryPod = cocoapodsExtension.pods.findByName("Sentry") + + cocoapodsAutoInstallExtension.sentryCocoaVersion.set("10000.0.0") + + // Check that it does not exist + assertNull(sentryPod) + + val plugin = project.plugins.getPlugin(SentryPlugin::class.java) + plugin.executeConfiguration(project) + + // Check that it now exists + assertEquals(cocoapodsExtension.pods.getByName("Sentry").version, "10000.0.0") } @Test @@ -179,11 +236,31 @@ class SentryPluginTest { cocoapods.pod("Sentry") { version = "custom version" } } - // plugin does not configure sentry pod if there is already an existing configuration - project.afterEvaluate { - val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) - val pod = cocoapodsExtension?.pods?.findByName("Sentry") - assertEquals(pod?.version, "custom version") + val plugin = project.plugins.getPlugin(SentryPlugin::class.java) + plugin.executeConfiguration(project) + + val cocoapodsExtension = kmpExtension.extensions.getByType(CocoapodsExtension::class.java) + assertEquals(cocoapodsExtension.pods.getByName("Sentry").version, "custom version") + } + + @Test + fun `do not install Sentry pod if host is not mac`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") + project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") + + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + (kmpExtension as ExtensionAware).extensions.configure(CocoapodsExtension::class.java) { cocoapods -> + cocoapods.ios.deploymentTarget = "14.1" + cocoapods.summary = "Test" + cocoapods.homepage = "https://sentry.io" } + + val plugin = project.plugins.getPlugin(SentryPlugin::class.java) + plugin.executeConfiguration(project, hostIsMac = false) + + val cocoapodsExtension = (kmpExtension as ExtensionAware).extensions.getByType(CocoapodsExtension::class.java) + assertNull(cocoapodsExtension.pods.findByName("Sentry")) } } From af76fcbe08ef5cce6c0ea0e9a0c4ae00961bec2f Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 6 Feb 2025 12:18:56 +0100 Subject: [PATCH 25/34] detekt --- config/detekt/detekt.yml | 2 + .../gradle/CocoaFrameworkLinker.kt | 63 ++++--------------- .../gradle/DerivedDataPathValueSource.kt | 3 +- .../multiplatform/gradle/FrameworkLinker.kt | 59 +++++++++++++++++ .../gradle/FrameworkPathResolver.kt | 14 +++-- .../ManualFrameworkPathSearchValueSource.kt | 13 ++-- 6 files changed, 89 insertions(+), 65 deletions(-) create mode 100644 sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 818763de..ffdd669a 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -13,6 +13,8 @@ style: UnusedPrivateMember: excludes: - "**/Attachment.kt" + ReturnCount: + active: false naming: MatchingDeclarationName: active: false diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index cb8edbc0..4bcff06a 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -25,7 +25,7 @@ class CocoaFrameworkLinker( "Start resolving Sentry Cocoa framework paths for target: ${target.name}" ) processTarget(target) - } catch (e: Exception) { + } catch (e: FrameworkLinkingException) { throw FrameworkLinkingException("Failed to configure ${target.name}: ${e.message}", e) } } @@ -44,55 +44,6 @@ class CocoaFrameworkLinker( } } -class FrameworkLinker( - private val logger: Logger -) { - fun configureBinaries( - binaries: KotlinNativeBinaryContainer, - dynamicPath: String?, - staticPath: String? - ) { - binaries.all { binary -> - when (binary) { - is TestExecutable -> linkTestBinary(binary, chooseTestPath(dynamicPath, staticPath)) - is Framework -> linkFrameworkBinary(binary, dynamicPath, staticPath) - else -> logger.info("Ignoring binary type: ${binary::class.java.simpleName}") - } - } - } - - private fun chooseTestPath(dynamic: String?, static: String?) = when { - dynamic != null && static != null -> { - logger.debug("Both framework types available, preferring dynamic for tests") - dynamic - } - - dynamic != null -> dynamic - static != null -> static - else -> throw FrameworkLinkingException("No valid framework path found for tests") - } - - private fun linkTestBinary(binary: TestExecutable, path: String) { - // Linking in test binaries works with both dynamic and static framework - binary.linkerOpts("-rpath", path, "-F$path") - logger.info("Linked Sentry Cocoa framework to test binary ${binary.name}") - } - - private fun linkFrameworkBinary(binary: Framework, dynamicPath: String?, staticPath: String?) { - val (path, type) = when { - binary.isStatic && staticPath != null -> staticPath to "static" - !binary.isStatic && dynamicPath != null -> dynamicPath to "dynamic" - else -> throw FrameworkLinkingException( - "Framework mismatch for ${binary.name}. " + - "Required ${if (binary.isStatic) "static" else "dynamic"} Sentry Cocoa framework not found." - ) - } - - binary.linkerOpts("-F$path") - logger.info("Linked $type Sentry Cocoa framework to ${binary.name}") - } -} - internal class FrameworkLinkingException( message: String, cause: Throwable? = null @@ -106,7 +57,8 @@ internal class FrameworkLinkingException( * across different versions. For example: * - iosArm64 -> [SentryCocoaFrameworkArchitectures.IOS_ARM64] * - macosArm64 -> [SentryCocoaFrameworkArchitectures.MACOS_ARM64_AND_X64] - * * @return Set of possible architecture folder names for the given target. Returns empty set if target is not supported. + * @return Set of possible architecture folder names for the given target. + * Returns empty set if target is not supported. */ internal fun KotlinNativeTarget.toSentryFrameworkArchitecture(): Set = buildSet { when (name) { @@ -130,7 +82,14 @@ internal object SentryCocoaFrameworkArchitectures { val WATCHOS_SIMULATOR_AND_X64 = setOf("watchos-arm64_i386_x86_64-simulator") // Used for tests - val all = setOf(IOS_SIMULATOR_AND_X64, IOS_ARM64, MACOS_ARM64_AND_X64, TVOS_SIMULATOR_AND_X64, TVOS_ARM64, WATCHOS_ARM, WATCHOS_SIMULATOR_AND_X64) + val all = setOf( + IOS_SIMULATOR_AND_X64, + IOS_ARM64, MACOS_ARM64_AND_X64, + TVOS_SIMULATOR_AND_X64, + TVOS_ARM64, + WATCHOS_ARM, + WATCHOS_SIMULATOR_AND_X64 + ) } internal fun KotlinMultiplatformExtension.appleTargets() = diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathValueSource.kt index a28f97bb..3269350e 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathValueSource.kt @@ -41,8 +41,7 @@ abstract class DerivedDataPathValueSource : val buildSettings = buildDirOutput.toString("UTF-8") val buildDirMatch = buildDirRegex.find(buildSettings) val buildDir = buildDirMatch?.groupValues?.get(1) - ?: return null - if (buildDir.contains("DerivedData").not()) { + if (buildDir == null || buildDir.contains("DerivedData").not()) { return null } return buildDir.replace("/Build/Products", "") diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt new file mode 100644 index 00000000..15f3fcfa --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt @@ -0,0 +1,59 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.api.logging.Logger +import org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer +import org.jetbrains.kotlin.gradle.plugin.mpp.Framework +import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable + +/** + * Responsible for executing the linking. + * This involves configuring and linking binaries to the Sentry Cocoa framework. + */ +class FrameworkLinker( + private val logger: Logger +) { + fun configureBinaries( + binaries: KotlinNativeBinaryContainer, + dynamicPath: String?, + staticPath: String? + ) { + binaries.all { binary -> + when (binary) { + is TestExecutable -> linkTestBinary(binary, chooseTestPath(dynamicPath, staticPath)) + is Framework -> linkFrameworkBinary(binary, dynamicPath, staticPath) + else -> logger.info("Ignoring binary type: ${binary::class.java.simpleName}") + } + } + } + + private fun chooseTestPath(dynamic: String?, static: String?) = when { + dynamic != null && static != null -> { + logger.debug("Both framework types available, preferring dynamic for tests") + dynamic + } + + dynamic != null -> dynamic + static != null -> static + else -> throw FrameworkLinkingException("No valid framework path found for tests") + } + + private fun linkTestBinary(binary: TestExecutable, path: String) { + // Linking in test binaries works with both dynamic and static framework + binary.linkerOpts("-rpath", path, "-F$path") + logger.info("Linked Sentry Cocoa framework to test binary ${binary.name}") + } + + private fun linkFrameworkBinary(binary: Framework, dynamicPath: String?, staticPath: String?) { + val (path, type) = when { + binary.isStatic && staticPath != null -> staticPath to "static" + !binary.isStatic && dynamicPath != null -> dynamicPath to "dynamic" + else -> throw FrameworkLinkingException( + "Framework mismatch for ${binary.name}. " + + "Required ${if (binary.isStatic) "static" else "dynamic"} Sentry Cocoa framework not found." + ) + } + + binary.linkerOpts("-F$path") + logger.info("Linked $type Sentry Cocoa framework to ${binary.name}") + } +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index 28a10b30..ec7c1b78 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -53,7 +53,8 @@ interface FrameworkResolutionStrategy { } /** - * Finds the framework path based on the custom framework paths set by the user. This should generally be executed first. + * Finds the framework path based on the custom framework paths set by the user. + * This should generally be executed first. */ class CustomPathStrategy( project: Project @@ -190,7 +191,9 @@ class FrameworkPathResolver( ): FrameworkPaths { strategies.forEach { strategy -> try { - project.logger.lifecycle("Attempt to resolve Sentry Cocoa framework paths using ${strategy::class.simpleName}") + project.logger.lifecycle( + "Attempt to resolve Sentry Cocoa framework paths using ${strategy::class.simpleName}" + ) val result = strategy.resolvePaths(architecture) if (result != FrameworkPaths.NONE) { project.logger.lifecycle("Found Sentry Cocoa framework paths using ${strategy::class.simpleName}") @@ -198,7 +201,7 @@ class FrameworkPathResolver( } else { project.logger.debug("Strategy ${strategy::class.simpleName} did not find valid paths") } - } catch (e: Exception) { + } catch (e: FrameworkLinkingException) { project.logger.warn( "Strategy ${strategy::class.simpleName} failed due to error: ${e.message}" ) @@ -229,8 +232,9 @@ class FrameworkPathResolver( /** * Default resolution strategies for finding the Sentry Cocoa framework path. * - * The order of resolution strategies matters, as the framework path will be resolved by the first successful strategy - * Specifically here Custom Path will be checked first, if that fails then it is followed by the Derived Data strategy etc... + * The order of resolution strategies matters, as the framework path will be + * resolved by the first successful strategy. Specifically here Custom Path will be checked first, + * if that fails then it is followed by the Derived Data strategy etc... */ fun defaultStrategies(project: Project): List { return listOf( diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt index 5c9e9ed4..60081d8c 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt @@ -19,8 +19,9 @@ abstract class ManualFrameworkPathSearchValueSource : override fun obtain(): String? { val frameworkType = parameters.frameworkType.get() val basePathToSearch = - parameters.basePathToSearch.convention("\"${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData\"") - .get() + parameters.basePathToSearch.convention( + "\"${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData\"" + ).get() return findFrameworkWithFindCommand(frameworkType, basePathToSearch) } @@ -42,10 +43,10 @@ abstract class ManualFrameworkPathSearchValueSource : "bash", "-c", "find $basePathToSearch " + - "-name $xcFrameworkName " + - "-exec stat -f \"%m %N\" {} \\; | " + - "sort -nr | " + - "cut -d' ' -f2-" + "-name $xcFrameworkName " + + "-exec stat -f \"%m %N\" {} \\; | " + + "sort -nr | " + + "cut -d' ' -f2-" ) it.standardOutput = output it.isIgnoreExitValue = true From e7f395820036c44fe455045888c9335dca870bea Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 6 Feb 2025 12:23:44 +0100 Subject: [PATCH 26/34] fix test --- .../kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt index 0f95eef0..2f510cda 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt @@ -36,7 +36,7 @@ class FrameworkPathResolverTest { @Test fun `proceeds past multiple failing strategies and returns first success`() { - every { mockStrategy1.resolvePaths(any()) } throws RuntimeException() + every { mockStrategy1.resolvePaths(any()) } throws FrameworkLinkingException("") every { mockStrategy2.resolvePaths(any()) } returns FrameworkPaths.NONE every { mockStrategy3.resolvePaths(any()) } returns FrameworkPaths(static = "valid") From 4e732353e24d724cb68a22077b0acf870ca72d29 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 6 Feb 2025 12:29:53 +0100 Subject: [PATCH 27/34] update --- .../kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt | 6 ++---- .../sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt | 2 +- .../gradle/ManualFrameworkPathSearchValueSource.kt | 8 ++++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index 4bcff06a..ba81a4f5 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -3,10 +3,7 @@ package io.sentry.kotlin.multiplatform.gradle import org.gradle.api.GradleException import org.gradle.api.logging.Logger import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer -import org.jetbrains.kotlin.gradle.plugin.mpp.Framework import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.gradle.plugin.mpp.TestExecutable /** * Configures Sentry Cocoa framework linking for Apple targets in Kotlin Multiplatform projects. @@ -84,7 +81,8 @@ internal object SentryCocoaFrameworkArchitectures { // Used for tests val all = setOf( IOS_SIMULATOR_AND_X64, - IOS_ARM64, MACOS_ARM64_AND_X64, + IOS_ARM64, + MACOS_ARM64_AND_X64, TVOS_SIMULATOR_AND_X64, TVOS_ARM64, WATCHOS_ARM, diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt index 15f3fcfa..8fbb04cf 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt @@ -49,7 +49,7 @@ class FrameworkLinker( !binary.isStatic && dynamicPath != null -> dynamicPath to "dynamic" else -> throw FrameworkLinkingException( "Framework mismatch for ${binary.name}. " + - "Required ${if (binary.isStatic) "static" else "dynamic"} Sentry Cocoa framework not found." + "Required ${if (binary.isStatic) "static" else "dynamic"} Sentry Cocoa framework not found." ) } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt index 60081d8c..f6ed4162 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt @@ -43,10 +43,10 @@ abstract class ManualFrameworkPathSearchValueSource : "bash", "-c", "find $basePathToSearch " + - "-name $xcFrameworkName " + - "-exec stat -f \"%m %N\" {} \\; | " + - "sort -nr | " + - "cut -d' ' -f2-" + "-name $xcFrameworkName " + + "-exec stat -f \"%m %N\" {} \\; | " + + "sort -nr | " + + "cut -d' ' -f2-" ) it.standardOutput = output it.isIgnoreExitValue = true From e1f05cccb2e2988ff4d26fbfd60dab2984178f22 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 6 Feb 2025 12:32:09 +0100 Subject: [PATCH 28/34] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cfee50e..28bf4727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- [Gradle Plugin]: Architecture folder name missmatch when using SPM and framework path searching ([#320](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/320)) + ### Dependencies - Bump Java SDK from v7.16.0 to v7.18.1 ([#295](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/295), [#299](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/299)) From abe48d995c1db53e2ba21826650336b9283b68bf Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 6 Feb 2025 12:35:40 +0100 Subject: [PATCH 29/34] update test --- .../io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt | 2 ++ 1 file changed, 2 insertions(+) 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 772b0c24..b1f2f830 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 @@ -198,6 +198,8 @@ class SentryPluginTest { // Check that it now exists val cocoapodsAutoInstallExtension = project.extensions.getByType(CocoapodsAutoInstallExtension::class.java) assertEquals(cocoapodsExtension.pods.getByName("Sentry").version, cocoapodsAutoInstallExtension.sentryCocoaVersion.get()) + assertTrue(cocoapodsExtension.pods.getByName("Sentry").linkOnly) + assertEquals(cocoapodsExtension.pods.getByName("Sentry").extraOpts, listOf("-compiler-option", "-fmodules")) } @Test From 524aef2e5c7973cce90ce9c41a2f6e623a9f02bc Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 6 Feb 2025 13:34:47 +0100 Subject: [PATCH 30/34] fix expected architectures --- .../gradle/CocoaFrameworkLinker.kt | 7 ++- .../gradle/FrameworkPathResolver.kt | 45 ++++++++++--------- .../ManualFrameworkPathSearchValueSource.kt | 6 +-- .../gradle/CocoaFrameworkLinkerTest.kt | 6 +-- .../gradle/CustomPathStrategyTest.kt | 16 +++---- .../gradle/DerivedDataStrategyTest.kt | 24 ++++++---- .../gradle/FrameworkPathResolverTest.kt | 8 ++-- .../gradle/ManualSearchStrategyTest.kt | 18 ++++---- 8 files changed, 69 insertions(+), 61 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt index ba81a4f5..ee47e83f 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -18,10 +18,11 @@ class CocoaFrameworkLinker( fun configure(appleTargets: List) { appleTargets.forEach { target -> try { - logger.lifecycle( + logger.info( "Start resolving Sentry Cocoa framework paths for target: ${target.name}" ) processTarget(target) + logger.lifecycle("Successfully configured Sentry Cocoa framework linking for target: ${target.name}") } catch (e: FrameworkLinkingException) { throw FrameworkLinkingException("Failed to configure ${target.name}: ${e.message}", e) } @@ -34,9 +35,7 @@ class CocoaFrameworkLinker( logger.warn("Skipping target ${target.name}: Unsupported architecture") return } - val paths: FrameworkPaths = architectures.firstNotNullOf { arch -> - pathResolver.resolvePaths(arch).takeIf { it != FrameworkPaths.NONE } - } + val paths: FrameworkPaths = pathResolver.resolvePaths(architectures) binaryLinker.configureBinaries(target.binaries, paths.dynamic, paths.static) } } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index ec7c1b78..75646e0c 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -18,17 +18,15 @@ data class FrameworkPaths( fun createValidated( dynamicBasePath: String? = null, staticBasePath: String? = null, - architecture: String, + architectures: Set, pathExists: (String) -> Boolean = { path -> File(path).exists() } ): FrameworkPaths { val dynamicPath = dynamicBasePath?.let { basePath -> - val path = "$basePath/$architecture" - path.takeIf { pathExists(it) } + architectures.map { arch -> "$basePath/$arch" }.firstOrNull { pathExists(it) } } val staticPath = staticBasePath?.let { basePath -> - val path = "$basePath/$architecture" - path.takeIf { pathExists(it) } + architectures.map { arch -> "$basePath/$arch" }.firstOrNull { pathExists(it) } } return when { @@ -49,7 +47,7 @@ data class FrameworkPaths( } interface FrameworkResolutionStrategy { - fun resolvePaths(architecture: String): FrameworkPaths + fun resolvePaths(architectures: Set): FrameworkPaths } /** @@ -57,28 +55,35 @@ interface FrameworkResolutionStrategy { * This should generally be executed first. */ class CustomPathStrategy( - project: Project + private val project: Project ) : FrameworkResolutionStrategy { private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) // In this function we don't distinguish between static and dynamic frameworks // We trust that the user knows the distinction if they purposefully override the framework path - override fun resolvePaths(architecture: String): FrameworkPaths { - return linker.frameworkPath.orNull?.takeIf { it.isNotEmpty() }?.let { basePath -> + override fun resolvePaths(architectures: Set): FrameworkPaths { + val result = linker.frameworkPath.orNull?.takeIf { it.isNotEmpty() }?.let { basePath -> when { basePath.endsWith("Sentry.xcframework") -> FrameworkPaths.createValidated( staticBasePath = basePath, - architecture = architecture + architectures = architectures ) basePath.endsWith("Sentry-Dynamic.xcframework") -> FrameworkPaths.createValidated( dynamicBasePath = basePath, - architecture = architecture + architectures = architectures ) else -> FrameworkPaths.NONE } } ?: FrameworkPaths.NONE + if (linker.frameworkPath.orNull != null && result == FrameworkPaths.NONE) { + project.logger.warn( + "Custom framework path has been set manually but could not be found. " + + "Trying to resolve framework paths using other strategies." + ) + } + return result } } @@ -99,7 +104,7 @@ class DerivedDataStrategy( ) : FrameworkResolutionStrategy { private val linker: LinkerExtension = project.extensions.getByType(LinkerExtension::class.java) - override fun resolvePaths(architecture: String): FrameworkPaths { + override fun resolvePaths(architectures: Set): FrameworkPaths { val xcodeprojSetByUser = linker.xcodeprojPath.orNull?.takeIf { it.isNotEmpty() } val foundXcodeproj = xcodeprojSetByUser ?: findXcodeprojFile(project.rootDir)?.absolutePath if (foundXcodeproj == null) { @@ -107,7 +112,6 @@ class DerivedDataStrategy( } val derivedDataPath = derivedDataProvider(foundXcodeproj) ?: return FrameworkPaths.NONE - val dynamicBasePath = "$derivedDataPath/SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework" val staticBasePath = @@ -116,7 +120,7 @@ class DerivedDataStrategy( return FrameworkPaths.createValidated( dynamicBasePath = dynamicBasePath, staticBasePath = staticBasePath, - architecture = architecture + architectures = architectures ) } @@ -158,7 +162,7 @@ class ManualSearchStrategy( private val project: Project, private val basePathToSearch: String? = null ) : FrameworkResolutionStrategy { - override fun resolvePaths(architecture: String): FrameworkPaths { + override fun resolvePaths(architectures: Set): FrameworkPaths { val dynamicValueSource = project.providers.of(ManualFrameworkPathSearchValueSource::class.java) { it.parameters.frameworkType.set(FrameworkType.DYNAMIC) @@ -177,7 +181,7 @@ class ManualSearchStrategy( return FrameworkPaths.createValidated( dynamicBasePath = dynamicValueSource.orNull, staticBasePath = staticValueSource.orNull, - architecture = architecture + architectures = architectures ) } } @@ -187,16 +191,17 @@ class FrameworkPathResolver( private val strategies: List = defaultStrategies(project) ) { fun resolvePaths( - architecture: String + architectures: Set ): FrameworkPaths { strategies.forEach { strategy -> try { - project.logger.lifecycle( + project.logger.info( "Attempt to resolve Sentry Cocoa framework paths using ${strategy::class.simpleName}" ) - val result = strategy.resolvePaths(architecture) + val result = strategy.resolvePaths(architectures) if (result != FrameworkPaths.NONE) { - project.logger.lifecycle("Found Sentry Cocoa framework paths using ${strategy::class.simpleName}") + val path = result.dynamic ?: result.static + project.logger.lifecycle("Found Sentry Cocoa framework path using ${strategy::class.simpleName} at $path") return result } else { project.logger.debug("Strategy ${strategy::class.simpleName} did not find valid paths") diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt index f6ed4162..55ee2649 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt @@ -19,10 +19,8 @@ abstract class ManualFrameworkPathSearchValueSource : override fun obtain(): String? { val frameworkType = parameters.frameworkType.get() val basePathToSearch = - parameters.basePathToSearch.convention( - "\"${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData\"" - ).get() - + parameters.basePathToSearch.orNull + ?: "\"${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData\"" return findFrameworkWithFindCommand(frameworkType, basePathToSearch) } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt index 9686a7f2..47345629 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt @@ -142,10 +142,10 @@ class CocoaFrameworkLinkerTest { // We don't really care what the strategy exactly does in this test // The strategies themselves are tested independently private class FakeStrategy : FrameworkResolutionStrategy { - override fun resolvePaths(architecture: String): FrameworkPaths { + override fun resolvePaths(architectures: Set): FrameworkPaths { return FrameworkPaths(static = staticPath, dynamic = dynamicPath) } } -val staticPath = "/path/to/static/Sentry.xcframework" -val dynamicPath = "/path/to/dynamic/Sentry-Dynamic.xcframework" +private const val staticPath = "/path/to/static/Sentry.xcframework" +private const val dynamicPath = "/path/to/dynamic/Sentry-Dynamic.xcframework" diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt index 0318dc06..8876beb6 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt @@ -24,12 +24,12 @@ class CustomPathStrategyTest { @ParameterizedTest(name = "should return static path for architecture {0}") @MethodSource("architectureMappingProvider") fun `should return static path when framework is Sentry xcframework`( - expectedArchitecture: String, + expectedArchitecture: Set, @TempDir dir: Path ) { val xcframeworkPath = dir.resolve("Sentry.xcframework") Files.createDirectory(xcframeworkPath) - val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture)) + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture.first())) val sut = fixture.getSut(xcframeworkPath.absolutePathString()) val paths = sut.resolvePaths(expectedArchitecture) @@ -41,12 +41,12 @@ class CustomPathStrategyTest { @ParameterizedTest(name = "should return dynamic path for architecture {0}") @MethodSource("architectureMappingProvider") fun `should return dynamic path when framework is Sentry xcframework`( - expectedArchitecture: String, + expectedArchitecture: Set, @TempDir dir: Path ) { val xcframeworkPath = dir.resolve("Sentry-Dynamic.xcframework") Files.createDirectory(xcframeworkPath) - val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture)) + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture.first())) val sut = fixture.getSut(xcframeworkPath.absolutePathString()) val paths = sut.resolvePaths(expectedArchitecture) @@ -58,7 +58,7 @@ class CustomPathStrategyTest { @Test fun `returns NONE when frameworkPath is null`() { val sut = fixture.getSut(null) - val result = sut.resolvePaths("doesnt matter") + val result = sut.resolvePaths(setOf("doesnt matter")) assertEquals(FrameworkPaths.NONE, result) } @@ -66,7 +66,7 @@ class CustomPathStrategyTest { @Test fun `returns NONE when frameworkPath is empty`() { val sut = fixture.getSut("") - val result = sut.resolvePaths("doesnt matter") + val result = sut.resolvePaths(setOf("doesnt matter")) assertEquals(FrameworkPaths.NONE, result) } @@ -76,14 +76,14 @@ class CustomPathStrategyTest { val xcframeworkPath = dir.resolve("Invalid.xcframework") val sut = fixture.getSut(xcframeworkPath.absolutePathString()) - val paths = sut.resolvePaths("doesnt matter") + val paths = sut.resolvePaths(setOf("doesnt matter")) assertEquals(FrameworkPaths.NONE, paths) } companion object { @JvmStatic - fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all.flatten() + fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all .map { Arguments.of(it) } .toList() } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt index dec1b32c..93f1a274 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt @@ -25,13 +25,15 @@ class DerivedDataStrategyTest { @ParameterizedTest(name = "resolve static path for architecture {0}") @MethodSource("architectureMappingProvider") fun `if xcodeproj is null and find xcode project successfully then resolve static path`( - expectedArchitecture: String, + expectedArchitecture: Set, @TempDir dir: Path ) { Files.createDirectory(dir.resolve("project.xcodeproj")) - val xcframeworkPath = dir.resolve("SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework") + val xcframeworkPath = + dir.resolve("SourcePackages/artifacts/sentry-cocoa/Sentry/Sentry.xcframework") val xcframeworkDirectory = Files.createDirectories(xcframeworkPath) - val archDirectory = Files.createDirectory(xcframeworkDirectory.resolve(expectedArchitecture)) + val archDirectory = + Files.createDirectory(xcframeworkDirectory.resolve(expectedArchitecture.first())) val sut = fixture.getSut(null, rootDirPath = dir.toFile().absolutePath) { _: String -> dir.toFile().absolutePath @@ -45,14 +47,15 @@ class DerivedDataStrategyTest { @ParameterizedTest(name = "should return dynamic path for architecture {0}") @MethodSource("architectureMappingProvider") fun `if xcodeproj is null and find xcode project successfully then resolve dynamic path`( - expectedArchitecture: String, + expectedArchitecture: Set, @TempDir dir: Path ) { Files.createDirectory(dir.resolve("project.xcodeproj")) val xcframeworkPath = dir.resolve("SourcePackages/artifacts/sentry-cocoa/Sentry-Dynamic/Sentry-Dynamic.xcframework") val xcframeworkDirectory = Files.createDirectories(xcframeworkPath) - val archDirectory = Files.createDirectory(xcframeworkDirectory.resolve(expectedArchitecture)) + val archDirectory = + Files.createDirectory(xcframeworkDirectory.resolve(expectedArchitecture.first())) val sut = fixture.getSut(null, rootDirPath = dir.toFile().absolutePath) { _: String -> dir.toFile().absolutePath @@ -71,7 +74,7 @@ class DerivedDataStrategyTest { dir.toFile().absolutePath } - val paths = sut.resolvePaths("doesnt matter") + val paths = sut.resolvePaths(setOf("doesnt matter")) assertEquals(FrameworkPaths.NONE, paths) } @@ -79,17 +82,20 @@ class DerivedDataStrategyTest { fun `if xcodeproj is not null and find xcode project is not successful then return NONE`( @TempDir dir: Path ) { - val sut = fixture.getSut("some invalid path", rootDirPath = dir.toFile().absolutePath) { _: String -> + val sut = fixture.getSut( + "some invalid path", + rootDirPath = dir.toFile().absolutePath + ) { _: String -> dir.toFile().absolutePath } - val paths = sut.resolvePaths("doesnt matter") + val paths = sut.resolvePaths(setOf("doesnt matter")) assertEquals(FrameworkPaths.NONE, paths) } companion object { @JvmStatic - fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all.flatten() + fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all .map { Arguments.of(it) } .toList() } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt index 2f510cda..e32839ef 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt @@ -29,7 +29,7 @@ class FrameworkPathResolverTest { val strategy2 = mockk() val sut = fixture.getSut(listOf(strategy1, strategy2)) - sut.resolvePaths("test") + sut.resolvePaths(setOf("test")) verify(exactly = 0) { strategy2.resolvePaths(any()) } } @@ -41,7 +41,7 @@ class FrameworkPathResolverTest { every { mockStrategy3.resolvePaths(any()) } returns FrameworkPaths(static = "valid") val sut = fixture.getSut(listOf(mockStrategy1, mockStrategy2, mockStrategy3)) - val result = sut.resolvePaths("test") + val result = sut.resolvePaths(setOf("test")) assertEquals("valid", result.static) } @@ -53,7 +53,7 @@ class FrameworkPathResolverTest { val sut = fixture.getSut(listOf(mockStrategy1, mockStrategy2)) assertThrows { - sut.resolvePaths("test") + sut.resolvePaths(setOf("test")) } verifyOrder { @@ -67,7 +67,7 @@ class FrameworkPathResolverTest { val sut = fixture.getSut(emptyList()) assertThrows { - sut.resolvePaths("test") + sut.resolvePaths(setOf("test")) } } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt index b6f2ae7c..347c3e1c 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt @@ -26,11 +26,11 @@ class ManualSearchStrategyTest { @ParameterizedTest(name = "should return static path for architecture {0}") @MethodSource("architectureMappingProvider") fun `should return static path when framework exists`( - expectedArchitecture: String, + expectedArchitecture: Set, @TempDir dir: Path ) { val xcframeworkPath = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() - val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture)) + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture.first())) val sut = fixture.getSut(dir.absolutePathString()) val paths = sut.resolvePaths(expectedArchitecture) @@ -42,11 +42,11 @@ class ManualSearchStrategyTest { @ParameterizedTest(name = "should return dynamic path for architecture {0}") @MethodSource("architectureMappingProvider") fun `should return dynamic path when framework exists`( - expectedArchitecture: String, + expectedArchitecture: Set, @TempDir dir: Path ) { val xcframeworkPath = dir.resolve("somewhere/hidden/Sentry-Dynamic.xcframework").createDirectories() - val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture)) + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture.first())) val sut = fixture.getSut(dir.absolutePathString()) val paths = sut.resolvePaths(expectedArchitecture) @@ -58,18 +58,18 @@ class ManualSearchStrategyTest { @ParameterizedTest(name = "should return most recently used path for architecture {0}") @MethodSource("architectureMappingProvider") fun `should return most recently used path when multiple framework exists`( - expectedArchitecture: String, + expectedArchitecture: Set, @TempDir dir: Path ) { val xcframeworkPath1 = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() - Files.createDirectory(xcframeworkPath1.resolve(expectedArchitecture)) + Files.createDirectory(xcframeworkPath1.resolve(expectedArchitecture.first())) // sleep so both directories have different timestamps. // This needs to be in seconds since the captured timestamps are not precise enough sleep(1000) val xcframeworkPath2 = dir.resolve("more/recent/Sentry.xcframework").createDirectories() - val archDirectory2 = Files.createDirectory(xcframeworkPath2.resolve(expectedArchitecture)) + val archDirectory2 = Files.createDirectory(xcframeworkPath2.resolve(expectedArchitecture.first())) val sut = fixture.getSut(dir.absolutePathString()) val paths = sut.resolvePaths(expectedArchitecture) @@ -81,14 +81,14 @@ class ManualSearchStrategyTest { @Test fun `returns NONE when no framework found`() { val sut = fixture.getSut("some/random/path") - val result = sut.resolvePaths("doesnt matter") + val result = sut.resolvePaths(setOf("doesnt matter")) assertEquals(FrameworkPaths.NONE, result) } companion object { @JvmStatic - fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all.flatten() + fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all .map { Arguments.of(it) } .toList() } From 3b4d218680cb3a2939d7cf8452b89470d392485f Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 6 Feb 2025 13:36:59 +0100 Subject: [PATCH 31/34] add todo --- .../sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index 75646e0c..d77c26df 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -162,6 +162,8 @@ class ManualSearchStrategy( private val project: Project, private val basePathToSearch: String? = null ) : FrameworkResolutionStrategy { + // TODO: currently the search doesnt differentiate between Cocoa versions + // we can improve this by checking the info.plist and prefer the ones that are the version we are looking for override fun resolvePaths(architectures: Set): FrameworkPaths { val dynamicValueSource = project.providers.of(ManualFrameworkPathSearchValueSource::class.java) { From 85a8b56e281c9cbe6e64fdf213af75b527430262 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 10 Feb 2025 16:32:36 +0100 Subject: [PATCH 32/34] review feedback --- .../multiplatform/gradle/FrameworkLinker.kt | 5 --- .../gradle/FrameworkPathResolver.kt | 41 +++++++++++++------ .../ManualFrameworkPathSearchValueSource.kt | 23 +++++++---- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt index 8fbb04cf..135f077e 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt @@ -27,11 +27,6 @@ class FrameworkLinker( } private fun chooseTestPath(dynamic: String?, static: String?) = when { - dynamic != null && static != null -> { - logger.debug("Both framework types available, preferring dynamic for tests") - dynamic - } - dynamic != null -> dynamic static != null -> static else -> throw FrameworkLinkingException("No valid framework path found for tests") diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index d77c26df..6f4879f4 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -2,6 +2,12 @@ package io.sentry.kotlin.multiplatform.gradle import org.gradle.api.Project import java.io.File +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import kotlin.io.path.absolutePathString enum class FrameworkType { STATIC, @@ -106,7 +112,7 @@ class DerivedDataStrategy( override fun resolvePaths(architectures: Set): FrameworkPaths { val xcodeprojSetByUser = linker.xcodeprojPath.orNull?.takeIf { it.isNotEmpty() } - val foundXcodeproj = xcodeprojSetByUser ?: findXcodeprojFile(project.rootDir)?.absolutePath + val foundXcodeproj = xcodeprojSetByUser ?: findXcodeprojFilePath(project.rootDir) if (foundXcodeproj == null) { return FrameworkPaths.NONE } @@ -128,23 +134,32 @@ class DerivedDataStrategy( * Searches for a xcodeproj starting from the root directory. This function will only work for * monorepos and if it is not, the user needs to provide the [LinkerExtension.xcodeprojPath]. */ - private fun findXcodeprojFile(startingDir: File): File? { - val ignoredDirectories = listOf("build", "DerivedData") + private fun findXcodeprojFilePath(startingDir: File): String? { + val ignoredDirectories = setOf("build", "DerivedData") + var foundXcodeprojPath: String? = null - fun searchDirectory(directory: File): File? { - val files = directory.listFiles() ?: return null - - return files.firstNotNullOfOrNull { file -> - when { - file.name in ignoredDirectories -> null - file.extension == "xcodeproj" -> file - file.isDirectory -> searchDirectory(file) - else -> null + Files.walkFileTree( + startingDir.toPath(), + object : SimpleFileVisitor() { + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + return when { + // Check if current directory is a xcodeproj before checking ignored dirs + dir.toString().endsWith(".xcodeproj") -> { + foundXcodeprojPath = dir.absolutePathString() + FileVisitResult.TERMINATE + } + ignoredDirectories.contains(dir.fileName.toString()) -> FileVisitResult.SKIP_SUBTREE + else -> FileVisitResult.CONTINUE + } } } + ) + + if (foundXcodeprojPath != null) { + project.logger.info("Found xcodeproj through file walking at path: $foundXcodeprojPath") } - return searchDirectory(startingDir) + return foundXcodeprojPath } } diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt index 55ee2649..09fddc17 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt @@ -32,11 +32,12 @@ abstract class ManualFrameworkPathSearchValueSource : frameworkType: FrameworkType, basePathToSearch: String ): String? { - val output = ByteArrayOutputStream() + val stdOutput = ByteArrayOutputStream() + val errOutput = ByteArrayOutputStream() val xcFrameworkName = if (frameworkType == FrameworkType.STATIC) "Sentry.xcframework" else "Sentry-Dynamic.xcframework" - execOperations.exec { + val execResult = execOperations.exec { it.commandLine( "bash", "-c", @@ -46,16 +47,20 @@ abstract class ManualFrameworkPathSearchValueSource : "sort -nr | " + "cut -d' ' -f2-" ) - it.standardOutput = output - it.isIgnoreExitValue = true + it.standardOutput = stdOutput + it.errorOutput = errOutput } - val stringOutput = output.toString("UTF-8") - return if (stringOutput.lineSequence().firstOrNull().isNullOrEmpty()) { - null + val stringOutput = stdOutput.toString("UTF-8") + return if (execResult.exitValue == 0) { + if (stringOutput.lineSequence().firstOrNull().isNullOrEmpty()) { + null + } else { + stringOutput.lineSequence() + .first() + } } else { - stringOutput.lineSequence() - .first() + null } } } From 15f1eb7d8e42baf334678e07d96fd973b2d7fe0e Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 10 Feb 2025 16:39:37 +0100 Subject: [PATCH 33/34] update formatting --- .../multiplatform/gradle/FrameworkPathResolver.kt | 12 +++++++++--- .../gradle/ManualFrameworkPathSearchValueSource.kt | 5 +++++ .../kotlin/multiplatform/gradle/SentryPlugin.kt | 7 +++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index 6f4879f4..9e865e18 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -86,7 +86,7 @@ class CustomPathStrategy( if (linker.frameworkPath.orNull != null && result == FrameworkPaths.NONE) { project.logger.warn( "Custom framework path has been set manually but could not be found. " + - "Trying to resolve framework paths using other strategies." + "Trying to resolve framework paths using other strategies." ) } return result @@ -141,13 +141,17 @@ class DerivedDataStrategy( Files.walkFileTree( startingDir.toPath(), object : SimpleFileVisitor() { - override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + override fun preVisitDirectory( + dir: Path, + attrs: BasicFileAttributes + ): FileVisitResult { return when { // Check if current directory is a xcodeproj before checking ignored dirs dir.toString().endsWith(".xcodeproj") -> { foundXcodeprojPath = dir.absolutePathString() FileVisitResult.TERMINATE } + ignoredDirectories.contains(dir.fileName.toString()) -> FileVisitResult.SKIP_SUBTREE else -> FileVisitResult.CONTINUE } @@ -218,7 +222,9 @@ class FrameworkPathResolver( val result = strategy.resolvePaths(architectures) if (result != FrameworkPaths.NONE) { val path = result.dynamic ?: result.static - project.logger.lifecycle("Found Sentry Cocoa framework path using ${strategy::class.simpleName} at $path") + project.logger.lifecycle( + "Found Sentry Cocoa framework path using ${strategy::class.simpleName} at $path" + ) return result } else { project.logger.debug("Strategy ${strategy::class.simpleName} did not find valid paths") diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt index 09fddc17..f353c73c 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt @@ -1,5 +1,6 @@ package io.sentry.kotlin.multiplatform.gradle +import io.sentry.kotlin.multiplatform.gradle.SentryPlugin.Companion.logger import org.gradle.api.provider.Property import org.gradle.api.provider.ValueSource import org.gradle.api.provider.ValueSourceParameters @@ -60,6 +61,10 @@ abstract class ManualFrameworkPathSearchValueSource : .first() } } else { + logger.warn( + "Manual search failed to find $xcFrameworkName in $basePathToSearch. " + + "Error output: ${errOutput.toString(Charsets.UTF_8)}" + ) null } } 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 552a72b9..907e9c0d 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 @@ -8,6 +8,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin import org.jetbrains.kotlin.konan.target.HostManager +import org.slf4j.LoggerFactory internal const val SENTRY_EXTENSION_NAME = "sentryKmp" internal const val LINKER_EXTENSION_NAME = "linker" @@ -72,6 +73,12 @@ class SentryPlugin : Plugin { ).configure(appleTargets) } } + + companion object { + internal val logger by lazy { + LoggerFactory.getLogger(SentryPlugin::class.java) + } + } } internal fun Project.installSentryForKmp( From c0fbf16f4f250adfef7f901d753e74a6cfb18d1b Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 10 Feb 2025 16:44:44 +0100 Subject: [PATCH 34/34] formatting --- .../sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt index 9e865e18..ec405e7c 100644 --- a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -86,7 +86,7 @@ class CustomPathStrategy( if (linker.frameworkPath.orNull != null && result == FrameworkPaths.NONE) { project.logger.warn( "Custom framework path has been set manually but could not be found. " + - "Trying to resolve framework paths using other strategies." + "Trying to resolve framework paths using other strategies." ) } return result