Reimplement friend task/friend paths using associated compilations

Replace the old ad-hoc task matching mechanism used for connecting the
compilation tasks that need to share internal visibility with an
implementation based on the associate compilations and compilation
outputs rather than `destinationDir`s of the tasks.

The only place that still requires ad-hoc friend paths is the Android
instrumented tests compiling against the JAR of the main variant, not
its classes dirs. Support that with `friendArtifacts`.

Issue #KT-17630 Fixed
Issue #KT-20760 Fixed
This commit is contained in:
Sergey Igushkin
2019-08-23 16:45:55 +03:00
parent c4d9c4cee4
commit d1016f0221
22 changed files with 223 additions and 196 deletions
@@ -2041,4 +2041,31 @@ class NewMultiplatformIT : BaseGradleIT() {
assertNotContains(DISABLED_NATIVE_TARGETS_REPORTER_WARNING_PREFIX)
}
}
@Test
fun testAssociateCompilations() = with(Project("new-mpp-associate-compilations")) {
setupWorkingDir()
gradleBuildScript().modify(::transformBuildScriptWithPluginsDsl)
val tasks = listOf("jvm", "js", nativeHostTargetName).map { ":compileIntegrationTestKotlin${it.capitalize()}" }
build(*tasks.toTypedArray()) {
assertSuccessful()
assertTasksExecuted(*tasks.toTypedArray())
// JVM:
checkBytecodeContains(
projectDir.resolve("build/classes/kotlin/jvm/integrationTest/com/example/HelloIntegrationTest.class"),
"Hello.internalFun\$new_mpp_associate_compilations",
"HelloTest.internalTestFun\$new_mpp_associate_compilations"
)
assertFileExists("build/classes/kotlin/jvm/integrationTest/META-INF/new-mpp-associate-compilations.kotlin_module")
// JS:
assertFileExists("build/classes/kotlin/js/integrationTest/new-mpp-associate-compilations_integrationTest.js")
// Native:
assertFileExists("build/classes/kotlin/$nativeHostTargetName/integrationTest/integrationTest.klib")
}
}
}
@@ -34,9 +34,7 @@ kotlin.target.compilations {
val benchmark by creating {
defaultSourceSet.dependencies {
implementation(main.compileDependencyFiles + main.output.allOutputs)
runtimeOnly(main.runtimeDependencyFiles)
associateWith(main)
implementation(kotlin("reflect"))
}
}
@@ -3,6 +3,7 @@ package com.example
fun main() {
val a = A()
val f = a::f
internalFun()
f()
println("${a::f.name} ran at the speed of light")
}
@@ -2,4 +2,6 @@ package com.example
class A {
fun f(): String = "hello".also(::println)
}
}
fun internalFun() = 1
@@ -0,0 +1,56 @@
plugins {
id("org.jetbrains.kotlin.multiplatform").version("<pluginMarkerVersion>")
}
repositories {
mavenLocal()
jcenter()
}
kotlin {
sourceSets {
getByName("commonMain") {
dependencies {
implementation(kotlin("stdlib-common"))
}
}
getByName("commonTest") {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
create("commonIntegrationTest")
}
jvm {
compilations["main"].defaultSourceSet.dependencies {
implementation(kotlin("stdlib"))
}
compilations["test"].defaultSourceSet.dependencies {
implementation(kotlin("test-junit"))
}
}
js {
compilations["main"].defaultSourceSet.dependencies {
implementation(kotlin("stdlib-js"))
}
compilations["test"].defaultSourceSet.dependencies {
implementation(kotlin("test-js"))
}
}
mingwX64("mingw64") {}
linuxX64("linux64") {}
macosX64("macos64") {}
targets.matching { it.name != "metadata" }.all {
compilations.create("integrationTest") {
associateWith(compilations["test"])
defaultSourceSet.dependsOn(sourceSets["commonIntegrationTest"])
}
}
}
@@ -0,0 +1,8 @@
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}
rootProject.name = "new-mpp-associate-compilations"
@@ -0,0 +1,24 @@
package com.example
import kotlin.test.Test
class HelloIntegrationTest {
@Test
fun test(): Unit = Hello().run {
hello()
internalFun()
HelloTest().run {
test()
internalTestFun()
}
}
@Test
fun secondTest() = Unit
@Test
fun thirdTest() = Unit
}
fun topLevelMemberToMakeTheCompilerGenerateTheModuleFile() = 1
@@ -0,0 +1,6 @@
package com.example
class Hello {
fun hello() = 0
internal fun internalFun() = 1
}
@@ -0,0 +1,19 @@
package com.example
import kotlin.test.Test
class HelloTest {
@Test
fun test(): Unit = Hello().run {
hello()
internalFun()
}
@Test
fun secondTest() = Unit
@Test
fun thirdTest() = Unit
internal fun internalTestFun() = 1
}
@@ -403,7 +403,6 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin<KotlinCompile> {
kotlinCompilation?.run {
output.apply {
addClassesDir { project.files(classesOutputDir).builtBy(kaptTask) }
kotlinCompile.attachClassesDir { classesOutputDir }
}
}
@@ -23,6 +23,7 @@ import org.jetbrains.kotlin.gradle.plugin.KOTLIN_JS_DSL_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.getConvention
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.associateWithTransitiveClosure
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
/**
@@ -128,7 +129,9 @@ class KotlinModelBuilder(private val kotlinPluginVersion: String, private val an
private fun AbstractKotlinCompile<*>.findFriendSourceSets(): Collection<String> {
val friendSourceSets = ArrayList<String>()
friendTask?.sourceSetName?.let { friendSourceSets.add(it) }
taskData.compilation.associateWithTransitiveClosure.forEach { associateCompilation ->
friendSourceSets.add(associateCompilation.name)
}
return friendSourceSets
}
@@ -1,49 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin
import org.gradle.api.Task
import org.jetbrains.annotations.TestOnly
internal abstract class TaskToFriendTaskMapper {
operator fun get(task: Task): String? =
getFriendByName(task.name)
@TestOnly
operator fun get(name: String): String? =
getFriendByName(name)
protected abstract fun getFriendByName(name: String): String?
}
sealed internal class RegexTaskToFriendTaskMapper(
private val prefix: String,
suffix: String,
private val targetName: String,
private val postfixReplacement: String
) : TaskToFriendTaskMapper() {
class Default(targetName: String) : RegexTaskToFriendTaskMapper("compile", "TestKotlin", targetName, "Kotlin")
class Android(targetName: String) : RegexTaskToFriendTaskMapper("compile", "(Unit|Android)TestKotlin", targetName, "Kotlin")
private val regex = "$prefix(.*)$suffix${targetName.capitalize()}".toRegex()
override fun getFriendByName(name: String): String? {
val match = regex.matchEntire(name) ?: return null
val variant = match.groups[1]?.value ?: ""
return prefix + variant + postfixReplacement + targetName.capitalize()
}
}
@@ -192,6 +192,13 @@ abstract class AbstractKotlinCompilation<T : KotlinCommonOptions>(
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,
* 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
get() = target.project.files()
override val moduleName: String
get() = KotlinCompilationsModuleGroups.getModuleLeaderCompilation(this).takeIf { it != this }?.ownModuleName ?: ownModuleName
@@ -75,12 +75,12 @@ class Android25ProjectHandler(
}
// Find the classpath entries that comes from the tested variant and register it as the friend path, lazily
kotlinTask.friendPaths = lazy {
compilation.testedVariantArtifacts.set(project.files(project.provider {
variantData.getCompileClasspathArtifacts(preJavaClasspathKey)
.filter { it.id.componentIdentifier is TestedComponentIdentifier }
.map { it.file.absolutePath }
.toTypedArray()
}
.map { it.file }
}))
kotlinTask.javaOutputDir = javaTask.destinationDir
compilation.output.classesDirs.run {
@@ -7,7 +7,11 @@
package org.jetbrains.kotlin.gradle.plugin.mpp
import com.android.build.gradle.api.BaseVariant
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.Property
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.getTestedVariantData
class KotlinJvmAndroidCompilation(
target: KotlinAndroidTarget,
@@ -19,6 +23,17 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
override val compileKotlinTask: org.jetbrains.kotlin.gradle.tasks.KotlinCompile
get() = super.compileKotlinTask as org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@Suppress("UnstableApiUsage")
internal val testedVariantArtifacts: Property<FileCollection> = target.project.objects.property(FileCollection::class.java)
override val friendArtifacts: FileCollection get() = target.project.files(super.friendArtifacts, testedVariantArtifacts)
override fun addAssociateCompilationDependencies(other: KotlinCompilation<*>) {
if ((other as? KotlinJvmAndroidCompilation)?.androidVariant != getTestedVariantData(androidVariant)) {
super.addAssociateCompilationDependencies(other)
} // otherwise, do nothing: the Android Gradle plugin adds these dependencies for us, we don't need to add them to the classpath
}
override val relatedConfigurationNames: List<String>
get() = super.relatedConfigurationNames + listOf(
"${androidVariant.name}ApiElements",
@@ -45,11 +45,24 @@ class KotlinNativeCompilation(
// TODO: Move into the compilation task when the linking task does klib linking instead of compilation.
internal val commonSources: ConfigurableFileCollection = project.files()
@Deprecated("Use associateWith(...) to add a friend compilation and associateWith to get all of them.")
var friendCompilationName: String? = null
set(value) {
SingleWarningPerBuild.show(
project,
"Property `friendCompilationName` of `KotlinNativeCompilation` has been deprecated and will be removed. " +
"Use `associateWith(...)` instead."
)
field = value
}
internal val friendCompilation: KotlinNativeCompilation?
get() = friendCompilationName?.let {
target.compilations.getByName(it)
internal val friendCompilations: List<KotlinNativeCompilation>
get() = mutableListOf<KotlinNativeCompilation>().apply {
@Suppress("DEPRECATION")
friendCompilationName?.let {
add(target.compilations.getByName(it))
}
addAll(associateWithTransitiveClosure.filterIsInstance<KotlinNativeCompilation>())
}
// Native-specific DSL.
@@ -20,13 +20,9 @@ class KotlinNativeCompilationFactory(
get() = KotlinNativeCompilation::class.java
override fun create(name: String): KotlinNativeCompilation =
KotlinNativeCompilation(target, name).apply {
if (name == KotlinCompilation.TEST_COMPILATION_NAME) {
friendCompilationName = KotlinCompilation.MAIN_COMPILATION_NAME
}
// TODO: Validate compilation free args using the [CompilationFreeArgsValidator]
// when the compilation and the link args are separated (see KT-33717).
// Note: such validation should be done in the whenEvaluate block because
// a user can change args during project configuration.
}
// TODO: Validate compilation free args using the [CompilationFreeArgsValidator]
// when the compilation and the link args are separated (see KT-33717).
// Note: such validation should be done in the whenEvaluate block because
// a user can change args during project configuration.
KotlinNativeCompilation(target, name)
}
@@ -22,7 +22,6 @@ import org.jetbrains.kotlin.compilerRunner.konanVersion
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonToolOptions
import org.jetbrains.kotlin.gradle.dsl.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder
import org.jetbrains.kotlin.gradle.plugin.cocoapods.asValidFrameworkName
import org.jetbrains.kotlin.gradle.plugin.mpp.*
@@ -278,7 +277,9 @@ open class KotlinNativeCompile : AbstractKotlinNativeCompile<KotlinCommonOptions
get() = project.files(compilation.commonSources).asFileTree
private val friendModule: FileCollection?
get() = compilation.friendCompilation?.output?.allOutputs
get() = project.files(
project.provider { compilation.friendCompilations.map { it.output.allOutputs } + compilation.friendArtifacts }
)
// endregion.
// region Language settings imported from a SourceSet.
@@ -440,7 +441,7 @@ open class KotlinNativeLink : AbstractKotlinNativeCompile<KotlinCommonToolOption
fn.call()
}
//region language settings inputs for the [linkFromSources] mode.
//region language settings inputs for the [linkFromSources] mode.
// TODO: Remove in 1.3.70.
@get:Optional
@get:Input
@@ -543,9 +544,15 @@ open class KotlinNativeLink : AbstractKotlinNativeCompile<KotlinCommonToolOption
// Allow a user to force the old behaviour of a link task.
// TODO: Remove in 1.3.70.
mutableListOf<String>().apply {
val friends = compilation.friendCompilation?.output?.allOutputs?.files
if (friends != null && friends.isNotEmpty()) {
addArg("-friend-modules", friends.joinToString(File.pathSeparator) { it.absolutePath })
val friendCompilations = compilation.friendCompilations
val friendFiles = if (friendCompilations.isNotEmpty())
project.files(
project.provider { friendCompilations.map { it.output.allOutputs } + compilation.friendArtifacts }
)
else null
if (friendFiles != null && !friendFiles.isEmpty) {
addArg("-friend-modules", friendFiles.joinToString(File.pathSeparator) { it.absolutePath })
}
addAll(project.files(compilation.allSources).map { it.absolutePath })
@@ -55,7 +55,6 @@ open class KotlinCompileCommon : AbstractKotlinCompile<K2MetadataCompilerArgumen
if (defaultsOnly) return
val classpathList = classpath.files.toMutableList()
friendTask?.let { classpathList.add(it.destinationDir) }
with(args) {
classpath = classpathList.joinToString(File.pathSeparator)
@@ -213,9 +213,6 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
?: coroutinesFromGradleProperties
?: Coroutines.DEFAULT
@get:Internal
internal var friendTaskName: String? = null
@get:Internal
internal var javaOutputDir: File?
get() = taskData.javaOutputDir
@@ -233,35 +230,14 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
internal val moduleName: String
get() = taskData.compilation.moduleName
@Suppress("UNCHECKED_CAST")
@get:Internal
internal val friendTask: AbstractKotlinCompile<T>?
get() = friendTaskName?.let { project.tasks.findByName(it) } as? AbstractKotlinCompile<T>
/** Classes directories that are not produced by this task but should be consumed by
* other tasks that have this one as a [friendTask]. */
private val attachedClassesDirs: MutableList<Lazy<File?>> = mutableListOf()
/** Registers the directory provided by the [provider] as attached, meaning that the directory should
* be consumed as a friend classes directory by other tasks that have this task as a [friendTask]. */
internal fun attachClassesDir(provider: () -> File?) {
attachedClassesDirs += lazy(provider)
}
@get:Internal // takes part in the compiler arguments
var friendPaths: Lazy<Array<String>?> = lazy {
friendTask?.let { friendTask ->
val possibleFriendDirs = ArrayList<File?>().apply {
add(friendTask.javaOutputDir)
add(friendTask.destinationDir)
addAll(friendTask.attachedClassesDirs.map { it.value })
}
possibleFriendDirs.filterNotNullTo(HashSet())
.map { it.absolutePath }
.toTypedArray()
val friendPaths: Array<String>
get() = taskData.compilation.run {
associateWithTransitiveClosure
.flatMap { it.output.classesDirs }
.plus(friendArtifacts)
.map { it.canonicalPath }.toTypedArray()
}
}
private val kotlinLogger by lazy { GradleKotlinLogger(logger) }
@@ -404,7 +380,7 @@ open class KotlinCompile : AbstractKotlinCompile<K2JVMCompilerArguments>(), Kotl
args.moduleName = taskData.compilation.moduleName
logger.kotlinDebug { "args.moduleName = ${args.moduleName}" }
args.friendPaths = friendPaths.value
args.friendPaths = friendPaths
logger.kotlinDebug { "args.friendPaths = ${args.friendPaths?.joinToString() ?: "[]"}" }
if (defaultsOnly) return
@@ -571,12 +547,11 @@ open class Kotlin2JsCompile : AbstractKotlinCompile<K2JSCompilerArguments>(), Ko
@get:InputFiles
@get:Optional
@get:PathSensitive(PathSensitivity.RELATIVE)
internal val friendDependency
get() = friendTaskName
?.let { project.getTasksByName(it, false).singleOrNull() as? Kotlin2JsCompile }
?.outputFile?.parentFile
?.let { if (libraryFilter(it)) it else null }
?.absolutePath
internal val friendDependencies: List<String>
get() {
val filter = libraryFilter
return friendPaths.filter { filter(File(it)) }
}
private val libraryFilter: (File) -> Boolean
get() = if ("-Xir" in kotlinOptions.freeCompilerArgs) {
@@ -601,13 +576,13 @@ open class Kotlin2JsCompile : AbstractKotlinCompile<K2JSCompilerArguments>(), Ko
.filter(libraryFilter)
.map { it.canonicalPath }
args.libraries = (dependencies + listOfNotNull(friendDependency)).distinct().let {
args.libraries = (dependencies + friendDependencies).distinct().let {
if (it.isNotEmpty())
it.joinToString(File.pathSeparator) else
null
}
args.friendModules = friendDependency
args.friendModules = friendDependencies.joinToString(File.pathSeparator)
if (args.sourceMapBaseDirs == null && !args.sourceMapPrefix.isNullOrEmpty()) {
args.sourceMapBaseDirs = project.projectDir.absolutePath
@@ -122,10 +122,6 @@ internal open class KotlinTasksProvider(val targetName: String) {
propertiesProvider: PropertiesProvider,
compilation: AbstractKotlinCompilation<*>
) {
kotlinTaskHolder.configure {
it.friendTaskName = taskToFriendTaskMapper[it]
}
project.runOnceAfterEvaluated("apply properties and language settings to ${kotlinTaskHolder.name}", kotlinTaskHolder) {
propertiesProvider.mapKotlinTaskProperties(kotlinTaskHolder.get())
@@ -136,17 +132,11 @@ internal open class KotlinTasksProvider(val targetName: String) {
}
}
protected open val taskToFriendTaskMapper: TaskToFriendTaskMapper =
RegexTaskToFriendTaskMapper.Default(targetName)
private inline fun <reified Task, reified WorkersTask : Task> taskOrWorkersTask(properties: PropertiesProvider): Class<out Task> =
if (properties.parallelTasksInProject != true) Task::class.java else WorkersTask::class.java
}
internal class AndroidTasksProvider(targetName: String) : KotlinTasksProvider(targetName) {
override val taskToFriendTaskMapper: TaskToFriendTaskMapper =
RegexTaskToFriendTaskMapper.Android(targetName)
override fun configure(
kotlinTaskHolder: TaskProvider<out AbstractKotlinCompile<*>>,
project: Project,
@@ -1,69 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin
import org.junit.Assert
import org.junit.Test
class RegexTaskToFriendTaskMapperTest {
@Test
fun getFriendTaskNameDefault() {
val mapper = RegexTaskToFriendTaskMapper.Default("")
Assert.assertEquals("compileKotlin", mapper["compileTestKotlin"])
Assert.assertEquals(null, mapper["compileKotlin"])
}
@Test
fun getFriendTaskNameCustomTargetName() {
val mapper = RegexTaskToFriendTaskMapper.Default("Foo")
Assert.assertEquals("compileKotlinFoo", mapper["compileTestKotlinFoo"])
Assert.assertEquals(null, mapper["compileKotlinFoo"])
Assert.assertEquals(null, mapper["compileTestKotlinBar"])
}
@Test
fun getFriendTaskNameAndroid() {
val mapper = RegexTaskToFriendTaskMapper.Android("")
// Unit test examples
Assert.assertEquals("compileDebugKotlin", mapper["compileDebugUnitTestKotlin"])
Assert.assertEquals("compileReleaseKotlin", mapper["compileReleaseUnitTestKotlin"])
Assert.assertEquals("compileProdDebugKotlin", mapper["compileProdDebugUnitTestKotlin"])
// Android test examples
Assert.assertEquals("compileDebugKotlin", mapper["compileDebugAndroidTestKotlin"])
Assert.assertEquals("compileReleaseKotlin", mapper["compileReleaseAndroidTestKotlin"])
Assert.assertEquals("compileProdDebugKotlin", mapper["compileProdDebugAndroidTestKotlin"])
Assert.assertEquals(null, mapper["compileDebugKotlin"])
}
@Test
fun getFriendTaskNameAndroidCustomTargetName() {
val mapper = RegexTaskToFriendTaskMapper.Android("Foo")
// Unit test examples
Assert.assertEquals("compileDebugKotlinFoo", mapper["compileDebugUnitTestKotlinFoo"])
Assert.assertEquals("compileReleaseKotlinFoo", mapper["compileReleaseUnitTestKotlinFoo"])
Assert.assertEquals("compileProdDebugKotlinFoo", mapper["compileProdDebugUnitTestKotlinFoo"])
// Android test examples
Assert.assertEquals("compileDebugKotlinFoo", mapper["compileDebugAndroidTestKotlinFoo"])
Assert.assertEquals("compileReleaseKotlinFoo", mapper["compileReleaseAndroidTestKotlinFoo"])
Assert.assertEquals("compileProdDebugKotlinFoo", mapper["compileProdDebugAndroidTestKotlinFoo"])
Assert.assertEquals(null, mapper["compileDebugKotlinFoo"])
Assert.assertEquals(null, mapper["compileDebugUnitTestKotlinBar"])
Assert.assertEquals(null, mapper["compileDebugAndroidTestKotlinBar"])
}
}