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)) 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 new file mode 100644 index 00000000..ee47e83f --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinker.kt @@ -0,0 +1,95 @@ +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.plugin.mpp.KotlinNativeTarget + +/** + * 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 logger: Logger, + private val pathResolver: FrameworkPathResolver, + private val binaryLinker: FrameworkLinker +) { + fun configure(appleTargets: List) { + appleTargets.forEach { target -> + try { + 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) + } + } + } + + private fun processTarget(target: KotlinNativeTarget) { + val architectures = + target.toSentryFrameworkArchitecture().takeIf { it.isNotEmpty() } ?: run { + logger.warn("Skipping target ${target.name}: Unsupported architecture") + return + } + val paths: FrameworkPaths = pathResolver.resolvePaths(architectures) + binaryLinker.configureBinaries(target.binaries, paths.dynamic, paths.static) + } +} + +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 -> [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" -> 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/DerivedDataPathValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathValueSource.kt index 12dbeafb..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 @@ -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,9 @@ 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") + 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..135f077e --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkLinker.kt @@ -0,0 +1,54 @@ +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 -> 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 new file mode 100644 index 00000000..ec405e7c --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolver.kt @@ -0,0 +1,277 @@ +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, + DYNAMIC +} + +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 { + val dynamicPath = dynamicBasePath?.let { basePath -> + architectures.map { arch -> "$basePath/$arch" }.firstOrNull { pathExists(it) } + } + + val staticPath = staticBasePath?.let { basePath -> + architectures.map { arch -> "$basePath/$arch" }.firstOrNull { 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 + } + } + } +} + +interface FrameworkResolutionStrategy { + fun resolvePaths(architectures: Set): 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 +) : 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(architectures: Set): FrameworkPaths { + val result = 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 -> 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 + } +} + +/** + * 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, + 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(architectures: Set): FrameworkPaths { + val xcodeprojSetByUser = linker.xcodeprojPath.orNull?.takeIf { it.isNotEmpty() } + val foundXcodeproj = xcodeprojSetByUser ?: findXcodeprojFilePath(project.rootDir) + if (foundXcodeproj == null) { + return FrameworkPaths.NONE + } + + val derivedDataPath = derivedDataProvider(foundXcodeproj) ?: 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 + ) + } + + /** + * 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 findXcodeprojFilePath(startingDir: File): String? { + val ignoredDirectories = setOf("build", "DerivedData") + var foundXcodeprojPath: String? = 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 foundXcodeprojPath + } +} + +/** + * 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, + 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) { + it.parameters.frameworkType.set(FrameworkType.DYNAMIC) + if (basePathToSearch != null) { + it.parameters.basePathToSearch.set(basePathToSearch) + } + } + val staticValueSource = + project.providers.of(ManualFrameworkPathSearchValueSource::class.java) { + it.parameters.frameworkType.set(FrameworkType.STATIC) + if (basePathToSearch != null) { + it.parameters.basePathToSearch.set(basePathToSearch) + } + } + + 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 { + strategies.forEach { strategy -> + try { + project.logger.info( + "Attempt to resolve Sentry Cocoa framework paths using ${strategy::class.simpleName}" + ) + 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" + ) + return result + } else { + project.logger.debug("Strategy ${strategy::class.simpleName} did not find valid paths") + } + } catch (e: FrameworkLinkingException) { + project.logger.warn( + "Strategy ${strategy::class.simpleName} failed due to error: ${e.message}" + ) + } + } + + // 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. + * + * 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) + // TODO: add DownloadStrategy -> downloads the framework and stores it in build dir + // this is especially useful for users who dont have a monorepo setup + ) + } + } +} 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..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 @@ -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` 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/ManualFrameworkPathSearchValueSource.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt new file mode 100644 index 00000000..f353c73c --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/ManualFrameworkPathSearchValueSource.kt @@ -0,0 +1,71 @@ +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 +import org.gradle.process.ExecOperations +import java.io.ByteArrayOutputStream + +abstract class ManualFrameworkPathSearchValueSource : + ValueSource { + interface Parameters : ValueSourceParameters { + val frameworkType: Property + val basePathToSearch: Property + } + + @get:javax.inject.Inject + abstract val execOperations: ExecOperations + + override fun obtain(): String? { + val frameworkType = parameters.frameworkType.get() + val basePathToSearch = + parameters.basePathToSearch.orNull + ?: "\"${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData\"" + return findFrameworkWithFindCommand(frameworkType, basePathToSearch) + } + + /** + * 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( + frameworkType: FrameworkType, + basePathToSearch: String + ): String? { + val stdOutput = ByteArrayOutputStream() + val errOutput = ByteArrayOutputStream() + + val xcFrameworkName = + if (frameworkType == FrameworkType.STATIC) "Sentry.xcframework" else "Sentry-Dynamic.xcframework" + val execResult = execOperations.exec { + it.commandLine( + "bash", + "-c", + "find $basePathToSearch " + + "-name $xcFrameworkName " + + "-exec stat -f \"%m %N\" {} \\; | " + + "sort -nr | " + + "cut -d' ' -f2-" + ) + it.standardOutput = stdOutput + it.errorOutput = errOutput + } + + val stringOutput = stdOutput.toString("UTF-8") + return if (execResult.exitValue == 0) { + if (stringOutput.lineSequence().firstOrNull().isNullOrEmpty()) { + null + } else { + stringOutput.lineSequence() + .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 edf96df4..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 @@ -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" @@ -43,27 +39,41 @@ 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 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) - } + 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) + } + } + companion object { internal val logger by lazy { LoggerFactory.getLogger(SentryPlugin::class.java) @@ -120,127 +130,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 frameworkArchitecture = target.toSentryFrameworkArchitecture() ?: run { - logger.warn("Skipping target ${target.name} - unsupported architecture.") - return@all - } - - val dynamicFrameworkPath: String - val staticFrameworkPath: String - - 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" - } - - val dynamicFrameworkExists = File(dynamicFrameworkPath).exists() - val staticFrameworkExists = File(staticFrameworkPath).exists() - - if (!dynamicFrameworkExists && !staticFrameworkExists) { - throw GradleException( - "Sentry Cocoa Framework not found at $dynamicFrameworkPath or $staticFrameworkPath" - ) - } - - 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") - } - - if (binary is Framework) { - val finalFrameworkPath = when { - binary.isStatic && staticFrameworkExists -> staticFrameworkPath - !binary.isStatic && dynamicFrameworkExists -> 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 the architecture name that is found inside - * Sentry's framework directory. - */ -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 - } -} - -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/CocoaFrameworkLinkerTest.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt new file mode 100644 index 00000000..47345629 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CocoaFrameworkLinkerTest.kt @@ -0,0 +1,151 @@ +import io.sentry.kotlin.multiplatform.gradle.CocoaFrameworkLinker +import io.sentry.kotlin.multiplatform.gradle.FrameworkLinker +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 +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 + +class CocoaFrameworkLinkerTest { + private lateinit var fixture: Fixture + + @BeforeEach + fun setUp() { + fixture = Fixture() + } + + @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() + 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() + 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() + 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(): CocoaFrameworkLinker { + return CocoaFrameworkLinker( + project.logger, + FrameworkPathResolver(project, strategies = listOf(FakeStrategy())), + FrameworkLinker(project.logger) + ) + } + } +} + +// 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(architectures: Set): FrameworkPaths { + return FrameworkPaths(static = staticPath, dynamic = dynamicPath) + } +} + +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 new file mode 100644 index 00000000..8876beb6 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/CustomPathStrategyTest.kt @@ -0,0 +1,106 @@ +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: Set, + @TempDir dir: Path + ) { + val xcframeworkPath = dir.resolve("Sentry.xcframework") + Files.createDirectory(xcframeworkPath) + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture.first())) + + 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: Set, + @TempDir dir: Path + ) { + val xcframeworkPath = dir.resolve("Sentry-Dynamic.xcframework") + Files.createDirectory(xcframeworkPath) + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture.first())) + + 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(setOf("doesnt matter")) + + assertEquals(FrameworkPaths.NONE, result) + } + + @Test + fun `returns NONE when frameworkPath is empty`() { + val sut = fixture.getSut("") + val result = sut.resolvePaths(setOf("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(setOf("doesnt matter")) + + assertEquals(FrameworkPaths.NONE, paths) + } + + companion object { + @JvmStatic + fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all + .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) + } + } +} 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..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,18 +3,16 @@ 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 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 { - private lateinit var valueSource: DerivedDataPathValueSource private lateinit var execOperations: ExecOperations private lateinit var parameters: DerivedDataPathValueSource.Parameters @@ -72,7 +70,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 { @@ -102,8 +100,6 @@ class DerivedDataPathTest { } } - assertThrows { - valueSource.obtain() - } + assertNull(valueSource.obtain()) } } 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..93f1a274 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataStrategyTest.kt @@ -0,0 +1,124 @@ +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.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 = "resolve static path for architecture {0}") + @MethodSource("architectureMappingProvider") + fun `if xcodeproj is null and find xcode project successfully then resolve static path`( + expectedArchitecture: Set, + @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.first())) + + 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: 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.first())) + + 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) + } + + @Test + fun `if xcodeproj is null and find xcode project is not successful then return NONE`( + @TempDir dir: Path + ) { + val sut = fixture.getSut(null, rootDirPath = dir.toFile().absolutePath) { _: String -> + dir.toFile().absolutePath + } + + val paths = sut.resolvePaths(setOf("doesnt matter")) + assertEquals(FrameworkPaths.NONE, paths) + } + + @Test + 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 -> + dir.toFile().absolutePath + } + + val paths = sut.resolvePaths(setOf("doesnt matter")) + assertEquals(FrameworkPaths.NONE, paths) + } + + companion object { + @JvmStatic + fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all + .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) + } + } +} 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..e32839ef --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/FrameworkPathResolverTest.kt @@ -0,0 +1,86 @@ +package io.sentry.kotlin.multiplatform.gradle + +import io.mockk.every +import io.mockk.mockk +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.assertThrows + +class FrameworkPathResolverTest { + private val mockStrategy1 = mockk() + private val mockStrategy2 = mockk() + private val mockStrategy3 = mockk() + private lateinit var fixture: Fixture + + @BeforeEach + fun setUp() { + fixture = Fixture() + } + + @Test + fun `does not execute subsequent strategies after first success`() { + val strategy1 = mockk { + every { resolvePaths(any()) } returns FrameworkPaths(dynamic = "dyn") + } + val strategy2 = mockk() + + val sut = fixture.getSut(listOf(strategy1, strategy2)) + sut.resolvePaths(setOf("test")) + + verify(exactly = 0) { strategy2.resolvePaths(any()) } + } + + @Test + fun `proceeds past multiple failing strategies and returns first success`() { + every { mockStrategy1.resolvePaths(any()) } throws FrameworkLinkingException("") + 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("test")) + + 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("test")) + } + + verifyOrder { + mockStrategy1.resolvePaths(any()) + mockStrategy2.resolvePaths(any()) + } + } + + @Test + fun `throws when no strategies provided`() { + val sut = fixture.getSut(emptyList()) + + assertThrows { + sut.resolvePaths(setOf("test")) + } + } + + 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") + } + + return FrameworkPathResolver(project, strategies) + } + } +} 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..347c3e1c --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/ManualSearchStrategyTest.kt @@ -0,0 +1,108 @@ +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.lang.Thread.sleep +import java.nio.file.Files +import java.nio.file.Path +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: Set, + @TempDir dir: Path + ) { + val xcframeworkPath = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture.first())) + + 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: Set, + @TempDir dir: Path + ) { + val xcframeworkPath = dir.resolve("somewhere/hidden/Sentry-Dynamic.xcframework").createDirectories() + val archDirectory = Files.createDirectory(xcframeworkPath.resolve(expectedArchitecture.first())) + + 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: Set, + @TempDir dir: Path + ) { + val xcframeworkPath1 = dir.resolve("somewhere/hidden/Sentry.xcframework").createDirectories() + 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.first())) + + 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(setOf("doesnt matter")) + + assertEquals(FrameworkPaths.NONE, result) + } + + companion object { + @JvmStatic + fun architectureMappingProvider() = SentryCocoaFrameworkArchitectures.all + .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) + } + } +} 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..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 @@ -2,43 +2,187 @@ package io.sentry.kotlin.multiplatform.gradle import io.mockk.every import io.mockk.mockk +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.junit.jupiter.api.Assertions.assertEquals +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 +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 architecture name compatibility with Cocoa Version {0} in static framework") + @MethodSource("cocoaVersions") + fun `finds arch folders in static framework`( + 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, isStatic = true) + 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 } + }" + } + } + } + + @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 } + }" + } + } + } + + @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() + tempDir.deleteOnExit() + + 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( + input, + targetFile.toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + } + + // Unzip + 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 } } 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..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 @@ -1,20 +1,20 @@ 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.plugin.mpp.NativeBuildType +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.io.TempDir +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import java.io.File class SentryPluginTest { @Test @@ -154,135 +154,115 @@ class SentryPluginTest { assertTrue(commonMainConfiguration!!.dependencies.contains(sentryDependency)) } - @Test - fun `install Sentry pod if not already exists`() { + @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") - project.pluginManager.apply("org.jetbrains.kotlin.native.cocoapods") - project.installSentryForCocoapods(project.extensions.getByName("cocoapods") as CocoapodsAutoInstallExtension) + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kmpExtension.apply { + when (unsupportedTarget) { + "wasm" -> wasmJs() + "js" -> js() + "mingw" -> mingwX64() + "linux" -> linuxX64() + "androidNative" -> androidNativeArm64() + } + } - project.afterEvaluate { - val cocoapodsExtension = project.extensions.findByType(CocoapodsExtension::class.java) - val sentryPod = cocoapodsExtension?.pods?.findByName("Sentry") - assertTrue(sentryPod != null) - assertTrue(sentryPod!!.linkOnly) + assertThrows { + project.installSentryForKmp(project.extensions.getByName("commonMain") as SourceSetAutoInstallExtension) } } @Test - fun `do not install Sentry pod when cocoapods plugin when Sentry cocoapods configuration exists`() { + fun `install Sentry pod if not already exists`() { val project = ProjectBuilder.builder().build() - project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") project.pluginManager.apply("org.jetbrains.kotlin.multiplatform") + project.pluginManager.apply("io.sentry.kotlin.multiplatform.gradle") 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" } - } + val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) as ExtensionAware + val cocoapodsExtension = kmpExtension.extensions.getByType(CocoapodsExtension::class.java) + val sentryPod = cocoapodsExtension.pods.findByName("Sentry") - // 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") - } + // 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()) + assertTrue(cocoapodsExtension.pods.getByName("Sentry").linkOnly) + assertEquals(cocoapodsExtension.pods.getByName("Sentry").extraOpts, listOf("-compiler-option", "-fmodules")) } @Test - fun `configureLinkingOptions sets up linker options for apple targets`(@TempDir tempDir: File) { + 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") - 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) - - project.configureLinkingOptions(linkerExtension) + 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") - kmpExtension.apply { - val frameworks = appleTargets().map { - it.binaries.findFramework(NativeBuildType.DEBUG) - } - frameworks.forEach { - assertTrue(it?.linkerOpts?.contains("-F${file.absolutePath}") ?: false) - } - } - } + cocoapodsAutoInstallExtension.sentryCocoaVersion.set("10000.0.0") - @Test - fun `findXcodeprojFile returns xcodeproj file when it exists`(@TempDir tempDir: File) { - val xcodeprojFile = File(tempDir, "TestProject.xcodeproj") - xcodeprojFile.mkdir() + // Check that it does not exist + assertNull(sentryPod) - val result = findXcodeprojFile(tempDir) + val plugin = project.plugins.getPlugin(SentryPlugin::class.java) + plugin.executeConfiguration(project) - assertEquals(xcodeprojFile, result) + // Check that it now exists + assertEquals(cocoapodsExtension.pods.getByName("Sentry").version, "10000.0.0") } @Test - fun `findXcodeprojFile returns null when no xcodeproj file exists`(@TempDir tempDir: File) { - val result = findXcodeprojFile(tempDir) - - assertNull(result) - } + 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") - @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 kmpExtension = project.extensions.findByName("kotlin") + (kmpExtension as ExtensionAware).extensions.configure(CocoapodsExtension::class.java) { cocoapods -> + cocoapods.pod("Sentry") { version = "custom version" } + } - val result = findXcodeprojFile(tempDir) + val plugin = project.plugins.getPlugin(SentryPlugin::class.java) + plugin.executeConfiguration(project) - assertEquals(xcodeprojFile, result) + val cocoapodsExtension = kmpExtension.extensions.getByType(CocoapodsExtension::class.java) + assertEquals(cocoapodsExtension.pods.getByName("Sentry").version, "custom version") } @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) - } + 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") - @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 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 result = findXcodeprojFile(tempDir) + val plugin = project.plugins.getPlugin(SentryPlugin::class.java) + plugin.executeConfiguration(project, hostIsMac = false) - assertEquals(xcodeprojFile1, result) + val cocoapodsExtension = (kmpExtension as ExtensionAware).extensions.getByType(CocoapodsExtension::class.java) + assertNull(cocoapodsExtension.pods.findByName("Sentry")) } }