From 2a3ffb408e1e9504e43b4ed6b27b3e8de78ea84b Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Tue, 5 Aug 2025 20:33:52 +0200 Subject: [PATCH] Added support for KMP Android library plugin `com.android.kotlin.multiplatform.library` Gradle plugin was recently added. Support for a new type of compilations has been added: `com.android.build.api.variant.impl.KotlinMultiplatformAndroidLibraryTargetImpl`. Due to the limitations of the Gradle API, it has not yet been possible to implement a reliable way to get javac details for this plugin. Also, the Gradle version is upgraded to `8.13`. Fixes #747 --- gradle/wrapper/gradle-wrapper.properties | 2 +- kover-cli/build.gradle.kts | 5 +- .../functional/cases/AndroidKmpLibTests.kt | 18 +++++ .../android-kmp-library/build.gradle.kts | 25 +++++++ .../android-kmp-library/settings.gradle.kts | 19 +++++ .../src/androidMain/kotlin/AndroidClass.kt | 7 ++ .../src/commonMain/kotlin/ExampleClass.kt | 7 ++ .../locators/KotlinMultiPlatformLocator.kt | 73 ++++++++++++++----- .../kover/gradle/plugin/util/DynamicBean.kt | 2 +- 9 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/AndroidKmpLibTests.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/build.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/settings.gradle.kts create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/src/androidMain/kotlin/AndroidClass.kt create mode 100644 kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/src/commonMain/kotlin/ExampleClass.kt diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793..37f853b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/kover-cli/build.gradle.kts b/kover-cli/build.gradle.kts index e313f489..a1d90eaa 100644 --- a/kover-cli/build.gradle.kts +++ b/kover-cli/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile /* @@ -50,8 +51,8 @@ dependencies { } tasks.withType().configureEach { - kotlinOptions { - jvmTarget = "1.8" + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/AndroidKmpLibTests.kt b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/AndroidKmpLibTests.kt new file mode 100644 index 00000000..86972319 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/kotlin/kotlinx/kover/gradle/plugin/test/functional/cases/AndroidKmpLibTests.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.kover.gradle.plugin.test.functional.cases + +import kotlinx.kover.gradle.plugin.test.functional.framework.checker.CheckerContext +import kotlinx.kover.gradle.plugin.test.functional.framework.starter.TemplateTest + +internal class AndroidKmpLibTests { + @TemplateTest("android-kmp-library", ["koverXmlReport"]) + fun CheckerContext.testPresence() { + xmlReport { + classCounter("org.jetbrains.ExampleClass").assertPresent() + classCounter("org.jetbrains.AndroidClass").assertPresent() + } + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/build.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/build.gradle.kts new file mode 100644 index 00000000..e9a5e6d5 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + kotlin("multiplatform") version "2.2.0" + id("com.android.kotlin.multiplatform.library") version "8.12.0" + id ("org.jetbrains.kotlinx.kover") version "0.9.1" +} + +kotlin { + androidLibrary { + namespace = "org.jetbrains.kover.kml.lib" + compileSdk = 33 + minSdk = 24 + + withJava() + withDeviceTestBuilder { + sourceSetTreeName = "test" + } + + } + + jvm() +} diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/settings.gradle.kts b/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/settings.gradle.kts new file mode 100644 index 00000000..96327650 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "android-kmp-library" + diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/src/androidMain/kotlin/AndroidClass.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/src/androidMain/kotlin/AndroidClass.kt new file mode 100644 index 00000000..fd6bb547 --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/src/androidMain/kotlin/AndroidClass.kt @@ -0,0 +1,7 @@ +package org.jetbrains + +class AndroidClass { + fun a() { + println("Hello World!") + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/src/commonMain/kotlin/ExampleClass.kt b/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/src/commonMain/kotlin/ExampleClass.kt new file mode 100644 index 00000000..aca8311a --- /dev/null +++ b/kover-gradle-plugin/src/functionalTest/templates/builds/android-kmp-library/src/commonMain/kotlin/ExampleClass.kt @@ -0,0 +1,7 @@ +package org.jetbrains + +class ExampleClass { + fun a() { + println("Hello World!") + } +} \ No newline at end of file diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/locators/KotlinMultiPlatformLocator.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/locators/KotlinMultiPlatformLocator.kt index 94e21a4b..52db0003 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/locators/KotlinMultiPlatformLocator.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/locators/KotlinMultiPlatformLocator.kt @@ -7,11 +7,20 @@ package kotlinx.kover.gradle.plugin.locators import kotlinx.kover.gradle.plugin.appliers.origin.AndroidVariantOrigin import kotlinx.kover.gradle.plugin.appliers.origin.JvmVariantOrigin import kotlinx.kover.gradle.plugin.appliers.origin.AllVariantOrigins +import kotlinx.kover.gradle.plugin.appliers.origin.CompilationDetails +import kotlinx.kover.gradle.plugin.appliers.origin.LanguageCompilation import kotlinx.kover.gradle.plugin.commons.* import kotlinx.kover.gradle.plugin.util.* import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.testing.* import org.gradle.kotlin.dsl.* +import java.io.File +import kotlin.collections.filter +import kotlin.collections.toSet /* Since the Kover and Kotlin Multiplatform plug-ins can be in different class loaders (declared in different projects), the plug-ins are stored in a single instance in the loader of the project where the plug-in was used for the first time. @@ -42,29 +51,59 @@ private fun Project.locateAndroidVariants(kotlinExtension: DynamicBean): List("name") == "jvm" - } ?: return null - - return extractJvmVariant(jvmTarget) -} - - -private fun Project.extractJvmVariant(target: DynamicBean): JvmVariantOrigin { - val targetName = target.value("targetName") + } + if (jvmTargets.isEmpty()) { + return null + } + val names = jvmTargets.map { it.value("targetName") }.toSet() val tests = tasks.withType().matching { - it.hasSuperclass("KotlinJvmTest") && it.bean().value("targetName") == targetName + it.hasSuperclass("KotlinJvmTest") && it.bean().value("targetName") in names } - val compilations = provider { - target.beanCollection("compilations").jvmCompilations { - // exclude java classes from report. Expected java class files are placed in directories like - // build/classes/java/main - it.parentFile.name == "java" - } + val compilations: Provider> = provider { + jvmTargets.extractPlainJvmVariant() + jvmTargets.extractKmpAndroidLibraryVariant() } return JvmVariantOrigin(tests, compilations) } + + +private fun List.extractPlainJvmVariant(): Map { + return singleOrNull { + it.origin.hasSuperclass("KotlinJvmTarget") + }?.beanCollection("compilations")?.jvmCompilations { + // exclude java classes from report. Expected java class files are placed in directories like + // build/classes/java/main + it.parentFile.name == "java" + } ?: emptyMap() +} + + +private fun List.extractKmpAndroidLibraryVariant(): Map { + return singleOrNull { + it.origin.hasSuperclass("KotlinMultiplatformAndroidLibraryTargetImpl") + }?.beanCollection("compilations") + ?.filter { + // exclude test compilations + val compilationName = it.value("name") + compilationName != "test" && !compilationName.endsWith("Test") + } + ?.associate { compilation -> + val name = compilation.value("name") + val sources = compilation.beanCollection("allKotlinSourceSets").flatMap { + it["kotlin"].valueCollection("srcDirs") + }.toSet() + + val kotlinOutputs = compilation["output"].value("classesDirs").files.toSet() + val kotlinCompileTask = compilation.valueOrNull?>("compileTaskProvider")?.orNull + val kotlin = LanguageCompilation(kotlinOutputs, kotlinCompileTask) + // at the moment, there is no way to get a task and directives for javac from the compilation + val java = kotlin + + // since we place compilations from different targets in one map, we should separate it because the original names may overlap (like `main`) + "${name}AndroidLibrary" to CompilationDetails(sources, kotlin, java) + } ?: emptyMap() +} diff --git a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/util/DynamicBean.kt b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/util/DynamicBean.kt index ef3df06e..72246a3e 100644 --- a/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/util/DynamicBean.kt +++ b/kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/util/DynamicBean.kt @@ -9,7 +9,7 @@ import org.gradle.internal.metaobject.* internal fun Any.bean(): DynamicBean = DynamicBean(this) -internal class DynamicBean(origin: Any) { +internal class DynamicBean(val origin: Any) { private val wrappedOrigin = BeanDynamicObject(origin) operator fun get(name: String): DynamicBean = bean(name)