Mark deprecated Gradle configurations with the Kotlin platform attribute

The traditional Gradle/Java model assumes several configurations, which
are now deprecated, which are both `canBeConsumed = true` and
`canBeResolved = true`.

* compile, testCompile, etc.
* runtime, testRuntime, etc.
* default

These configurations need to somehow resolve correctly to an appropriate
platform-specific artifact when they contain an MPP library or project
dependency.

However, simply marking them with the Kotlin platform type attribute
would put these configurations under considerations during Gradle
variant aware depdendency resolution of project dependencies, which
in order would lead to ambiguity (e.g. `compile` vs `runtime` vs
`testCompile` vs ... vs `apiElements`).

To deprioritize these configurations during dependency resolution, we
mark them with a special attribute with a unique value in each project.
Given that the values are different in different projects, Gradle will
not choose a configuration marked by this attribute.

But we still need 'project(path: '...', configuration: '...')`
dependencies to work, and so, instead of rejecting those different
values of the attribute, we say that all values are compatible, but
when an ambiguity arises, choose the configurations not marked by this
attribute, so effectively eliminating them from resolution.

Issue #KT-27111 Fixed
This commit is contained in:
Sergey Igushkin
2018-09-25 16:32:02 +03:00
parent 3f24f8bd8d
commit c4283de9cb
6 changed files with 148 additions and 11 deletions
@@ -21,7 +21,9 @@ class VariantAwareDependenciesIT : BaseGradleIT() {
embedProject(innerProject)
gradleBuildScript(innerProject.projectName).appendText("\ndependencies { compile rootProject }")
testResolveAllConfigurations(innerProject.projectName)
testResolveAllConfigurations(innerProject.projectName) {
assertContains(">> :${innerProject.projectName}:runtime --> sample-lib-jvm6-1.0.jar")
}
}
}
@@ -34,7 +36,9 @@ class VariantAwareDependenciesIT : BaseGradleIT() {
embedProject(innerProject)
gradleBuildScript(innerProject.projectName).appendText("\nrepositories { jcenter() }; dependencies { compile rootProject }")
testResolveAllConfigurations(innerProject.projectName)
testResolveAllConfigurations(innerProject.projectName) {
assertContains(">> :${innerProject.projectName}:runtime --> sample-lib-nodejs-1.0.jar")
}
}
}
@@ -106,6 +110,76 @@ class VariantAwareDependenciesIT : BaseGradleIT() {
}
}
@Test
fun testMppResolvesJvmAndJsKtLibs() {
val outerProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
val innerJvmProject = Project("simpleProject")
val innerJsProject = Project("kotlin2JsInternalTest")
with(outerProject) {
embedProject(innerJvmProject)
embedProject(innerJsProject)
gradleBuildScript().appendText("\n" + """
dependencies {
jvm6Implementation project(':${innerJvmProject.projectName}')
jvm6TestRuntime project(':${innerJvmProject.projectName}')
nodeJsImplementation project(':${innerJsProject.projectName}')
nodeJsTestRuntime project(':${innerJsProject.projectName}')
}
""".trimIndent())
testResolveAllConfigurations(innerJvmProject.projectName)
}
}
@Test
fun testJvmKtAppDependsOnMppTestRuntime() {
val outerProject = Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")
val innerProject = Project("simpleProject")
with(outerProject) {
embedProject(innerProject)
gradleBuildScript(innerProject.projectName).appendText(
"\ndependencies { testCompile project(path: ':', configuration: 'jvm6TestRuntime') }"
)
testResolveAllConfigurations(innerProject.projectName) {
assertContains(">> :${innerProject.projectName}:testCompile --> sample-lib-jvm6-1.0.jar")
assertContains(">> :${innerProject.projectName}:testRuntime --> sample-lib-jvm6-1.0.jar")
}
}
}
@Test
fun testKtAppResolvesOldMpp() {
val outerProject = Project("multiplatformProject")
val innerJvmProject = Project("simpleProject")
val innerJsProject = Project("kotlin2JsInternalTest")
with(outerProject) {
embedProject(innerJvmProject)
embedProject(innerJsProject)
listOf(innerJvmProject to ":libJvm", innerJsProject to ":libJs").forEach { (project, dependency) ->
gradleBuildScript(project.projectName).appendText(
"\n" + """
configurations.create('foo')
dependencies {
foo project('$dependency')
compile project('$dependency')
foo project(':lib')
compile project(':lib')
}
""".trimIndent()
)
testResolveAllConfigurations(project.projectName)
}
}
}
private fun Project.embedProject(other: Project) {
setupWorkingDir()
other.setupWorkingDir()
@@ -433,6 +433,10 @@ internal abstract class AbstractKotlinPlugin(
AbstractKotlinTargetConfigurator.defineConfigurationsForCompilation(compilation, kotlinTarget, project.configurations)
}
project.configurations.getByName("default").apply {
setupAsLocalTargetSpecificConfigurationIfSupported(kotlinTarget)
}
// Setup the published configurations:
// Don't set the attributes for common module; otherwise their 'common' platform won't be compatible with the one in
// platform-specific modules
@@ -81,6 +81,7 @@ abstract class KotlinBasePluginWrapper(
private fun setupAttributeMatchingStrategy(project: Project) = with(project.dependencies.attributesSchema) {
KotlinPlatformType.setupAttributesMatchingStrategy(this)
KotlinUsages.setupAttributesMatchingStrategy(this)
ProjectLocalConfigurations.setupAttributesMatchingStrategy(this)
}
internal abstract fun getPlugin(
@@ -154,7 +154,10 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
val configurations = project.configurations
val defaultConfiguration = configurations.maybeCreate(target.defaultConfigurationName)
val defaultConfiguration = configurations.maybeCreate(target.defaultConfigurationName).apply {
setupAsLocalTargetSpecificConfigurationIfSupported(target)
}
val mainCompilation = target.compilations.maybeCreate(KotlinCompilation.MAIN_COMPILATION_NAME)
val compileConfiguration = configurations.maybeCreate(mainCompilation.deprecatedCompileConfigurationName)
@@ -240,6 +243,7 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
configurations: ConfigurationContainer
) {
val compileConfiguration = configurations.maybeCreate(compilation.deprecatedCompileConfigurationName).apply {
setupAsLocalTargetSpecificConfigurationIfSupported(target)
isVisible = false
isCanBeResolved = true // Needed for IDE import
description = "Dependencies for $compilation (deprecated, use '${compilation.implementationConfigurationName} ' instead)."
@@ -262,6 +266,7 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
}
val compileOnlyConfiguration = configurations.maybeCreate(compilation.compileOnlyConfigurationName).apply {
setupAsLocalTargetSpecificConfigurationIfSupported(target)
isVisible = false
isCanBeResolved = true // Needed for IDE import
description = "Compile only dependencies for $compilation."
@@ -278,6 +283,7 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
if (compilation is KotlinCompilationToRunnableFiles) {
val runtimeConfiguration = configurations.maybeCreate(compilation.deprecatedRuntimeConfigurationName).apply {
setupAsLocalTargetSpecificConfigurationIfSupported(target)
extendsFrom(compileConfiguration)
isVisible = false
isCanBeResolved = true // Needed for IDE import
@@ -303,16 +309,15 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
}
}
}
internal val KotlinCompilation.deprecatedCompileConfigurationName: String
get() = disambiguateName("compile")
internal val KotlinCompilationToRunnableFiles.deprecatedRuntimeConfigurationName: String
get() = disambiguateName("runtime")
}
}
internal val KotlinCompilation.deprecatedCompileConfigurationName: String
get() = disambiguateName("compile")
internal val KotlinCompilationToRunnableFiles.deprecatedRuntimeConfigurationName: String
get() = disambiguateName("runtime")
open class KotlinTargetConfigurator<KotlinCompilationType: KotlinCompilation>(
buildOutputCleanupRegistry: BuildOutputCleanupRegistry,
createDefaultSourceSets: Boolean,
@@ -0,0 +1,53 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.plugin
import org.gradle.api.artifacts.Configuration
import org.gradle.api.attributes.*
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaTarget
import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast
internal object ProjectLocalConfigurations {
val ATTRIBUTE = Attribute.of("org.jetbrains.kotlin.localToProject", String::class.java)
fun setupAttributesMatchingStrategy(attributesSchema: AttributesSchema) = with(attributesSchema) {
attribute(ATTRIBUTE) {
if (gradleVersionSupportsAttributeRules) {
it.compatibilityRules.add(ProjectLocalCompatibility::class.java)
it.disambiguationRules.add(ProjectLocalDisambiguation::class.java)
}
}
}
class ProjectLocalCompatibility : AttributeCompatibilityRule<String> {
override fun execute(details: CompatibilityCheckDetails<String>) {
details.compatible()
}
}
class ProjectLocalDisambiguation : AttributeDisambiguationRule<String> {
override fun execute(details: MultipleCandidatesDetails<String?>) = with(details) {
if (candidateValues.contains(null)) {
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
closestMatch(null as String?)
}
}
}
}
internal fun Configuration.setupAsLocalTargetSpecificConfigurationIfSupported(target: KotlinTarget) {
if (gradleVersionSupportsAttributeRules &&
// don't setup in old MPP common modules, as their output configurations with KotlinPlatformType attribute would
// fail to resolve as transitive dependencies of the platform modules, just as we don't mark their
// `api/RuntimeElements` with the KotlinPlatformType
(target !is KotlinWithJavaTarget || target.platformType != KotlinPlatformType.COMMON)
) {
usesPlatformOf(target)
attributes.attribute(ProjectLocalConfigurations.ATTRIBUTE, target.project.path)
}
}
internal val gradleVersionSupportsAttributeRules = isGradleVersionAtLeast(4, 0)
@@ -66,7 +66,7 @@ object KotlinUsages {
// if both API and runtime artifacts are chosen according to the compatibility rules, then
// the consumer requested nothing specific, so provide them with the runtime variant, which is more complete:
if (candidateNames == setOf(KOTLIN_RUNTIME, KOTLIN_API)) {
if (candidateNames.filterNotNull().toSet() == setOf(KOTLIN_RUNTIME, KOTLIN_API)) {
details.closestMatch(candidateValues.single { it?.name == KOTLIN_RUNTIME }!!)
}
if (JAVA_API in candidateNames && JAVA_RUNTIME_JARS in candidateNames && values.none { it in candidateNames }) {