diff --git a/.craft.yml b/.craft.yml index 56039d39..900ba86a 100644 --- a/.craft.yml +++ b/.craft.yml @@ -2,6 +2,7 @@ minVersion: 1.2.1 changelogPolicy: auto targets: - name: maven + id: kmp gradleCliPath: ./gradlew mavenCliPath: scripts/mvnw mavenSettingsPath: scripts/settings.xml @@ -14,8 +15,19 @@ targets: kmp: rootDistDirRegex: /^(?!.*(?:jvm|android|ios|watchos|tvos|macos)).*$/ appleDistDirRegex: /(ios|watchos|tvos|macos)/ + excludeNames: /sentry-kotlin-multiplatform-gradle-plugin.*$/ + - name: maven + id: plugin + gradleCliPath: ./gradlew + mavenCliPath: scripts/mvnw + mavenSettingsPath: scripts/settings.xml + mavenRepoId: ossrh + mavenRepoUrl: https://oss.sonatype.org/service/local/staging/deploy/maven2/ + android: false + includeNames: /sentry-kotlin-multiplatform-gradle-plugin.*$/ - name: github - name: registry sdks: maven:io.sentry:sentry-kotlin-multiplatform: +# maven:io.sentry:sentry-kotlin-multiplatform-gradle-plugin: diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index ee498dd0..47d438b6 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -26,4 +26,5 @@ jobs: distribution: temurin - name: Analyze - run: ./gradlew apiCheck detekt \ No newline at end of file + run: + ./gradlew apiCheck detekt spotlessCheck \ No newline at end of file diff --git a/.github/workflows/kotlin-multiplatform-gradle-plugin.yml b/.github/workflows/kotlin-multiplatform-gradle-plugin.yml new file mode 100644 index 00000000..719a3a58 --- /dev/null +++ b/.github/workflows/kotlin-multiplatform-gradle-plugin.yml @@ -0,0 +1,75 @@ +name: "Plugin: sentry-kotlin-multiplatform" +on: + push: + branches: + - main + - release/** + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: macos-latest-xlarge + defaults: + run: + working-directory: sentry-kotlin-multiplatform-gradle-plugin + + steps: + - uses: actions/checkout@v4 + + - name: JDK setup + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: temurin + + - name: Cached Konan + uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 #v3 + with: + path: ~/.konan + key: ${{ runner.os }}-konan-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-konan- + + - name: Cached Gradle + uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a + + - name: Build + run: | + ./gradlew build + ./gradlew koverXmlReport + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # pin@v4 + with: + name: sentry-kotlin-multiplatform-gradle-plugin + token: ${{ secrets.CODECOV_TOKEN }} + + archive-distribution: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: JDK setup + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: temurin + + - name: Cached Gradle + uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a + + - name: DistZip + run: | + cd sentry-kotlin-multiplatform-gradle-plugin && ./gradlew distZip + + - name: Archive packages + uses: actions/upload-artifact@v3 + with: + name: ${{ github.sha }} + if-no-files-found: error + path: | + ./*/build/distributions/*.zip diff --git a/.github/workflows/kotlin-multiplatform.yml b/.github/workflows/kotlin-multiplatform.yml index 7bc2a9d5..6ed25778 100644 --- a/.github/workflows/kotlin-multiplatform.yml +++ b/.github/workflows/kotlin-multiplatform.yml @@ -95,6 +95,25 @@ jobs: name: sentry-kotlin-multiplatform token: ${{ secrets.CODECOV_TOKEN }} + test-samples: + runs-on: macos-latest-xlarge + + steps: + - uses: actions/checkout@v4 + + - name: JDK setup + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: temurin + + - name: Cached Gradle + uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a + + - name: Test samples + run: | + ./gradlew allTests -p sentry-samples + archive-distribution: runs-on: macos-latest-xlarge diff --git a/.run/iosApp-spm.run.xml b/.run/iosApp-spm.run.xml index 6295acd9..70ee7b23 100644 --- a/.run/iosApp-spm.run.xml +++ b/.run/iosApp-spm.run.xml @@ -1,5 +1,5 @@ - + diff --git a/CHANGELOG.md b/CHANGELOG.md index 982e9528..41a05801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Features + +- New Sentry KMP Gradle plugin ([#230](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/230)) + - Install via `plugins { id("io.sentry.kotlin.multiplatform.gradle") version "{version}" }` + - Enables auto installing of the KMP SDK to commonMain (if all targets are supported) + - Enables auto installing of the required Sentry Cocoa SDK with Cocoapods (if Cocoapods plugin is enabled) + - Configures linking for SPM (needed if you want to compile a dynamic framework) + - Configure via the `sentryKmp` configuration block in your build file + ### Dependencies - Bump Kotlin version from v1.9.21 to v1.9.23 ([#250](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/250)) diff --git a/build.gradle.kts b/build.gradle.kts index 93d3f0d5..7a15ad86 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -72,10 +72,12 @@ spotless { kotlin { target("**/*.kt") + targetExclude("**/generated/**/*.kt") ktlint() } kotlinGradle { target("**/*.kts") + targetExclude("**/generated/**/*.kts") ktlint() } } @@ -115,3 +117,8 @@ val detektProjectBaseline by tasks.registering(io.gitlab.arturbosch.detekt.Detek include("**/*.kt") detektExcludes() } + +// Configure tasks so it is also run for the plugin +tasks.getByName("detekt") { + gradle.includedBuild("sentry-kotlin-multiplatform-gradle-plugin").task(":detekt") +} diff --git a/scripts/build-jvm.sh b/scripts/build-jvm.sh index 31ec07a0..37ab2138 100755 --- a/scripts/build-jvm.sh +++ b/scripts/build-jvm.sh @@ -6,7 +6,7 @@ if [ -z "$1" ]; then fi PROJECT_NAME="$1" -./gr + ./gradlew "testDebugUnitTest" \ "testReleaseUnitTest" \ "publishAndroidReleasePublicationToMavenLocal" \ diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 0f68b3c4..4d11d697 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -12,7 +12,9 @@ OLD_VERSION="$1" NEW_VERSION="$2" GRADLE_FILEPATH="gradle.properties" +PLUGIN_GRADLE_FILEPATH="sentry-kotlin-multiplatform-gradle-plugin/gradle.properties" # Replace `versionName` with the given version VERSION_NAME_PATTERN="versionName" perl -pi -e "s/$VERSION_NAME_PATTERN=.*$/$VERSION_NAME_PATTERN=$NEW_VERSION/g" $GRADLE_FILEPATH +perl -pi -e "s/$VERSION_NAME_PATTERN=.*$/$VERSION_NAME_PATTERN=$NEW_VERSION/g" $PLUGIN_GRADLE_FILEPATH diff --git a/sentry-kotlin-multiplatform-gradle-plugin/build.gradle.kts b/sentry-kotlin-multiplatform-gradle-plugin/build.gradle.kts new file mode 100644 index 00000000..6789a439 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/build.gradle.kts @@ -0,0 +1,100 @@ +import com.vanniktech.maven.publish.MavenPublishPluginExtension +import io.gitlab.arturbosch.detekt.Detekt +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.kotlin) + alias(libs.plugins.detekt) + `java-gradle-plugin` + alias(libs.plugins.vanniktech.publish) + id("distribution") + alias(libs.plugins.buildConfig) + alias(libs.plugins.kover) +} + +version = property("versionName").toString() + +group = property("group").toString() + +dependencies { + compileOnly(kotlin("stdlib")) + compileOnly(gradleApi()) + compileOnly(kotlin("gradle-plugin")) + + testImplementation(kotlin("gradle-plugin")) + testImplementation(libs.junit) + testImplementation(libs.junit.params) + testImplementation(libs.mockk) +} + +tasks.test { + useJUnitPlatform() +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } + +gradlePlugin { + plugins { + create(property("id").toString()) { + id = property("id").toString() + implementationClass = property("implementationClass").toString() + } + } +} + +val publish = extensions.getByType(MavenPublishPluginExtension::class.java) +// signing is done when uploading files to MC +// via gpg:sign-and-deploy-file (release.kts) +publish.releaseSigningEnabled = false + +tasks.named("distZip").configure { + dependsOn("publishToMavenLocal") + this.doLast { + val distributionFilePath = + "${project.layout.buildDirectory.asFile.get().path}${sep}distributions${sep}${project.name}-${project.version}.zip" + val file = File(distributionFilePath) + if (!file.exists()) { + throw IllegalStateException("Distribution file: $distributionFilePath does not exist") + } + if (file.length() == 0L) { + throw IllegalStateException("Distribution file: $distributionFilePath is empty") + } + } +} + +val sep = File.separator + +distributions { + main { + contents { + from("build${sep}libs") + from("build${sep}publications${sep}maven") + } + } +} + +buildConfig { + useKotlinOutput() + packageName("io.sentry") + className("BuildConfig") + + buildConfigField( + "String", + "SentryCocoaVersion", + provider { "\"${project.property("sentryCocoaVersion")}\"" } + ) +} + +detekt { config.setFrom(rootProject.files("../config/detekt/detekt.yml")) } + +tasks.withType().configureEach { + reports { + html.required.set(true) + html.outputLocation.set(file("build/reports/detekt.html")) + } +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/gradle.properties b/sentry-kotlin-multiplatform-gradle-plugin/gradle.properties new file mode 100644 index 00000000..73eb14b5 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/gradle.properties @@ -0,0 +1,6 @@ +id=io.sentry.kotlin.multiplatform.gradle +implementationClass=io.sentry.kotlin.multiplatform.gradle.SentryPlugin +versionName=0.7.1 +group=io.sentry +# TODO: Update update-cocoa.sh so the cocoa version is auto updated as well +sentryCocoaVersion=8.26.0 diff --git a/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml b/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml new file mode 100644 index 00000000..42893a18 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/gradle/libs.versions.toml @@ -0,0 +1,20 @@ +[versions] +detekt = "1.23.6" +kotlin = "1.9.23" +pluginPublish = "1.2.1" +buildConfig = "5.3.5" +vanniktechPublish = "0.18.0" +kover = "0.7.3" + +[plugins] +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt"} +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"} +pluginPublish = { id = "com.gradle.plugin-publish", version.ref = "pluginPublish"} +vanniktech-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechPublish"} +buildConfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildConfig"} +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover"} + +[libraries] +junit = "org.junit.jupiter:junit-jupiter-api:5.10.3" +junit-params = "org.junit.jupiter:junit-jupiter-params:5.10.3" +mockk = "io.mockk:mockk:1.13.12" diff --git a/sentry-kotlin-multiplatform-gradle-plugin/gradle/wrapper b/sentry-kotlin-multiplatform-gradle-plugin/gradle/wrapper new file mode 120000 index 00000000..cbfd2e1d --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/gradle/wrapper @@ -0,0 +1 @@ +../../gradle/wrapper/ \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/gradlew b/sentry-kotlin-multiplatform-gradle-plugin/gradlew new file mode 120000 index 00000000..502f5a2d --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/gradlew @@ -0,0 +1 @@ +../gradlew \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/gradlew.bat b/sentry-kotlin-multiplatform-gradle-plugin/gradlew.bat new file mode 120000 index 00000000..28401328 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/gradlew.bat @@ -0,0 +1 @@ +../gradlew.bat \ No newline at end of file diff --git a/sentry-kotlin-multiplatform-gradle-plugin/settings.gradle.kts b/sentry-kotlin-multiplatform-gradle-plugin/settings.gradle.kts new file mode 100644 index 00000000..4ef43ffb --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/settings.gradle.kts @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "sentry-kotlin-multiplatform-gradle-plugin" diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt new file mode 100644 index 00000000..9132450c --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/AutoInstallExtension.kt @@ -0,0 +1,26 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.api.Project +import org.gradle.api.provider.Property +import javax.inject.Inject + +@Suppress("UnnecessaryAbstractClass") +abstract class AutoInstallExtension @Inject constructor(project: Project) { + private val objects = project.objects + + /** + * Enable auto-installation of the Sentry dependencies through [CocoapodsAutoInstallExtension] + * and [SourceSetAutoInstallExtension]. + * + * Disabling this will prevent the plugin from auto installing any dependency. + * + * Defaults to true. + */ + val enabled: Property = objects.property(Boolean::class.java).convention(true) + + val cocoapods: CocoapodsAutoInstallExtension = + objects.newInstance(CocoapodsAutoInstallExtension::class.java, project) + + val commonMain: SourceSetAutoInstallExtension = + objects.newInstance(SourceSetAutoInstallExtension::class.java, project) +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoapodsAutoInstallExtension.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoapodsAutoInstallExtension.kt new file mode 100644 index 00000000..e3877211 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/CocoapodsAutoInstallExtension.kt @@ -0,0 +1,29 @@ +package io.sentry.kotlin.multiplatform.gradle + +import io.sentry.BuildConfig +import org.gradle.api.Project +import org.gradle.api.provider.Property +import javax.inject.Inject + +@Suppress("UnnecessaryAbstractClass") +abstract class CocoapodsAutoInstallExtension @Inject constructor(project: Project) { + private val objects = project.objects + + /** + * Enable auto-installation of the Sentry Cocoa SDK pod. + * + * If the cocoapods plugin is applied and no existing Sentry pod configuration exists, the + * Sentry-Cocoa SDK pod will be installed. + * + * Defaults to true. + */ + val enabled: Property = objects.property(Boolean::class.java).convention(true) + + /** + * Overrides default Sentry Cocoa version. + * + * Defaults to the version used in the latest KMP SDK. + */ + val sentryCocoaVersion: Property = + objects.property(String::class.java).convention("~> ${BuildConfig.SentryCocoaVersion}") +} 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 new file mode 100644 index 00000000..12dbeafb --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathValueSource.kt @@ -0,0 +1,43 @@ +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 +import org.gradle.api.tasks.Input +import org.gradle.process.ExecOperations +import java.io.ByteArrayOutputStream +import javax.inject.Inject + +internal abstract class DerivedDataPathValueSource : + ValueSource { + interface Parameters : ValueSourceParameters { + @get:Input + val xcodeprojPath: Property + } + + @get:Inject + abstract val execOperations: ExecOperations + + companion object { + private val buildDirRegex = Regex("BUILD_DIR = (.+)") + } + + override fun obtain(): String { + val buildDirOutput = ByteArrayOutputStream() + execOperations.exec { + it.commandLine = listOf( + "xcodebuild", + "-project", + parameters.xcodeprojPath.get(), + "-showBuildSettings" + ) + it.standardOutput = buildDirOutput + } + 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 buildDir.replace("/Build/Products", "") + } +} 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 new file mode 100644 index 00000000..81505a7f --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/LinkerExtension.kt @@ -0,0 +1,22 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.api.Project +import org.gradle.api.provider.Property +import javax.inject.Inject + +@Suppress("UnnecessaryAbstractClass") +abstract class LinkerExtension @Inject constructor(project: Project) { + private val objects = project.objects + + /** + * Path to the Xcode project that will be used to link the framework. + * This is used to find the derived data path in which the framework is stored for SPM. + */ + val xcodeprojPath: Property = objects.property(String::class.java) + + /** + * Path to the framework that will be linked. + * Takes precedence over [xcodeprojPath] if both are set. + */ + val frameworkPath: Property = objects.property(String::class.java) +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryExtension.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryExtension.kt new file mode 100644 index 00000000..38baf874 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryExtension.kt @@ -0,0 +1,20 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.api.Project +import javax.inject.Inject + +@Suppress("UnnecessaryAbstractClass") +abstract class SentryExtension @Inject constructor(project: Project) { + private val objects = project.objects + + /** + * Linker configuration. + * + * If you use SPM this configuration is necessary for setting up linking the framework and test + * executable. + */ + val linker: LinkerExtension = objects.newInstance(LinkerExtension::class.java, project) + + val autoInstall: AutoInstallExtension = + objects.newInstance(AutoInstallExtension::class.java, project) +} diff --git a/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt new file mode 100644 index 00000000..edf96df4 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SentryPlugin.kt @@ -0,0 +1,246 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +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" +internal const val AUTO_INSTALL_EXTENSION_NAME = "autoInstall" +internal const val COCOAPODS_AUTO_INSTALL_EXTENSION_NAME = "cocoapods" +internal const val COMMON_MAIN_AUTO_INSTALL_EXTENSION_NAME = "commonMain" +internal const val KOTLIN_EXTENSION_NAME = "kotlin" + +@Suppress("unused") +class SentryPlugin : Plugin { + override fun apply(project: Project): Unit = + with(project) { + val sentryExtension = + project.extensions.create( + SENTRY_EXTENSION_NAME, + SentryExtension::class.java, + project + ) + project.extensions.add(LINKER_EXTENSION_NAME, sentryExtension.linker) + project.extensions.add(AUTO_INSTALL_EXTENSION_NAME, sentryExtension.autoInstall) + project.extensions.add( + COCOAPODS_AUTO_INSTALL_EXTENSION_NAME, + sentryExtension.autoInstall.cocoapods + ) + project.extensions.add( + COMMON_MAIN_AUTO_INSTALL_EXTENSION_NAME, + sentryExtension.autoInstall.commonMain + ) + + 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) + } + } + } + + companion object { + internal val logger by lazy { + LoggerFactory.getLogger(SentryPlugin::class.java) + } + } +} + +internal fun Project.installSentryForKmp( + commonMainAutoInstallExtension: SourceSetAutoInstallExtension +) { + val kmpExtension = extensions.findByName(KOTLIN_EXTENSION_NAME) + if (kmpExtension !is KotlinMultiplatformExtension) { + logger.info("Kotlin Multiplatform plugin not found. Skipping Sentry installation.") + return + } + + val unsupportedTargets = listOf("wasm", "js", "mingw", "linux", "androidNative") + kmpExtension.targets.forEach { target -> + 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." + ) + } + } + + val commonMain = kmpExtension.sourceSets.find { it.name.contains("common") } + + val sentryVersion = commonMainAutoInstallExtension.sentryKmpVersion.get() + commonMain?.dependencies { api("io.sentry:sentry-kotlin-multiplatform:$sentryVersion") } +} + +internal fun Project.installSentryForCocoapods( + cocoapodsAutoInstallExtension: CocoapodsAutoInstallExtension +) { + val kmpExtension = extensions.findByName(KOTLIN_EXTENSION_NAME) + if (kmpExtension !is KotlinMultiplatformExtension || kmpExtension.targets.isEmpty() || !HostManager.hostIsMac) { + logger.info("Skipping Cocoapods installation.") + return + } + + (kmpExtension as ExtensionAware).extensions.configure(CocoapodsExtension::class.java) { cocoapods + -> + val podName = "Sentry" + val sentryPod = cocoapods.pods.findByName(podName) + if (sentryPod == null) { + cocoapods.pod(podName) { + version = cocoapodsAutoInstallExtension.sentryCocoaVersion.get() + linkOnly = true + extraOpts += listOf("-compiler-option", "-fmodules") + } + } + } +} + +@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/main/java/io/sentry/kotlin/multiplatform/gradle/SourceSetAutoInstallExtension.kt b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SourceSetAutoInstallExtension.kt new file mode 100644 index 00000000..c2dff135 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/main/java/io/sentry/kotlin/multiplatform/gradle/SourceSetAutoInstallExtension.kt @@ -0,0 +1,28 @@ +package io.sentry.kotlin.multiplatform.gradle + +import org.gradle.api.Project +import org.gradle.api.provider.Property +import javax.inject.Inject + +@Suppress("UnnecessaryAbstractClass") +abstract class SourceSetAutoInstallExtension @Inject constructor(project: Project) { + private val objects = project.objects + + /** + * Enable auto-installation of the Sentry Kotlin Multiplatform SDK dependency in the commonMain + * source set. + * + * Defaults to true. + */ + val enabled: Property = objects.property(Boolean::class.java).convention(true) + + /** + * Overrides the default Sentry Kotlin Multiplatform SDK dependency version. + * + * Defaults to the latest version of the Sentry Kotlin Multiplatform SDK. + */ + val sentryKmpVersion: Property = + objects + .property(String::class.java) + .convention("latest.release") // Replace with the actual default version +} 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 new file mode 100644 index 00000000..7e6007e9 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/DerivedDataPathTest.kt @@ -0,0 +1,109 @@ +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.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 + + @BeforeEach + fun setup() { + execOperations = mockk() + parameters = mockk() + + valueSource = object : DerivedDataPathValueSource() { + override val execOperations: ExecOperations = this@DerivedDataPathTest.execOperations + override fun getParameters(): Parameters { + return this@DerivedDataPathTest.parameters + } + } + } + + @Test + fun `obtain should return correct derived data path`() { + val xcodebuildOutput = """ + Build settings for action build and target MyTarget: + BUILD_DIR = /DerivedData/Example/Build/Products + """.trimIndent() + + every { parameters.xcodeprojPath } returns mockk { + every { get() } returns "/path/to/project.xcodeproj" + } + + every { execOperations.exec(any()) } answers { + val execSpecLambda = it.invocation.args[0] as Action + val mockExecSpec = mockk(relaxed = true) + + val buildDirOutput = ByteArrayOutputStream() + buildDirOutput.write(xcodebuildOutput.toByteArray()) + + var capturedOutputStream: ByteArrayOutputStream? = null + every { mockExecSpec.commandLine }.returns(listOf()) + every { mockExecSpec.setStandardOutput(any()) } answers { + capturedOutputStream = firstArg() + mockExecSpec + } + + execSpecLambda.execute(mockExecSpec) + + capturedOutputStream?.write(xcodebuildOutput.toByteArray()) + + mockk { + every { exitValue } returns 0 + } + } + + val result = valueSource.obtain() + + assertEquals("/DerivedData/Example", result) + } + + @Test + fun `obtain should throw GradleException when BUILD_DIR is not found`() { + val xcodebuildOutput = "Some output without BUILD_DIR" + + every { parameters.xcodeprojPath } returns mockk { + every { get() } returns "/path/to/project.xcodeproj" + } + + every { execOperations.exec(any()) } answers { + val execSpecLambda = it.invocation.args[0] as Action + val mockExecSpec = mockk(relaxed = true) + + val buildDirOutput = ByteArrayOutputStream() + buildDirOutput.write(xcodebuildOutput.toByteArray()) + + var capturedOutputStream: ByteArrayOutputStream? = null + every { mockExecSpec.commandLine }.returns(listOf()) + every { mockExecSpec.setStandardOutput(any()) } answers { + capturedOutputStream = firstArg() + mockExecSpec + } + + execSpecLambda.execute(mockExecSpec) + + capturedOutputStream?.write(xcodebuildOutput.toByteArray()) + + mockk { + every { exitValue } returns 0 + } + } + + assertThrows { + valueSource.obtain() + } + } +} 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 new file mode 100644 index 00000000..01737d51 --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryFrameworkArchitectureTest.kt @@ -0,0 +1,44 @@ +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.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +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) + ) + } + + @ParameterizedTest(name = "Target {0} should return {1}") + @MethodSource("architectureData") + fun `toSentryFrameworkArchitecture returns correct architecture for all targets`( + targetName: String, + expectedArchitecture: String? + ) { + val target = mockk() + every { target.name } returns targetName + + val result = target.toSentryFrameworkArchitecture() + + assertEquals(expectedArchitecture, result) + } +} 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 new file mode 100644 index 00000000..dfa841aa --- /dev/null +++ b/sentry-kotlin-multiplatform-gradle-plugin/src/test/java/io/sentry/kotlin/multiplatform/gradle/SentryPluginTest.kt @@ -0,0 +1,253 @@ +package io.sentry.kotlin.multiplatform.gradle + +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 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 `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) + + project.configureLinkingOptions(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) + } +} diff --git a/sentry-samples/kmp-app-cocoapods/shared/build.gradle.kts b/sentry-samples/kmp-app-cocoapods/shared/build.gradle.kts index 1f5841f6..24c26567 100644 --- a/sentry-samples/kmp-app-cocoapods/shared/build.gradle.kts +++ b/sentry-samples/kmp-app-cocoapods/shared/build.gradle.kts @@ -4,6 +4,7 @@ plugins { kotlin("multiplatform") kotlin("native.cocoapods") id("com.android.library") + id("io.sentry.kotlin.multiplatform.gradle") } java { @@ -64,3 +65,9 @@ android { minSdk = Config.Android.minSdkVersion } } + +// disabling autoInstall because we are using project(":sentry-kotlin-multiplatform") directly +// for our sample apps +sentryKmp { + autoInstall.commonMain.enabled = false +} diff --git a/sentry-samples/kmp-app-cocoapods/shared/src/commonTest/kotlin/CompilationTest.kt b/sentry-samples/kmp-app-cocoapods/shared/src/commonTest/kotlin/CompilationTest.kt new file mode 100644 index 00000000..6b9d1f34 --- /dev/null +++ b/sentry-samples/kmp-app-cocoapods/shared/src/commonTest/kotlin/CompilationTest.kt @@ -0,0 +1,9 @@ +import kotlin.test.Test + +// Simple compilation check that is run during CI +class CompilationTest { + @Test + fun test() { + print("see if this runs") + } +} diff --git a/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.pbxproj b/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.pbxproj index 4fc741d7..cfe22a19 100644 --- a/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.pbxproj +++ b/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.pbxproj @@ -9,9 +9,9 @@ /* Begin PBXBuildFile section */ 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; - 1DBB948028897D4700E79663 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 1DBB947F28897D4700E79663 /* Sentry */; }; 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 243652EF29F34FF500FD902A /* sentry.png in Resources */ = {isa = PBXBuildFile; fileRef = 243652EE29F34FF500FD902A /* sentry.png */; }; + 24E1CB3F2C17E00200F78D70 /* Sentry-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 24E1CB3E2C17E00200F78D70 /* Sentry-Dynamic */; }; 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; /* End PBXBuildFile section */ @@ -44,7 +44,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 1DBB948028897D4700E79663 /* Sentry in Frameworks */, + 24E1CB3F2C17E00200F78D70 /* Sentry-Dynamic in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -117,7 +117,7 @@ ); name = iosApp; packageProductDependencies = ( - 1DBB947F28897D4700E79663 /* Sentry */, + 24E1CB3E2C17E00200F78D70 /* Sentry-Dynamic */, ); productName = NSExceptionKtSample; productReference = 7555FF7B242A565900829871 /* iosApp.app */; @@ -148,7 +148,7 @@ ); mainGroup = 7555FF72242A565900829871; packageReferences = ( - 1DBB947E28897D4700E79663 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, + 24E1CB3D2C17E00200F78D70 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, ); productRefGroup = 7555FF7C242A565900829871 /* Products */; projectDirPath = ""; @@ -355,6 +355,8 @@ "$(inherited)", "@executable_path/Frameworks", ); + OTHER_CFLAGS = ""; + "OTHER_CFLAGS[arch=*]" = "\"-compiler-option -fmodules\""; OTHER_LDFLAGS = ( "$(inherited)", "-framework", @@ -380,6 +382,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + OTHER_CFLAGS = ""; OTHER_LDFLAGS = ( "$(inherited)", "-framework", @@ -416,21 +419,21 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 1DBB947E28897D4700E79663 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + 24E1CB3D2C17E00200F78D70 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; requirement = { - kind = exactVersion; - version = 8.20.0; + kind = upToNextMajorVersion; + minimumVersion = 8.28.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 1DBB947F28897D4700E79663 /* Sentry */ = { + 24E1CB3E2C17E00200F78D70 /* Sentry-Dynamic */ = { isa = XCSwiftPackageProductDependency; - package = 1DBB947E28897D4700E79663 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; - productName = Sentry; + package = 24E1CB3D2C17E00200F78D70 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = "Sentry-Dynamic"; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 50e76216..e6d3b6ae 100644 --- a/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/sentry-samples/kmp-app-spm/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "7b5e54f81ac1ebaa640945691cb38c371b637198701f04fba811702fc8e7067e", "pins" : [ { "identity" : "sentry-cocoa", "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa.git", "state" : { - "revision" : "b847a202a517a90763e8fd0656d8028aeee7b78d", - "version" : "8.20.0" + "revision" : "a62862c99f5bcb28fd78617fab1a5fe29607c06c", + "version" : "8.28.0" } } ], diff --git a/sentry-samples/kmp-app-spm/shared/build.gradle.kts b/sentry-samples/kmp-app-spm/shared/build.gradle.kts index 183a4fee..44a3aaf5 100644 --- a/sentry-samples/kmp-app-spm/shared/build.gradle.kts +++ b/sentry-samples/kmp-app-spm/shared/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("multiplatform") id("com.android.library") + id("io.sentry.kotlin.multiplatform.gradle") } java { @@ -26,7 +27,7 @@ kotlin { ).forEach { it.binaries.framework { baseName = "shared" - isStatic = true + isStatic = false export(project(":sentry-kotlin-multiplatform")) } } @@ -49,3 +50,9 @@ android { minSdk = Config.Android.minSdkVersion } } + +// disabling autoInstall because we are using project(":sentry-kotlin-multiplatform") directly +// for our sample apps +sentryKmp { + autoInstall.commonMain.enabled = false +} diff --git a/sentry-samples/kmp-app-spm/shared/src/commonTest/kotlin/CompilationTest.kt b/sentry-samples/kmp-app-spm/shared/src/commonTest/kotlin/CompilationTest.kt new file mode 100644 index 00000000..6b9d1f34 --- /dev/null +++ b/sentry-samples/kmp-app-spm/shared/src/commonTest/kotlin/CompilationTest.kt @@ -0,0 +1,9 @@ +import kotlin.test.Test + +// Simple compilation check that is run during CI +class CompilationTest { + @Test + fun test() { + print("see if this runs") + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 3e294e97..ae3265f4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,6 +20,7 @@ dependencyResolutionManagement { rootProject.name = "sentry-kotlin-multiplatform-sdk" include(":sentry-kotlin-multiplatform") +includeBuild("sentry-kotlin-multiplatform-gradle-plugin") /* Simple KMP App with targets: