[Gradle] Setup Consistent metadata dependencies resolution

Configure consistent metadata resolution only for source sets that
participate in metadata compilation. i.e. test or any other extra
compilations should be excluded.

^KT-65954 Verification Pending
^KT-66047 Verification Pending
This commit is contained in:
Anton Lakotka
2024-02-21 09:32:08 +01:00
committed by Space Team
parent c027ba642f
commit 3e5afcaaba
13 changed files with 283 additions and 58 deletions
@@ -1282,7 +1282,8 @@ open class HierarchicalMppIT : KGPBaseTest() {
gradleVersion,
localRepoDir = tempDir,
).run {
// add a dependency from commonTest on 2.0 version
// add a dependency from jvmTest on 2.0 version
// and assert that this dependency isn't leaking to the main source sets, including metadata compilation
buildGradleKts.appendText(
"""
@@ -355,4 +355,6 @@ internal fun <T : KotlinTarget> KotlinTargetsContainerWithPresets.configureOrCre
}
}
internal val KotlinMultiplatformExtension.metadataTarget get() = metadata() as KotlinMetadataTarget
internal val KotlinMultiplatformExtension.metadataTarget get() = metadata() as KotlinMetadataTarget
internal val Collection<KotlinTarget>.platformTargets: List<KotlinTarget> get() = filter { it !is KotlinMetadataTarget }
@@ -10,14 +10,14 @@ import org.gradle.api.artifacts.Configuration
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.Usage
import org.gradle.api.provider.Provider
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.categoryByName
import org.jetbrains.kotlin.gradle.plugin.hierarchy.orNull
import org.jetbrains.kotlin.gradle.plugin.sources.*
import org.jetbrains.kotlin.gradle.plugin.sources.InternalKotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.sources.disambiguateName
import org.jetbrains.kotlin.gradle.plugin.usageByName
import org.jetbrains.kotlin.gradle.plugin.usesPlatformOf
import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.gradle.utils.listProperty
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
@@ -35,12 +35,13 @@ internal val InternalKotlinSourceSet.resolvableMetadataConfigurationName: String
*/
internal val InternalKotlinSourceSet.resolvableMetadataConfiguration: Configuration by extrasStoredProperty {
assert(resolvableMetadataConfigurationName !in project.configurations.names)
val configuration = project.configurations.maybeCreateResolvable(resolvableMetadataConfigurationName)
val configuration = project.configurations
.maybeCreateResolvable(resolvableMetadataConfigurationName)
.configureMetadataDependenciesAttribute(project)
withDependsOnClosure.forAll { sourceSet ->
configuration.extendsFrom(project.configurations.getByName(sourceSet.apiConfigurationName))
configuration.extendsFrom(project.configurations.getByName(sourceSet.implementationConfigurationName))
configuration.extendsFrom(project.configurations.getByName(sourceSet.compileOnlyConfigurationName))
val extenders = sourceSet.internal.compileDependenciesConfigurations
configuration.extendsFrom(*extenders.toTypedArray())
}
/**
@@ -50,26 +51,23 @@ internal val InternalKotlinSourceSet.resolvableMetadataConfiguration: Configurat
*/
configuration.dependencies.addAllLater(project.listProvider {
getVisibleSourceSetsFromAssociateCompilations(this).flatMap { sourceSet ->
project.configurations.getByName(sourceSet.apiConfigurationName).allDependencies +
project.configurations.getByName(sourceSet.implementationConfigurationName).allDependencies +
project.configurations.getByName(sourceSet.compileOnlyConfigurationName).allDependencies
sourceSet.internal.compileDependenciesConfigurations.flatMap { it.allDependencies }
}
})
val allCompileMetadataConfiguration = project.allCompileMetadataConfiguration
/* Ensure consistent dependency resolution result within the whole module */
configuration.shouldResolveConsistentlyWith(allCompileMetadataConfiguration)
allCompileMetadataConfiguration.copyAttributesTo(
project,
dest = configuration
)
configureMetadataDependenciesConfigurations(configuration)
// needed for old IDEs
configureLegacyMetadataDependenciesConfigurations(configuration)
configuration
}
private val InternalKotlinSourceSet.compileDependenciesConfigurations: List<Configuration>
get() = listOf(
project.configurations.getByName(apiConfigurationName),
project.configurations.getByName(implementationConfigurationName),
project.configurations.getByName(compileOnlyConfigurationName),
)
/**
Older IDEs still rely on resolving the metadata configurations explicitly.
Dependencies will be coming from extending the newer 'resolvableMetadataConfiguration'.
@@ -77,7 +75,7 @@ Dependencies will be coming from extending the newer 'resolvableMetadataConfigur
the intransitiveMetadataConfigurationName will not extend this mechanism, since it only
relies on dependencies being added explicitly by the Kotlin Gradle Plugin
*/
private fun InternalKotlinSourceSet.configureMetadataDependenciesConfigurations(resolvableMetadataConfiguration: Configuration) {
private fun InternalKotlinSourceSet.configureLegacyMetadataDependenciesConfigurations(resolvableMetadataConfiguration: Configuration) {
@Suppress("DEPRECATION")
listOf(
apiMetadataConfigurationName,
@@ -90,27 +88,55 @@ private fun InternalKotlinSourceSet.configureMetadataDependenciesConfigurations(
}
}
/**
* Configuration containing all compile dependencies from *all* source sets.
* This configuration is used to provide a dependency 'consistency scope' for
* the [InternalKotlinSourceSet.resolvableMetadataConfiguration]
*/
private val Project.allCompileMetadataConfiguration
get(): Configuration = configurations.findResolvable("allSourceSetsCompileDependenciesMetadata")
?: configurations.createResolvable("allSourceSetsCompileDependenciesMetadata").also { configuration ->
configuration.usesPlatformOf(multiplatformExtension.metadata())
configuration.attributes.setAttribute(Usage.USAGE_ATTRIBUTE, project.usageByName(KotlinUsages.KOTLIN_METADATA))
configuration.attributes.setAttribute(Category.CATEGORY_ATTRIBUTE, project.categoryByName(Category.LIBRARY))
kotlinExtension.sourceSets.all { sourceSet ->
configuration.extendsFrom(configurations.getByName(sourceSet.apiConfigurationName))
configuration.extendsFrom(configurations.getByName(sourceSet.implementationConfigurationName))
configuration.extendsFrom(configurations.getByName(sourceSet.compileOnlyConfigurationName))
}
}
private fun Configuration.configureMetadataDependenciesAttribute(project: Project): Configuration = apply {
usesPlatformOf(project.multiplatformExtension.metadata())
attributes.setAttribute(Usage.USAGE_ATTRIBUTE, project.usageByName(KotlinUsages.KOTLIN_METADATA))
attributes.setAttribute(Category.CATEGORY_ATTRIBUTE, project.categoryByName(Category.LIBRARY))
}
private inline fun <reified T> Project.listProvider(noinline provider: () -> List<T>): Provider<List<T>> {
return project.objects.listProperty<T>().apply {
set(project.provider(provider))
}
}
/**
* Ensure a consistent dependencies resolution result between common source sets and actual
* See [ResolvableMetadataConfigurationTest] for the cases where dependencies should resolve consistently
*/
internal val SetupConsistentMetadataDependenciesResolution = KotlinProjectSetupCoroutine {
KotlinPluginLifecycle.Stage.AfterFinaliseRefinesEdges.await()
val sourceSets = multiplatformExtension.awaitSourceSets()
val sourceSetsBySourceSetTree = mutableMapOf<KotlinSourceSetTree?, MutableSet<KotlinSourceSet>>()
for (sourceSet in sourceSets) {
val trees = sourceSet.internal.compilations.map { KotlinSourceSetTree.orNull(it) }
trees.forEach { tree -> sourceSetsBySourceSetTree.getOrPut(tree) { mutableSetOf() }.add(sourceSet) }
}
for ((sourceSetTree, sourceSetsOfTree) in sourceSetsBySourceSetTree) {
val configurationName = when(sourceSetTree) {
null -> continue // for unknown trees there should be no relation between source sets, so just skip
KotlinSourceSetTree.main -> "allSourceSetsCompileDependenciesMetadata"
else -> lowerCamelCaseName("all", sourceSetTree.name, "SourceSetsCompileDependenciesMetadata")
}
configureConsistentDependencyResolution(sourceSetsOfTree, configurationName)
}
}
private fun Project.configureConsistentDependencyResolution(groupOfSourceSets: Collection<KotlinSourceSet>, configurationName: String) {
if (groupOfSourceSets.isEmpty()) return
val configuration = configurations.createResolvable(configurationName)
configuration.configureMetadataDependenciesAttribute(project)
val allVisibleSourceSets = groupOfSourceSets + groupOfSourceSets.flatMap { getVisibleSourceSetsFromAssociateCompilations(it) }
val extenders = allVisibleSourceSets.flatMap { it.internal.compileDependenciesConfigurations }
configuration.extendsFrom(*extenders.toTypedArray())
groupOfSourceSets.forEach { it.internal.resolvableMetadataConfiguration.shouldResolveConsistentlyWith(configuration) }
// Make actual compilation classpaths/libraries configurations to have the same consistent dependencies
groupOfSourceSets
.flatMap { it.internal.compilations }
.toSet()
.forEach { project.configurations.getByName(it.compileDependencyConfigurationName).shouldResolveConsistentlyWith(configuration) }
}
@@ -77,6 +77,7 @@ internal fun Project.registerKotlinPluginExtensions() {
register(project, IdeMultiplatformImportActionSetupAction)
register(project, KotlinLLDBScriptSetupAction)
register(project, ExcludeDefaultPlatformDependenciesFromKotlinNativeCompileTasks)
register(project, SetupConsistentMetadataDependenciesResolution)
}
}
@@ -31,4 +31,4 @@ internal interface InternalKotlinSourceSet : KotlinSourceSet {
internal suspend fun InternalKotlinSourceSet.awaitPlatformCompilations(): Set<KotlinCompilation<*>> {
KotlinPluginLifecycle.Stage.AfterFinaliseRefinesEdges.await()
return compilations.filter { it !is KotlinMetadataCompilation }.toSet()
}
}
@@ -7,6 +7,7 @@
package org.jetbrains.kotlin.gradle.dependencyResolutionTests
import org.gradle.api.Project
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
@@ -17,15 +18,12 @@ import org.jetbrains.kotlin.gradle.plugin.ide.kotlinIdeMultiplatformImport
import org.jetbrains.kotlin.gradle.plugin.kotlinToolingVersion
import org.jetbrains.kotlin.gradle.plugin.mpp.resolvableMetadataConfiguration
import org.jetbrains.kotlin.gradle.plugin.sources.internal
import org.jetbrains.kotlin.gradle.util.applyMultiplatformPlugin
import org.jetbrains.kotlin.gradle.util.buildProject
import org.jetbrains.kotlin.gradle.util.enableDefaultStdlibDependency
import org.jetbrains.kotlin.gradle.util.enableDependencyVerification
import org.jetbrains.kotlin.gradle.util.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.fail
class ResolvableMetadataConfigurationTest {
class ResolvableMetadataConfigurationTest : SourceSetDependenciesResolution() {
@Test
fun `test - resolves consistent in project`() {
@@ -97,4 +95,92 @@ class ResolvableMetadataConfigurationTest {
binaryCoordinates(Regex("com.arkivanov.mvikotlin:mvikotlin(-*)?:.*:3.0.2")),
)
}
@Test
fun commonMainWithHigherVersion() {
assertSourceSetDependenciesResolution("commonMainWithHigherVersion.txt") { project ->
project.defaultTargets()
project.kotlin { linuxArm64() }
// commonMain depends on `lib:2.0`, so during dependency resolution,
// this version should win over jvmMain and linuxMain that depend on 1.0
api("jvmMain", "lib", "1.0")
api("linuxMain", "lib", "1.0")
api("commonMain", "lib", "2.0")
// for test source sets, it should work the same due to transitivity.
// because test code depends on the main with all its transitive dependencies.
// this is the general logic of `associatedWith` compilations.
api("commonTest", "lib", "1.0")
}
}
@Test
fun jvmMainWithHigherVersion() {
assertSourceSetDependenciesResolution("leafSourceSetWithHigherVersion.txt") { project ->
project.defaultTargets()
// jvmMain depends on 3.0.
// commonMain code is included in jvmMain compilation, so it should see the same version as jvmMain (3.0 wins here).
// commonMain code is included in jsMain compilation, so it should see the same version as commonMain (3.0 wins here).
// linuxX64Main should receive 3.0 version from commonMain transitively
api("jvmMain", "lib", "3.0")
api("jsMain", "lib", "2.0")
api("commonMain", "lib", "1.0")
}
}
@Test
fun nativeMainWithHigherVersion() {
assertSourceSetDependenciesResolution("leafSourceSetWithHigherVersion.txt") { project ->
project.defaultTargets()
/** Same as for [jvmMainWithHigherVersion] but for linuxX64 */
api("linuxX64Main", "lib", "3.0")
api("jsMain", "lib", "2.0")
api("commonMain", "lib", "1.0")
}
}
@Test
fun jsMainWithHigherVersion() {
assertSourceSetDependenciesResolution("leafSourceSetWithHigherVersion.txt") { project ->
project.defaultTargets()
/** Same as for [jvmMainWithHigherVersion] but for js */
api("jsMain", "lib", "3.0")
api("nativeMain", "lib", "2.0")
api("commonMain", "lib", "1.0")
}
}
@Test
fun commonTestShouldNotAffectMainSourceSets() {
assertSourceSetDependenciesResolution("commonTestShouldNotAffectMainSourceSets.txt") { project ->
project.defaultTargets()
/** Test source sets are not compiled together with the main code, but just depend on it.
Thus, by default, there is no need for the main code to receive the same dependency versions as tests.
However, the other way around works in the opposite. See, for example, test [commonMainWithHigherVersion] */
api("commonMain", "lib", "1.0")
api("commonTest", "lib", "2.0")
}
}
@Test
fun leafSourceSetsDependsOnDifferentVersionsAndCommonCodeDoesNot() {
assertSourceSetDependenciesResolution("leafSourceSetsDependsOnDifferentVersionsAndCommonCodeDoesNot.txt") { project ->
project.defaultTargets()
/** Even though commonMain has no dependency on lib, thus there is no connection between
* jvmMain and linuxX64Main their dependencies should be still resolved consistently.
* This is by design!
* It complies with the desire of having global dependencies for the whole project */
api("jvmMain", "lib", "1.0")
api("linuxX64Main", "lib", "2.0")
}
}
private fun Project.defaultTargets() {
kotlin { jvm(); linuxX64(); js(); applyDefaultHierarchyTemplate() }
}
}
@@ -261,18 +261,6 @@ class ConfigurationsTest : MultiplatformExtensionTest() {
project.evaluate()
fun HasKotlinDependencies.allDependenciesConfigurationNames() = listOfNotNull(
apiConfigurationName,
implementationConfigurationName,
compileOnlyConfigurationName,
runtimeOnlyConfigurationName
)
fun KotlinCompilation<*>.allCompilationDependenciesConfigurationNames() = allDependenciesConfigurationNames() + listOfNotNull(
compileDependencyConfigurationName,
runtimeDependencyConfigurationName,
)
project.kotlinExtension.targets.flatMap { it.compilations }.forEach { compilation ->
val compilationSourceSets = compilation.allKotlinSourceSets
val compilationConfigurationNames = compilation.allCompilationDependenciesConfigurationNames()
@@ -0,0 +1,21 @@
/*
* Copyright 2010-2024 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.util
import org.jetbrains.kotlin.gradle.plugin.HasKotlinDependencies
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
fun HasKotlinDependencies.allDependenciesConfigurationNames() = listOfNotNull(
apiConfigurationName,
implementationConfigurationName,
compileOnlyConfigurationName,
runtimeOnlyConfigurationName
)
fun KotlinCompilation<*>.allCompilationDependenciesConfigurationNames() = allDependenciesConfigurationNames() + listOfNotNull(
compileDependencyConfigurationName,
runtimeDependencyConfigurationName,
)
@@ -0,0 +1,28 @@
commonMain
test:lib:2.0
commonTest
test:lib:2.0
jsMain
test:lib:2.0
jsTest
test:lib:2.0
jvmMain
test:lib:2.0
jvmTest
test:lib:2.0
linuxArm64Main
test:lib:2.0
linuxArm64Test
test:lib:2.0
linuxMain
test:lib:2.0
linuxTest
test:lib:2.0
linuxX64Main
test:lib:2.0
linuxX64Test
test:lib:2.0
nativeMain
test:lib:2.0
nativeTest
test:lib:2.0
@@ -0,0 +1,24 @@
commonMain
test:lib:1.0
commonTest
test:lib:2.0
jsMain
test:lib:1.0
jsTest
test:lib:2.0
jvmMain
test:lib:1.0
jvmTest
test:lib:2.0
linuxMain
test:lib:1.0
linuxTest
test:lib:2.0
linuxX64Main
test:lib:1.0
linuxX64Test
test:lib:2.0
nativeMain
test:lib:1.0
nativeTest
test:lib:2.0
@@ -0,0 +1,24 @@
commonMain
test:lib:3.0
commonTest
test:lib:3.0
jsMain
test:lib:3.0
jsTest
test:lib:3.0
jvmMain
test:lib:3.0
jvmTest
test:lib:3.0
linuxMain
test:lib:3.0
linuxTest
test:lib:3.0
linuxX64Main
test:lib:3.0
linuxX64Test
test:lib:3.0
nativeMain
test:lib:3.0
nativeTest
test:lib:3.0
@@ -0,0 +1,24 @@
commonMain
commonTest
jsMain
jsTest
jvmMain
test:lib:2.0
jvmTest
test:lib:2.0
linuxMain
linuxTest
test:lib:2.0
linuxX64Main
test:lib:2.0
linuxX64Test
test:lib:2.0
nativeMain
nativeTest
test:lib:2.0