Skip to content

Commit 81ab639

Browse files
authored
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 PR #755
1 parent 43b1af3 commit 81ab639

File tree

9 files changed

+137
-21
lines changed

9 files changed

+137
-21
lines changed

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME

kover-cli/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
12
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
23

34
/*
@@ -50,8 +51,8 @@ dependencies {
5051
}
5152

5253
tasks.withType<KotlinCompile>().configureEach {
53-
kotlinOptions {
54-
jvmTarget = "1.8"
54+
compilerOptions {
55+
jvmTarget = JvmTarget.JVM_1_8
5556
}
5657
}
5758

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.kover.gradle.plugin.test.functional.cases
6+
7+
import kotlinx.kover.gradle.plugin.test.functional.framework.checker.CheckerContext
8+
import kotlinx.kover.gradle.plugin.test.functional.framework.starter.TemplateTest
9+
10+
internal class AndroidKmpLibTests {
11+
@TemplateTest("android-kmp-library", ["koverXmlReport"])
12+
fun CheckerContext.testPresence() {
13+
xmlReport {
14+
classCounter("org.jetbrains.ExampleClass").assertPresent()
15+
classCounter("org.jetbrains.AndroidClass").assertPresent()
16+
}
17+
}
18+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
plugins {
6+
kotlin("multiplatform") version "2.2.0"
7+
id("com.android.kotlin.multiplatform.library") version "8.12.0"
8+
id ("org.jetbrains.kotlinx.kover") version "0.9.1"
9+
}
10+
11+
kotlin {
12+
androidLibrary {
13+
namespace = "org.jetbrains.kover.kml.lib"
14+
compileSdk = 33
15+
minSdk = 24
16+
17+
withJava()
18+
withDeviceTestBuilder {
19+
sourceSetTreeName = "test"
20+
}
21+
22+
}
23+
24+
jvm()
25+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
pluginManagement {
2+
repositories {
3+
google()
4+
mavenCentral()
5+
gradlePluginPortal()
6+
}
7+
}
8+
9+
dependencyResolutionManagement {
10+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11+
repositories {
12+
google()
13+
mavenCentral()
14+
gradlePluginPortal()
15+
}
16+
}
17+
18+
rootProject.name = "android-kmp-library"
19+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.jetbrains
2+
3+
class AndroidClass {
4+
fun a() {
5+
println("Hello World!")
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.jetbrains
2+
3+
class ExampleClass {
4+
fun a() {
5+
println("Hello World!")
6+
}
7+
}

kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/locators/KotlinMultiPlatformLocator.kt

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,20 @@ package kotlinx.kover.gradle.plugin.locators
77
import kotlinx.kover.gradle.plugin.appliers.origin.AndroidVariantOrigin
88
import kotlinx.kover.gradle.plugin.appliers.origin.JvmVariantOrigin
99
import kotlinx.kover.gradle.plugin.appliers.origin.AllVariantOrigins
10+
import kotlinx.kover.gradle.plugin.appliers.origin.CompilationDetails
11+
import kotlinx.kover.gradle.plugin.appliers.origin.LanguageCompilation
1012
import kotlinx.kover.gradle.plugin.commons.*
1113
import kotlinx.kover.gradle.plugin.util.*
1214
import org.gradle.api.Project
15+
import org.gradle.api.Task
16+
import org.gradle.api.file.ConfigurableFileCollection
17+
import org.gradle.api.provider.Provider
18+
import org.gradle.api.tasks.TaskProvider
1319
import org.gradle.api.tasks.testing.*
1420
import org.gradle.kotlin.dsl.*
21+
import java.io.File
22+
import kotlin.collections.filter
23+
import kotlin.collections.toSet
1524

1625
/*
1726
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<An
4251
}
4352

4453
private fun Project.locateJvmVariant(kotlinExtension: DynamicBean): JvmVariantOrigin? {
45-
// only one JVM target is allowed, so we can take the first one
46-
val jvmTarget = kotlinExtension.beanCollection("targets").firstOrNull {
54+
val jvmTargets = kotlinExtension.beanCollection("targets").filter {
4755
it["platformType"].value<String>("name") == "jvm"
48-
} ?: return null
49-
50-
return extractJvmVariant(jvmTarget)
51-
}
52-
53-
54-
private fun Project.extractJvmVariant(target: DynamicBean): JvmVariantOrigin {
55-
val targetName = target.value<String>("targetName")
56+
}
57+
if (jvmTargets.isEmpty()) {
58+
return null
59+
}
5660

61+
val names = jvmTargets.map { it.value<String>("targetName") }.toSet()
5762
val tests = tasks.withType<Test>().matching {
58-
it.hasSuperclass("KotlinJvmTest") && it.bean().value<String>("targetName") == targetName
63+
it.hasSuperclass("KotlinJvmTest") && it.bean().value("targetName") in names
5964
}
6065

61-
val compilations = provider {
62-
target.beanCollection("compilations").jvmCompilations {
63-
// exclude java classes from report. Expected java class files are placed in directories like
64-
// build/classes/java/main
65-
it.parentFile.name == "java"
66-
}
66+
val compilations: Provider<Map<String, CompilationDetails>> = provider {
67+
jvmTargets.extractPlainJvmVariant() + jvmTargets.extractKmpAndroidLibraryVariant()
6768
}
6869

6970
return JvmVariantOrigin(tests, compilations)
7071
}
72+
73+
74+
private fun List<DynamicBean>.extractPlainJvmVariant(): Map<String, CompilationDetails> {
75+
return singleOrNull {
76+
it.origin.hasSuperclass("KotlinJvmTarget")
77+
}?.beanCollection("compilations")?.jvmCompilations {
78+
// exclude java classes from report. Expected java class files are placed in directories like
79+
// build/classes/java/main
80+
it.parentFile.name == "java"
81+
} ?: emptyMap()
82+
}
83+
84+
85+
private fun List<DynamicBean>.extractKmpAndroidLibraryVariant(): Map<String, CompilationDetails> {
86+
return singleOrNull {
87+
it.origin.hasSuperclass("KotlinMultiplatformAndroidLibraryTargetImpl")
88+
}?.beanCollection("compilations")
89+
?.filter {
90+
// exclude test compilations
91+
val compilationName = it.value<String>("name")
92+
compilationName != "test" && !compilationName.endsWith("Test")
93+
}
94+
?.associate { compilation ->
95+
val name = compilation.value<String>("name")
96+
val sources = compilation.beanCollection("allKotlinSourceSets").flatMap<DynamicBean, File> {
97+
it["kotlin"].valueCollection("srcDirs")
98+
}.toSet()
99+
100+
val kotlinOutputs = compilation["output"].value<ConfigurableFileCollection>("classesDirs").files.toSet()
101+
val kotlinCompileTask = compilation.valueOrNull<TaskProvider<Task>?>("compileTaskProvider")?.orNull
102+
val kotlin = LanguageCompilation(kotlinOutputs, kotlinCompileTask)
103+
// at the moment, there is no way to get a task and directives for javac from the compilation
104+
val java = kotlin
105+
106+
// since we place compilations from different targets in one map, we should separate it because the original names may overlap (like `main`)
107+
"${name}AndroidLibrary" to CompilationDetails(sources, kotlin, java)
108+
} ?: emptyMap()
109+
}

kover-gradle-plugin/src/main/kotlin/kotlinx/kover/gradle/plugin/util/DynamicBean.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import org.gradle.internal.metaobject.*
99

1010
internal fun Any.bean(): DynamicBean = DynamicBean(this)
1111

12-
internal class DynamicBean(origin: Any) {
12+
internal class DynamicBean(val origin: Any) {
1313
private val wrappedOrigin = BeanDynamicObject(origin)
1414

1515
operator fun get(name: String): DynamicBean = bean(name)

0 commit comments

Comments
 (0)