Register Kotlin MPP source sets in AGP

^KT-43391 fixed
This commit is contained in:
sellmair
2019-11-25 11:12:10 +01:00
committed by Sebastian Sellmair
parent 3a166f3592
commit 2c325c45e2
21 changed files with 479 additions and 33 deletions
@@ -21,6 +21,55 @@ open class KotlinAndroid36GradleIT : KotlinAndroid33GradleIT() {
override val defaultGradleVersion: GradleVersionRequired
get() = GradleVersionRequired.AtLeast("6.0")
@Test
fun testAndroidMppSourceSets(): Unit = with(Project("new-mpp-android-source-sets", GradleVersionRequired.FOR_MPP_SUPPORT)) {
build("sourceSets") {
assertSuccessful()
assertContains("Java sources: [lib/src/androidTest/java, lib/src/androidAndroidTest/kotlin]")
assertContains("Java sources: [lib/src/androidTestDebug/java, lib/src/androidAndroidTestDebug/kotlin]")
assertContains("Java sources: [lib/src/debug/java, lib/src/androidDebug/kotlin, lib/src/debug/kotlin]")
assertContains("Java sources: [lib/src/main/java, lib/src/androidMain/kotlin, lib/src/main/kotlin]")
assertContains("Java sources: [lib/src/release/java, lib/src/androidRelease/kotlin, lib/src/release/kotlin]")
assertContains("Java sources: [lib/src/test/java, lib/src/androidTest/kotlin, lib/src/test/kotlin]")
assertContains("Java sources: [lib/src/testDebug/java, lib/src/androidTestDebug/kotlin, lib/src/testDebug/kotlin]")
assertContains("Java sources: [lib/src/testRelease/java, lib/src/androidTestRelease/kotlin, lib/src/testRelease/kotlin]")
assertContains("Android resources: [lib/src/main/res, lib/src/androidMain/res]")
assertContains("Assets: [lib/src/main/assets, lib/src/androidMain/assets]")
assertContains("AIDL sources: [lib/src/main/aidl, lib/src/androidMain/aidl]")
assertContains("RenderScript sources: [lib/src/main/rs, lib/src/androidMain/rs]")
assertContains("JNI sources: [lib/src/main/jni, lib/src/androidMain/jni]")
assertContains("JNI libraries: [lib/src/main/jniLibs, lib/src/androidMain/jniLibs]")
assertContains("Java-style resources: [lib/src/main/resources, lib/src/androidMain/resources]")
assertContains("Android resources: [lib/src/androidTestDebug/res, lib/src/androidAndroidTestDebug/res]")
assertContains("Assets: [lib/src/androidTestDebug/assets, lib/src/androidAndroidTestDebug/assets]")
assertContains("AIDL sources: [lib/src/androidTestDebug/aidl, lib/src/androidAndroidTestDebug/aidl]")
assertContains("RenderScript sources: [lib/src/androidTestDebug/rs, lib/src/androidAndroidTestDebug/rs]")
assertContains("JNI sources: [lib/src/androidTestDebug/jni, lib/src/androidAndroidTestDebug/jni]")
assertContains("JNI libraries: [lib/src/androidTestDebug/jniLibs, lib/src/androidAndroidTestDebug/jniLibs]")
assertContains("Java-style resources: [lib/src/androidTestDebug/resources, lib/src/androidAndroidTestDebug/resources]")
}
build("testDebug") {
assertFailed()
assertContains("CommonTest > fail FAILED")
assertContains("TestKotlin > fail FAILED")
assertContains("AndroidTestKotlin > fail FAILED")
assertContains("TestJava > fail FAILED")
}
build("assemble") {
assertSuccessful()
}
// Test for KT-35016: MPP should recognize android instrumented tests correctly
build("connectedAndroidTest") {
assertFailed()
assertContains("No connected devices!")
}
}
@Test
fun testAndroidWithNewMppApp() = with(Project("new-mpp-android", GradleVersionRequired.FOR_MPP_SUPPORT)) {
build("assemble", "compileDebugUnitTestJavaWithJavac", "printCompilerPluginOptions") {
@@ -600,7 +649,12 @@ fun getSomething() = 10
}
val libAndroidClassesOnlyUtilKt = project.projectDir.getFileByName("LibAndroidClassesOnlyUtil.kt")
libAndroidClassesOnlyUtilKt.modify { it.replace("fun libAndroidClassesOnlyUtil(): String", "fun libAndroidClassesOnlyUtil(): CharSequence") }
libAndroidClassesOnlyUtilKt.modify {
it.replace(
"fun libAndroidClassesOnlyUtil(): String",
"fun libAndroidClassesOnlyUtil(): CharSequence"
)
}
project.build("assembleDebug", options = options) {
assertSuccessful()
val affectedSources = project.projectDir.getFilesByNames("LibAndroidClassesOnlyUtil.kt", "useLibAndroidClassesOnlyUtil.kt")
@@ -0,0 +1,22 @@
buildscript {
repositories {
mavenCentral()
google()
mavenLocal()
jcenter()
}
dependencies {
classpath(kotlin("gradle-plugin:${property("kotlin_version")}"))
classpath("com.android.tools.build:gradle:${property("android_tools_version")}")
}
}
allprojects {
repositories {
mavenCentral()
google()
mavenLocal()
jcenter()
}
}
@@ -0,0 +1,38 @@
plugins {
id("com.android.library")
kotlin("multiplatform")
}
android {
compileSdkVersion(28)
defaultConfig {
minSdkVersion(21)
targetSdkVersion(28)
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
}
}
kotlin {
android()
macosX64("macos")
sourceSets {
getByName("commonMain").dependencies {
implementation(kotlin("stdlib-common"))
}
getByName("commonMain").dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-annotations-common"))
}
getByName("androidMain").dependencies {
implementation(kotlin("stdlib-jdk8"))
}
getByName("androidAndroidTest").dependencies {
implementation(kotlin("test-junit"))
implementation("com.android.support.test:runner:1.0.2")
}
}
}
@@ -0,0 +1,13 @@
import kotlin.test.Test
/**
* Expected to fail!
*/
class AndroidAndroidTest {
@Test
fun fail() {
MainApiKotlin.sayHi()
MainApiJava.sayHi()
CommonApi.throwException()
}
}
@@ -0,0 +1,10 @@
import org.junit.Test
class AndroidTestJava {
@Test
fun fail() {
MainApiJava.sayHi()
MainApiKotlin.sayHi()
CommonApi.throwException()
}
}
@@ -0,0 +1,10 @@
import org.junit.Test
class AndroidTestKotlin {
@Test
fun fail() {
MainApiJava.sayHi()
MainApiKotlin.sayHi()
CommonApi.throwException()
}
}
@@ -0,0 +1,3 @@
object CommonApi {
fun throwException(): Unit = error("This is supposed to fail!")
}
@@ -0,0 +1,9 @@
import kotlin.test.Test
/* Expected to fail ! */
class CommonTest {
@Test
fun fail() {
CommonApi.throwException()
}
}
@@ -0,0 +1,13 @@
import org.junit.Test
class TestJava {
@Test
fun fail() {
MainApiKotlin.sayHi()
MainApiJava.sayHi()
AndroidMainApiKotlin.sayHi()
CommonApi.throwException()
}
}
@@ -0,0 +1,13 @@
import kotlin.test.Test
/**
* Expected to fail!
*/
class TestKotlin {
@Test
fun fail() {
MainApiKotlin.sayHi()
MainApiJava.sayHi()
CommonApi.throwException()
}
}
@@ -0,0 +1,2 @@
rootProject.name = "mpp-playgound"
include(":lib")
@@ -0,0 +1,152 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("invisible_reference", "invisible_member", "FunctionName")
package org.jetbrains.kotlin.gradle
import com.android.build.gradle.LibraryExtension
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.testfixtures.ProjectBuilder
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import kotlin.test.*
class SyncKotlinAndAndroidSourceSetsTest {
private lateinit var project: ProjectInternal
private lateinit var kotlin: KotlinMultiplatformExtension
private lateinit var android: LibraryExtension
@BeforeTest
fun setup() {
project = ProjectBuilder.builder().build() as ProjectInternal
project.plugins.apply("kotlin-multiplatform")
project.plugins.apply("android-library")
/* Arbitrary minimal Android setup */
android = project.extensions.getByName("android") as LibraryExtension
android.compileSdkVersion(30)
/* Kotlin Setup */
kotlin = project.multiplatformExtension
}
@Test
fun `main source set with default settings`() {
kotlin.android()
val kotlinAndroidMainSourceSet = kotlin.sourceSets.getByName("androidMain")
val androidMainSourceSet = android.sourceSets.getByName("main")
assertEquals(
androidMainSourceSet.java.srcDirs.toSet(),
kotlinAndroidMainSourceSet.kotlin.srcDirs.toSet(),
"Expected all source directories being present in all models"
)
}
@Test
fun `test source set with default settings`() {
kotlin.android()
val kotlinAndroidTestSourceSet = kotlin.sourceSets.getByName("androidTest")
val testSourceSet = android.sourceSets.getByName("test")
assertEquals(
testSourceSet.java.srcDirs.toSet(),
kotlinAndroidTestSourceSet.kotlin.srcDirs.toSet(),
"Expected all source directories being present in all models"
)
}
@Test
fun `androidTest source set with default settings`() {
kotlin.android()
val kotlinAndroidAndroidTestSourceSet = kotlin.sourceSets.getByName("androidAndroidTest")
val androidTestSourceSet = android.sourceSets.getByName("androidTest")
assertTrue(
androidTestSourceSet.java.srcDirs.toSet().containsAll(kotlinAndroidAndroidTestSourceSet.kotlin.srcDirs),
"Expected all kotlin source directories being registered on AGP"
)
assertTrue(
project.file("src/androidTest/kotlin") !in kotlinAndroidAndroidTestSourceSet.kotlin.srcDirs,
"Expected no source directory of 'androidTest' kotlin source set (Unit Test) " +
"being present in 'androidAndroidTest' kotlin source set (Instrumented Test)"
)
assertTrue(
project.file("src/androidTest/kotlin") !in androidTestSourceSet.java.srcDirs,
"Expected no source directory of 'androidTest' kotlin source set (Unit Test) " +
"being present in 'androidTest' Android source set (Instrumented Test)"
)
}
@Test
fun `all source directories are disjoint in source sets`() {
kotlin.android()
project.evaluate()
kotlin.sourceSets.toSet().allPairs()
.forEach { (sourceSetA, sourceSetB) ->
val sourceDirsInBothSourceSets = sourceSetA.kotlin.srcDirs.intersect(sourceSetB.kotlin.srcDirs)
assertTrue(
sourceDirsInBothSourceSets.isEmpty(),
"Expected disjoint source directories in source sets. " +
"Found $sourceDirsInBothSourceSets present in ${sourceSetA.name}(Kotlin) and ${sourceSetB.name}(Kotlin)"
)
}
android.sourceSets.toSet().allPairs()
.forEach { (sourceSetA, sourceSetB) ->
val sourceDirsInBothSourceSets = sourceSetA.java.srcDirs.intersect(sourceSetB.java.srcDirs)
assertTrue(
sourceDirsInBothSourceSets.isEmpty(),
"Expected disjoint source directories in source sets. " +
"Found $sourceDirsInBothSourceSets present in ${sourceSetA.name}(Android) and ${sourceSetB.name}(Android)"
)
}
}
@Test
fun `sync includes user configuration`() {
kotlin.android()
val kotlinAndroidMain = kotlin.sourceSets.getByName("androidMain")
val androidMain = android.sourceSets.getByName("main")
kotlinAndroidMain.kotlin.srcDir(project.file("fromKotlin"))
androidMain.java.srcDir(project.file("fromAndroid"))
project.evaluate()
assertTrue(
kotlinAndroidMain.kotlin.srcDirs.containsAll(setOf(project.file("fromKotlin"), project.file("fromAndroid"))),
"Expected custom configured source directories being present on kotlin source set after evaluation"
)
assertTrue(
androidMain.java.srcDirs.containsAll(setOf(project.file("fromKotlin"), project.file("fromAndroid"))),
"Expected custom configured source directories being present on android source set after evaluation"
)
}
}
private fun <T> Set<T>.allPairs(): Sequence<Pair<T, T>> {
val values = this.toList()
return sequence {
for (index in values.indices) {
val first = values[index]
for (remainingIndex in (index + 1)..values.lastIndex) {
val second = values[remainingIndex]
yield(first to second)
}
}
}
}
@@ -777,25 +777,11 @@ abstract class AbstractAndroidProjectHandler(private val kotlinConfigurationTool
)
fun configureTarget(kotlinAndroidTarget: KotlinAndroidTarget) {
syncKotlinAndAndroidSourceSets(kotlinAndroidTarget)
val project = kotlinAndroidTarget.project
val ext = project.extensions.getByName("android") as BaseExtension
ext.sourceSets.all { sourceSet ->
logger.kotlinDebug("Creating KotlinBaseSourceSet for source set $sourceSet")
val kotlinSourceSet = project.kotlinExtension.sourceSets.maybeCreate(
kotlinSourceSetNameForAndroidSourceSet(kotlinAndroidTarget, sourceSet.name)
).apply {
createDefaultDependsOnEdges(kotlinAndroidTarget, sourceSet, this)
kotlin.srcDir(project.file(project.file("src/${sourceSet.name}/kotlin")))
kotlin.srcDirs(sourceSet.java.srcDirs)
}
sourceSet.addConvention(KOTLIN_DSL_NAME, kotlinSourceSet)
ifKaptEnabled(project) {
Kapt3GradleSubplugin.createAptConfigurationIfNeeded(project, sourceSet.name)
}
}
val kotlinOptions = KotlinJvmOptionsImpl()
project.whenEvaluated {
// TODO don't require the flag once there is an Android Gradle plugin build that supports desugaring of Long.hashCode and
@@ -852,21 +838,6 @@ abstract class AbstractAndroidProjectHandler(private val kotlinConfigurationTool
}
}
private fun createDefaultDependsOnEdges(
kotlinAndroidTarget: KotlinAndroidTarget,
androidSourceSet: AndroidSourceSet,
kotlinSourceSet: KotlinSourceSet
) {
val commonSourceSetName = when (androidSourceSet.name) {
"main" -> "commonMain"
"test" -> "commonTest"
"androidTest" -> "commonTest"
else -> return
}
val commonSourceSet = kotlinAndroidTarget.project.kotlinExtension.sourceSets.findByName(commonSourceSetName) ?: return
kotlinSourceSet.dependsOn(commonSourceSet)
}
/**
* The Android variants have their configurations extendsFrom relation set up in a way that only some of the configurations of the
* variants propagate the dependencies from production variants to test ones. To make this dependency propagation work for the Kotlin
@@ -1049,7 +1020,7 @@ internal fun configureJavaTask(
}
}
private fun ifKaptEnabled(project: Project, block: () -> Unit) {
internal fun ifKaptEnabled(project: Project, block: () -> Unit) {
var triggered = false
fun trigger() {
@@ -0,0 +1,123 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.plugin.mpp
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.AndroidSourceSet
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.internal.Kapt3GradleSubplugin
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.AbstractAndroidProjectHandler.Companion.kotlinSourceSetNameForAndroidSourceSet
import org.jetbrains.kotlin.gradle.plugin.addConvention
import java.io.File
internal fun syncKotlinAndAndroidSourceSets(target: KotlinAndroidTarget) {
val project = target.project
val android = project.extensions.getByName("android") as BaseExtension
android.sourceSets.all { androidSourceSet ->
val kotlinSourceSetName = kotlinSourceSetNameForAndroidSourceSet(target, androidSourceSet.name)
val kotlinSourceSet = project.kotlinExtension.sourceSets.maybeCreate(kotlinSourceSetName)
createDefaultDependsOnEdges(target, kotlinSourceSet, androidSourceSet)
syncKotlinAndAndroidSourceDirs(target, kotlinSourceSet, androidSourceSet)
syncKotlinAndAndroidResources(target, kotlinSourceSet, androidSourceSet)
androidSourceSet.addConvention(KOTLIN_DSL_NAME, kotlinSourceSet)
ifKaptEnabled(project) {
Kapt3GradleSubplugin.createAptConfigurationIfNeeded(project, androidSourceSet.name)
}
}
}
private fun createDefaultDependsOnEdges(
target: KotlinAndroidTarget,
kotlinSourceSet: KotlinSourceSet,
androidSourceSet: AndroidSourceSet
) {
val commonSourceSetName = when (androidSourceSet.name) {
"main" -> "commonMain"
"test" -> "commonTest"
"androidTest" -> "commonTest"
else -> return
}
val commonSourceSet = target.project.kotlinExtension.sourceSets.findByName(commonSourceSetName) ?: return
kotlinSourceSet.dependsOn(commonSourceSet)
}
private fun syncKotlinAndAndroidSourceDirs(
target: KotlinAndroidTarget, kotlinSourceSet: KotlinSourceSet, androidSourceSet: AndroidSourceSet
) {
val disambiguationClassifier = target.disambiguationClassifier
/*
Mitigate ambiguity!
Example: disambiguationClassifier="android"
Source Directory "src/androidTest/kotlin"
-- could be claimed by kotlin {android}Test (unit test)
-- could be claimed by Android androidTest (instrumented test)
The Kotlin source set would win in this scenario.
*/
if (disambiguationClassifier == null || !androidSourceSet.name.startsWith(disambiguationClassifier)) {
kotlinSourceSet.kotlin.srcDir("src/${androidSourceSet.name}/kotlin")
}
kotlinSourceSet.kotlin.srcDirs(*androidSourceSet.java.srcDirs.toTypedArray())
androidSourceSet.java.srcDirs(*kotlinSourceSet.kotlin.srcDirs.toTypedArray())
/*
Make sure to include user configuration as well.
Unfortunately, there does not exist any "ad-hoc" API like `all`.
Therefore we sync the directories once again after evaluation
*/
target.project.whenEvaluated {
kotlinSourceSet.kotlin.srcDirs(*androidSourceSet.java.srcDirs.toTypedArray())
androidSourceSet.java.srcDirs(*kotlinSourceSet.kotlin.srcDirs.toTypedArray())
}
}
private fun syncKotlinAndAndroidResources(
target: KotlinAndroidTarget,
kotlinSourceSet: KotlinSourceSet,
androidSourceSet: AndroidSourceSet
) {
val project = target.project
androidSourceSet.resources.srcDirs(*kotlinSourceSet.resources.toList().toTypedArray())
if (androidSourceSet.resources.srcDirs.isNotEmpty()) {
androidSourceSet.resources.srcDir(kotlinSourceSet.sourceFolderFor(project, "resources"))
kotlinSourceSet.resources.srcDirs(androidSourceSet.resources.srcDirs - kotlinSourceSet.resources.srcDirs)
}
if (androidSourceSet.assets.srcDirs.isNotEmpty()) {
androidSourceSet.assets.srcDir(kotlinSourceSet.sourceFolderFor(project, "assets"))
}
if (androidSourceSet.res.srcDirs.isNotEmpty()) {
androidSourceSet.res.srcDir(kotlinSourceSet.sourceFolderFor(project, "res"))
}
if (androidSourceSet.aidl.srcDirs.isNotEmpty()) {
androidSourceSet.aidl.srcDir(kotlinSourceSet.sourceFolderFor(project, "aidl"))
}
if (androidSourceSet.renderscript.srcDirs.isNotEmpty()) {
androidSourceSet.renderscript.srcDir(kotlinSourceSet.sourceFolderFor(project, "rs"))
}
if (androidSourceSet.jni.srcDirs.isNotEmpty()) {
androidSourceSet.jni.srcDir(kotlinSourceSet.sourceFolderFor(project, "jni"))
}
if (androidSourceSet.jniLibs.srcDirs.isNotEmpty()) {
androidSourceSet.jniLibs.srcDir(kotlinSourceSet.sourceFolderFor(project, "jniLibs"))
}
}
private fun KotlinSourceSet.sourceFolderFor(project: Project, type: String): File {
return project.file("src/${this.name}/$type")
}