Fix the localToProject attribute schema for Gradle 4.10.2+

In Gradle 4.10.2, a change was made in the attributes disambiguation
process: if some of the candidates have a single attribute value and
others don't have that attribute (i.e. have a null value), the
disambiguation rule for that attribute doesn't get called.
Effectively, null values are excluded from disambiguation, but still may
cause ambiguity (sic!)

See: https://github.com/gradle/gradle/issues/6747#issuecomment-445921139

This change affected our attribute `localToProject` that we use to
disambiguate the deprecated but still consumable configurations like
`compile`, `runtime`, `testCompile`, `testRuntime` from those
configurations which should have priority during project dependency
resolution: the `*Element` ones. Our scheme marked the former
configurations with the attribute and the latter were not marked, with
a disambiguation rule that preferred null values.

To fix this logic with Gradle 4.10.2, we instead mark both kinds of
configurations with the attribute, which now has a value 'public'
that is preferred by the rule over the other values. We also make sure
that the attribute doesn't leak into the published Gradle metadata, as
it is only needed for project-to-project dependencies resolution.

Issue #KT-28795 Fixed
This commit is contained in:
Sergey Igushkin
2018-12-12 15:58:13 +03:00
parent 2349aaf488
commit 557fb07f44
6 changed files with 37 additions and 16 deletions
@@ -4,6 +4,7 @@
*/ */
package org.jetbrains.kotlin.gradle package org.jetbrains.kotlin.gradle
import org.jetbrains.kotlin.gradle.plugin.ProjectLocalConfigurations
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMultiplatformPlugin import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMultiplatformPlugin
import org.jetbrains.kotlin.gradle.plugin.mpp.UnusedSourceSetsChecker import org.jetbrains.kotlin.gradle.plugin.mpp.UnusedSourceSetsChecker
import org.jetbrains.kotlin.gradle.plugin.sources.METADATA_CONFIGURATION_NAME_SUFFIX import org.jetbrains.kotlin.gradle.plugin.sources.METADATA_CONFIGURATION_NAME_SUFFIX
@@ -75,6 +76,9 @@ class NewMultiplatformIT : BaseGradleIT() {
Assert.assertTrue("$it should exist", groupDir.resolve(it).exists()) Assert.assertTrue("$it should exist", groupDir.resolve(it).exists())
} }
val gradleMetadata = groupDir.resolve("sample-lib/1.0/sample-lib-1.0.module").readText()
assertFalse(gradleMetadata.contains(ProjectLocalConfigurations.ATTRIBUTE.name))
listOf(jvmJarName, jsJarName, metadataJarName, wasmKlibName, nativeKlibName).forEach { listOf(jvmJarName, jsJarName, metadataJarName, wasmKlibName, nativeKlibName).forEach {
val pom = groupDir.resolve(it.replaceAfterLast('.', "pom")) val pom = groupDir.resolve(it.replaceAfterLast('.', "pom"))
Assert.assertTrue( Assert.assertTrue(
@@ -448,11 +448,13 @@ internal abstract class AbstractKotlinPlugin(
if (kotlinTarget.platformType != KotlinPlatformType.common) { if (kotlinTarget.platformType != KotlinPlatformType.common) {
project.configurations.getByName(kotlinTarget.apiElementsConfigurationName).run { project.configurations.getByName(kotlinTarget.apiElementsConfigurationName).run {
attributes.attribute(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerApiUsage(kotlinTarget)) attributes.attribute(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerApiUsage(kotlinTarget))
setupAsPublicConfigurationIfSupported(kotlinTarget)
usesPlatformOf(kotlinTarget) usesPlatformOf(kotlinTarget)
} }
project.configurations.getByName(kotlinTarget.runtimeElementsConfigurationName).run { project.configurations.getByName(kotlinTarget.runtimeElementsConfigurationName).run {
attributes.attribute(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerRuntimeUsage(kotlinTarget)) attributes.attribute(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerRuntimeUsage(kotlinTarget))
setupAsPublicConfigurationIfSupported(kotlinTarget)
usesPlatformOf(kotlinTarget) usesPlatformOf(kotlinTarget)
} }
} }
@@ -187,6 +187,7 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
extendsFrom(runtimeConfiguration) extendsFrom(runtimeConfiguration)
} }
usesPlatformOf(target) usesPlatformOf(target)
setupAsPublicConfigurationIfSupported(target)
} }
if (mainCompilation is KotlinCompilationToRunnableFiles<*>) { if (mainCompilation is KotlinCompilationToRunnableFiles<*>) {
@@ -199,6 +200,7 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
val runtimeConfiguration = configurations.maybeCreate(mainCompilation.deprecatedRuntimeConfigurationName) val runtimeConfiguration = configurations.maybeCreate(mainCompilation.deprecatedRuntimeConfigurationName)
extendsFrom(implementationConfiguration, runtimeOnlyConfiguration, runtimeConfiguration) extendsFrom(implementationConfiguration, runtimeOnlyConfiguration, runtimeConfiguration)
usesPlatformOf(target) usesPlatformOf(target)
setupAsPublicConfigurationIfSupported(target)
} }
defaultConfiguration.extendsFrom(runtimeElementsConfiguration) defaultConfiguration.extendsFrom(runtimeElementsConfiguration)
} else { } else {
@@ -10,8 +10,11 @@ import org.gradle.api.attributes.*
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaTarget
import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast
internal object ProjectLocalConfigurations { object ProjectLocalConfigurations {
val ATTRIBUTE = Attribute.of("org.jetbrains.kotlin.localToProject", String::class.java) val ATTRIBUTE: Attribute<String> = Attribute.of("org.jetbrains.kotlin.localToProject", String::class.java)
const val PUBLIC_VALUE = "public"
const val LOCAL_TO_PROJECT_PREFIX = "local to "
fun setupAttributesMatchingStrategy(attributesSchema: AttributesSchema) = with(attributesSchema) { fun setupAttributesMatchingStrategy(attributesSchema: AttributesSchema) = with(attributesSchema) {
attribute(ATTRIBUTE) { attribute(ATTRIBUTE) {
@@ -30,9 +33,8 @@ internal object ProjectLocalConfigurations {
class ProjectLocalDisambiguation : AttributeDisambiguationRule<String> { class ProjectLocalDisambiguation : AttributeDisambiguationRule<String> {
override fun execute(details: MultipleCandidatesDetails<String?>) = with(details) { override fun execute(details: MultipleCandidatesDetails<String?>) = with(details) {
if (candidateValues.contains(null)) { if (candidateValues.contains(PUBLIC_VALUE)) {
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") closestMatch(PUBLIC_VALUE)
closestMatch(null as String?)
} }
} }
} }
@@ -46,7 +48,15 @@ internal fun Configuration.setupAsLocalTargetSpecificConfigurationIfSupported(ta
(target !is KotlinWithJavaTarget<*> || target.platformType != KotlinPlatformType.common) (target !is KotlinWithJavaTarget<*> || target.platformType != KotlinPlatformType.common)
) { ) {
usesPlatformOf(target) usesPlatformOf(target)
attributes.attribute(ProjectLocalConfigurations.ATTRIBUTE, target.project.path) attributes.attribute(ProjectLocalConfigurations.ATTRIBUTE, ProjectLocalConfigurations.LOCAL_TO_PROJECT_PREFIX + target.project.path)
}
}
internal fun Configuration.setupAsPublicConfigurationIfSupported(target: KotlinTarget) {
if (gradleVersionSupportsAttributeRules &&
(target !is KotlinWithJavaTarget<*> || target.platformType != KotlinPlatformType.common)
) {
attributes.attribute(ProjectLocalConfigurations.ATTRIBUTE, ProjectLocalConfigurations.PUBLIC_VALUE)
} }
} }
@@ -20,19 +20,25 @@ import org.gradle.api.attributes.AttributeContainer
import java.util.* import java.util.*
// TODO better implementation: attribute invariants (no attrs with same name and different types allowed), thread safety? // TODO better implementation: attribute invariants (no attrs with same name and different types allowed), thread safety?
class HierarchyAttributeContainer(val parent: AttributeContainer?) : AttributeContainer { class HierarchyAttributeContainer(
val parent: AttributeContainer?,
val filterParentAttributes: (Attribute<*>) -> Boolean = { true }
) : AttributeContainer {
private val attributesMap = Collections.synchronizedMap(mutableMapOf<Attribute<*>, Any>()) private val attributesMap = Collections.synchronizedMap(mutableMapOf<Attribute<*>, Any>())
private fun getFilteredParentAttribute(key: Attribute<*>) =
if (parent != null && filterParentAttributes(key)) parent.getAttribute(key) else null
override fun contains(key: Attribute<*>): Boolean = override fun contains(key: Attribute<*>): Boolean =
attributesMap.contains(key) || parent?.contains(key) ?: false attributesMap.contains(key) || getFilteredParentAttribute(key) != null
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : Any?> getAttribute(key: Attribute<T>?): T? = override fun <T : Any?> getAttribute(key: Attribute<T>?): T? =
attributesMap.get(key as Attribute<*>) as T? ?: parent?.getAttribute(key) attributesMap.get(key as Attribute<*>) as T? ?: getFilteredParentAttribute(key) as T?
override fun isEmpty(): Boolean = attributesMap.isEmpty() && (parent?.isEmpty ?: true) override fun isEmpty(): Boolean = attributesMap.isEmpty() && (parent?.keySet().orEmpty().filter(filterParentAttributes).isEmpty())
override fun keySet(): Set<Attribute<*>> = attributesMap.keys + parent?.keySet().orEmpty() override fun keySet(): Set<Attribute<*>> = attributesMap.keys + parent?.keySet().orEmpty().filter(filterParentAttributes)
override fun <T : Any?> attribute(key: Attribute<T>?, value: T): AttributeContainer { override fun <T : Any?> attribute(key: Attribute<T>?, value: T): AttributeContainer {
val checkedValue = requireNotNull(value as Any?) { "null values for attributes are not supported" } val checkedValue = requireNotNull(value as Any?) { "null values for attributes are not supported" }
@@ -13,10 +13,7 @@ import org.gradle.api.capabilities.Capability
import org.gradle.api.component.ComponentWithVariants import org.gradle.api.component.ComponentWithVariants
import org.gradle.api.internal.component.SoftwareComponentInternal import org.gradle.api.internal.component.SoftwareComponentInternal
import org.gradle.api.internal.component.UsageContext import org.gradle.api.internal.component.UsageContext
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetComponent
import org.jetbrains.kotlin.utils.ifEmpty import org.jetbrains.kotlin.utils.ifEmpty
class KotlinSoftwareComponent( class KotlinSoftwareComponent(
@@ -72,7 +69,7 @@ internal class KotlinPlatformUsageContext(
configuration.artifacts configuration.artifacts
override fun getAttributes(): AttributeContainer = override fun getAttributes(): AttributeContainer =
configuration.outgoing.attributes HierarchyAttributeContainer(configuration.outgoing.attributes) { it != ProjectLocalConfigurations.ATTRIBUTE }
override fun getCapabilities(): Set<Capability> = emptySet() override fun getCapabilities(): Set<Capability> = emptySet()