Implement dependsOn relation for Kotlin source sets

* Setup configurations' extendsFrom and inherit sources of source sets.
  This make it unnecessary to restore transitive dependencies of a
  source set at its use sites, since their sources and dependencies are
  already included.

* Use dependsOn during default configuration instead of compilation.source()
This commit is contained in:
Sergey Igushkin
2018-08-01 22:20:00 +03:00
parent 93943a565c
commit 3ff61f59a7
7 changed files with 86 additions and 25 deletions
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.gradle.plugin.HasKotlinDependencies
interface KotlinSourceSet : Named, HasKotlinDependencies {
val kotlin: SourceDirectorySet
val resources: SourceDirectorySet
fun kotlin(configureClosure: Closure<Any?>): SourceDirectorySet
@@ -19,8 +20,8 @@ interface KotlinSourceSet : Named, HasKotlinDependencies {
const val COMMON_MAIN_SOURCE_SET_NAME = "commonMain"
const val COMMON_TEST_SOURCE_SET_NAME = "commonTest"
}
}
interface KotlinSourceSetWithResources : KotlinSourceSet {
val resources: SourceDirectorySet
fun dependsOn(other: KotlinSourceSet)
val dependsOn: Set<KotlinSourceSet>
}
@@ -417,7 +417,7 @@ internal abstract class AbstractKotlinPlugin(
kotlinCompilation.javaSourceSet = javaSourceSets.maybeCreate(sourceSetName)
// Another Kotlin source set following the other convention, named according to the compilation, not the Java source set:
val kotlinSourceSet = project.kotlinExtension.sourceSets.maybeCreate(kotlinCompilation.fullName)
val kotlinSourceSet = project.kotlinExtension.sourceSets.maybeCreate(kotlinCompilation.defaultSourceSetName)
kotlinCompilation.source(kotlinSourceSet)
}
}
@@ -29,7 +29,7 @@ import org.gradle.language.jvm.tasks.ProcessResources
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.disambiguateName
import org.jetbrains.kotlin.gradle.plugin.mpp.fullName
import org.jetbrains.kotlin.gradle.plugin.mpp.defaultSourceSetName
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
@@ -75,7 +75,7 @@ open class KotlinTargetConfigurator(
target.compilations.all { compilation ->
defineConfigurationsForCompilation(compilation, target, project.configurations)
project.kotlinExtension.sourceSets.maybeCreate(compilation.fullName).also { sourceSet ->
project.kotlinExtension.sourceSets.maybeCreate(compilation.defaultSourceSetName).also { sourceSet ->
compilation.source(sourceSet) // also adds dependencies, requires the configurations for target and source set to exist at this point
}
@@ -107,9 +107,14 @@ internal class KotlinMultiplatformPlugin(
val production = sourceSets.create(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)
val test = sourceSets.create(KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME)
targets.all {
it.compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME)?.source(production)
it.compilations.findByName(KotlinCompilation.TEST_COMPILATION_NAME)?.source(test)
targets.all { target ->
target.compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME)?.let { mainCompilation ->
sourceSets.maybeCreate(mainCompilation.defaultSourceSetName).dependsOn(production)
}
target.compilations.findByName(KotlinCompilation.TEST_COMPILATION_NAME)?.let { testCompilation ->
sourceSets.maybeCreate(testCompilation.defaultSourceSetName).dependsOn(test)
}
}
}
@@ -24,6 +24,7 @@ import org.gradle.api.tasks.SourceSetOutput
import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.source.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.sources.getSourceSetHierarchy
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
import org.jetbrains.kotlin.gradle.utils.addExtendsFromRelation
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
@@ -35,10 +36,8 @@ internal fun KotlinCompilation.composeName(prefix: String? = null, suffix: Strin
return lowerCamelCaseName(prefix, targetNamePart, compilationNamePart, suffix)
}
internal val KotlinCompilation.fullName: String
get() = fullCompilationName(target, compilationName)
internal fun fullCompilationName(target: KotlinTarget, simpleName: String) = lowerCamelCaseName(target.disambiguationClassifier, simpleName)
internal val KotlinCompilation.defaultSourceSetName: String
get() = lowerCamelCaseName(target.disambiguationClassifier, compilationName)
internal class DefaultKotlinDependencyHandler(
val parent: HasKotlinDependencies,
@@ -71,17 +70,19 @@ abstract class AbstractKotlinCompilation(
if (kotlinSourceSets.add(sourceSet)) {
with(target.project) {
whenEvaluated {
(target.project.tasks.getByName(compileKotlinTaskName) as AbstractKotlinCompile<*>).source(sourceSet.kotlin)
}
sourceSet.getSourceSetHierarchy().forEach { sourceSet ->
(target.project.tasks.getByName(compileKotlinTaskName) as AbstractKotlinCompile<*>).source(sourceSet.kotlin)
// Use `forced = false` since `api`, `implementation`, and `compileOnly` may be missing in some cases like
// old Java & Android projects:
addExtendsFromRelation(apiConfigurationName, sourceSet.apiConfigurationName, forced = false)
addExtendsFromRelation(implementationConfigurationName, sourceSet.implementationConfigurationName, forced = false)
addExtendsFromRelation(compileOnlyConfigurationName, sourceSet.compileOnlyConfigurationName, forced = false)
// Use `forced = false` since `api`, `implementation`, and `compileOnly` may be missing in some cases like
// old Java & Android projects:
addExtendsFromRelation(apiConfigurationName, sourceSet.apiConfigurationName, forced = false)
addExtendsFromRelation(implementationConfigurationName, sourceSet.implementationConfigurationName, forced = false)
addExtendsFromRelation(compileOnlyConfigurationName, sourceSet.compileOnlyConfigurationName, forced = false)
if (this is KotlinCompilationToRunnableFiles) {
addExtendsFromRelation(runtimeOnlyConfigurationName, sourceSet.runtimeOnlyConfigurationName)
if (this is KotlinCompilationToRunnableFiles) {
addExtendsFromRelation(runtimeOnlyConfigurationName, sourceSet.runtimeOnlyConfigurationName, forced = false)
}
}
}
}
}
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.gradle.plugin.sources
import groovy.lang.Closure
import org.gradle.api.InvalidUserCodeException
import org.gradle.api.Project
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.internal.file.DefaultSourceDirectorySet
@@ -14,15 +15,15 @@ import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.mpp.DefaultKotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.source.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.source.KotlinSourceSetWithResources
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import java.lang.reflect.Constructor
import java.util.*
class DefaultKotlinSourceSet(
private val project: Project,
val displayName: String,
fileResolver: FileResolver
) : KotlinSourceSetWithResources {
) : KotlinSourceSet {
override val apiConfigurationName: String
get() = disambiguateName("api")
@@ -52,9 +53,48 @@ class DefaultKotlinSourceSet(
override fun dependencies(configureClosure: Closure<Any?>) =
dependencies f@{ ConfigureUtil.configure(configureClosure, this@f) }
override fun dependsOn(other: KotlinSourceSet) {
dependsOnSourceSetsImpl.add(other)
// Fail-fast approach: check on each new added edge and report a circular dependency at once when the edge is added.
checkForCircularDependencies()
}
private val dependsOnSourceSetsImpl = mutableSetOf<KotlinSourceSet>()
override val dependsOn: Set<KotlinSourceSet>
get() = dependsOnSourceSetsImpl
override fun toString(): String = "source set $name"
}
private fun KotlinSourceSet.checkForCircularDependencies(): Unit {
// If adding an edge creates a cycle, than the source node of the edge belongs to the cycle, so run DFS from that node
// to check whether it became reachable from itself
val visited = hashSetOf<KotlinSourceSet>()
val stack = LinkedHashSet<KotlinSourceSet>() // Store the stack explicitly to pretty-print the cycle
fun checkReachableRecursively(from: KotlinSourceSet) {
stack += from
visited += from
for (to in from.dependsOn) {
if (to == this@checkForCircularDependencies)
throw InvalidUserCodeException(
"Circular dependsOn hierarchy found in the Kotlin source sets: " +
(stack.toList() + to).joinToString(" -> ") { it.name }
)
if (to !in visited) {
checkReachableRecursively(to)
}
}
stack -= from
}
checkReachableRecursively(this@checkForCircularDependencies)
}
internal fun KotlinSourceSet.disambiguateName(simpleName: String): String {
val nameParts = listOfNotNull(this.name.takeIf { it != "main" }, simpleName)
return lowerCamelCaseName(*nameParts.toTypedArray())
@@ -77,6 +117,20 @@ private val createDefaultSourceDirectorySet: (name: String?, resolver: FileResol
}
}
internal fun KotlinSourceSet.getSourceSetHierarchy(): Set<KotlinSourceSet> {
val result = mutableSetOf<KotlinSourceSet>()
fun processSourceSet(sourceSet: KotlinSourceSet) {
if (result.add(sourceSet)) {
sourceSet.dependsOn.forEach { processSourceSet(it) }
}
}
processSourceSet(this)
return result
}
private fun <T> Class<T>.constructorOrNull(vararg parameterTypes: Class<*>): Constructor<T>? =
try {
getConstructor(*parameterTypes)
@@ -7,7 +7,7 @@ package org.jetbrains.kotlin.gradle.utils
import org.gradle.api.Project
fun Project.addExtendsFromRelation(extendingConfigurationName: String, extendsFromConfigurationName: String, forced: Boolean = false) {
fun Project.addExtendsFromRelation(extendingConfigurationName: String, extendsFromConfigurationName: String, forced: Boolean = true) {
if (extendingConfigurationName != extendsFromConfigurationName) {
if (forced || configurations.findByName(extendingConfigurationName) != null) {
project.dependencies.add(extendingConfigurationName, project.configurations.getByName(extendsFromConfigurationName))