Include the production artifacts in friend paths (KT-35942)

In some setups, the friendArtifacts that we had in addition to direct
associate compilations' output classes were insufficient. For example:

1. When an Android module's tests depend on another module which
   transitively depends on the module containing tests: then the AGP
   API doesn't tell us that there are artifacts from the tested
   variant, but instead there's just the module's artifact.

2. Similar case but without Android, and if the classes are read from
   the module's artifact rather than the production output classes dir,
   then they are definitely not on the friend paths (which include the
   production classes dirs and not the the production artifact).

In both cases, the friendArtifacts mechanism allows us to add those
potential sources of the friend classes to the friend paths.

Issue #KT-35942 Fixed
This commit is contained in:
Sergey Igushkin
2020-02-05 21:22:57 +03:00
parent 4eb04af01e
commit 9b861739cd
17 changed files with 206 additions and 10 deletions
@@ -22,6 +22,7 @@ import org.jetbrains.kotlin.gradle.plugin.MULTIPLE_KOTLIN_PLUGINS_SPECIFIC_PROJE
import org.jetbrains.kotlin.gradle.scripting.internal.ScriptingGradleSubplugin import org.jetbrains.kotlin.gradle.scripting.internal.ScriptingGradleSubplugin
import org.jetbrains.kotlin.gradle.tasks.USING_JVM_INCREMENTAL_COMPILATION_MESSAGE import org.jetbrains.kotlin.gradle.tasks.USING_JVM_INCREMENTAL_COMPILATION_MESSAGE
import org.jetbrains.kotlin.gradle.util.* import org.jetbrains.kotlin.gradle.util.*
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.util.zip.ZipFile import java.util.zip.ZipFile
@@ -1003,4 +1004,26 @@ class KotlinGradleIT : BaseGradleIT() {
assertNotContains(ScriptingGradleSubplugin.MISCONFIGURATION_MESSAGE_SUFFIX) assertNotContains(ScriptingGradleSubplugin.MISCONFIGURATION_MESSAGE_SUFFIX)
} }
} }
@Test
fun testKtKt35942InternalsFromMainInTestViaTransitiveDeps() = with(Project("kt-35942-jvm")) {
build(":lib1:compileTestKotlin") {
assertSuccessful()
assertTasksExecuted(":lib1:compileKotlin", ":lib2:jar")
}
}
@Test
fun testKtKt35942InternalsFromMainInTestViaTransitiveDepsAndroid() = with(Project("kt-35942-android")) {
build(
":lib1:compileDebugUnitTestKotlin",
options = defaultBuildOptions().copy(
androidGradlePluginVersion = AGPVersion.v3_2_0,
androidHome = KotlinTestUtils.findAndroidSdk(),
),
) {
assertSuccessful()
assertTasksExecuted(":lib1:compileDebugKotlin")
}
}
} }
@@ -0,0 +1,20 @@
buildscript {
repositories {
mavenLocal()
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:$android_tools_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
mavenLocal()
google()
jcenter()
}
}
@@ -0,0 +1,19 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
testImplementation project(':lib2')
testImplementation 'junit:junit:4.13'
}
@@ -0,0 +1,10 @@
/*
* 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 com.example.lib1
internal object InternalClass {
fun hello() {}
}
@@ -0,0 +1,17 @@
/*
* 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 com.example.lib1
import org.junit.Test
import org.junit.Assert.*
class UnitTest {
@Test
fun `call internal class`() {
InternalClass.hello()
}
}
@@ -0,0 +1,17 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
}
}
dependencies {
api project(':lib1')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
}
@@ -0,0 +1,17 @@
buildscript {
repositories {
mavenLocal()
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
mavenLocal()
jcenter()
}
}
@@ -0,0 +1,12 @@
apply plugin: 'org.jetbrains.kotlin.jvm'
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
testImplementation project(':lib2')
testImplementation 'junit:junit:4.13'
}
kotlin.target.compilations.test {
compileDependencyFiles = compileDependencyFiles - kotlin.target.compilations.main.output.classesDirs
}
@@ -0,0 +1,10 @@
/*
* 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 com.example.lib1
internal object InternalClass {
fun hello() {}
}
@@ -0,0 +1,17 @@
/*
* 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 com.example.lib1
import org.junit.Test
import org.junit.Assert.*
class UnitTest {
@Test
fun `call internal class`() {
InternalClass.hello()
}
}
@@ -0,0 +1,6 @@
apply plugin: 'org.jetbrains.kotlin.jvm'
dependencies {
api project(':lib1')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
}
@@ -16,6 +16,7 @@ import org.gradle.api.plugins.ExtraPropertiesExtension
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.TaskState import org.gradle.api.tasks.TaskState
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.util.ConfigureUtil import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.*
@@ -26,6 +27,7 @@ import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
import org.jetbrains.kotlin.gradle.tasks.locateTask import org.jetbrains.kotlin.gradle.tasks.locateTask
import org.jetbrains.kotlin.gradle.utils.addExtendsFromRelation import org.jetbrains.kotlin.gradle.utils.addExtendsFromRelation
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import java.io.File
import java.util.* import java.util.*
import java.util.concurrent.Callable import java.util.concurrent.Callable
@@ -198,12 +200,22 @@ abstract class AbstractKotlinCompilation<T : KotlinCommonOptions>(
override fun toString(): String = "compilation '$compilationName' ($target)" override fun toString(): String = "compilation '$compilationName' ($target)"
/** If a compilation is aware of its associate compilations' outputs being added to the classpath in a transformed or packaged way, /**
* If a compilation is aware of its associate compilations' outputs being added to the classpath in a transformed or packaged way,
* it should point to those friend artifact files via this property. * it should point to those friend artifact files via this property.
* This is a workaround for Android variants that are compiled against */
* JARs of each other, which is not exposed in the API in any other way than in the consumer's classpath. */
internal open val friendArtifacts: FileCollection internal open val friendArtifacts: FileCollection
get() = target.project.files() get() = with(target.project) {
if (associateWithTransitiveClosure.any { it.name == KotlinCompilation.MAIN_COMPILATION_NAME }) {
// In case the main artifact is transitively added to the test classpath via a test dependency on another module
// that depends on this module's production part, include the main artifact in the friend artifacts, lazily:
files(
provider {
listOfNotNull(tasks.withType(AbstractArchiveTask::class.java).findByName(target.artifactsTaskName)?.archiveFile)
}
)
} else files()
}
override val moduleName: String override val moduleName: String
get() = KotlinCompilationsModuleGroups.getModuleLeaderCompilation(this).takeIf { it != this }?.ownModuleName ?: ownModuleName get() = KotlinCompilationsModuleGroups.getModuleLeaderCompilation(this).takeIf { it != this }?.ownModuleName ?: ownModuleName
@@ -12,6 +12,7 @@ import com.android.build.gradle.api.*
import com.android.build.gradle.tasks.MergeResources import com.android.build.gradle.tasks.MergeResources
import com.android.builder.model.SourceProvider import com.android.builder.model.SourceProvider
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.file.FileCollection import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.AbstractArchiveTask import org.gradle.api.tasks.bundling.AbstractArchiveTask
@@ -57,12 +58,23 @@ class Android25ProjectHandler(
kotlinClasspath + project.files(AndroidGradleWrapper.getRuntimeJars(androidPlugin, androidExt)) kotlinClasspath + project.files(AndroidGradleWrapper.getRuntimeJars(androidPlugin, androidExt))
} }
// Find the classpath entries that comes from the tested variant and register it as the friend path, lazily // Find the classpath entries that come from the tested variant and register them as the friend paths, lazily
compilation.testedVariantArtifacts.set(project.files(project.provider { compilation.testedVariantArtifacts.set(
variantData.getCompileClasspathArtifacts(preJavaClasspathKey) project.files(
.filter { it.id.componentIdentifier is TestedComponentIdentifier } project.provider {
.map { it.file } variantData.getCompileClasspathArtifacts(preJavaClasspathKey)
})) .filter {
it.id.componentIdentifier is TestedComponentIdentifier ||
// If tests depend on the main classes transitively, through a test dependency on another module which
// depends on this module, then there's no artifact with a TestedComponentIdentifier, so consider the artifact of the
// current module a friend path, too:
getTestedVariantData(variantData) != null &&
(it.id.componentIdentifier as? ProjectComponentIdentifier)?.projectPath == project.path
}
.map { it.file }
}
)
)
kotlinTask.javaOutputDir = javaTask.destinationDir kotlinTask.javaOutputDir = javaTask.destinationDir