From c4283de9cb4e095eddc1668b300a229775a283d4 Mon Sep 17 00:00:00 2001 From: Sergey Igushkin Date: Tue, 25 Sep 2018 16:32:02 +0300 Subject: [PATCH] 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 --- .../gradle/VariantAwareDependenciesIT.kt | 78 ++++++++++++++++++- .../kotlin/gradle/plugin/KotlinPlugin.kt | 4 + .../gradle/plugin/KotlinPluginWrapper.kt | 1 + .../gradle/plugin/KotlinTargetConfigurator.kt | 21 +++-- .../plugin/ProjectLocalConfigurations.kt | 53 +++++++++++++ .../kotlin/gradle/plugin/mpp/KotlinUsages.kt | 2 +- 6 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ProjectLocalConfigurations.kt diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/VariantAwareDependenciesIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/VariantAwareDependenciesIT.kt index 309af0789af..caae2409948 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/VariantAwareDependenciesIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/VariantAwareDependenciesIT.kt @@ -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() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPlugin.kt index 5e508309004..c0007093978 100755 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPlugin.kt @@ -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 diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt index 424f335b3cf..18852a68bd1 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt @@ -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( diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt index 94a9bd8f86f..70a8df7ac38 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt @@ -154,7 +154,10 @@ abstract class AbstractKotlinTargetConfigurator 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 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 } 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 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 } } } - - - 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( buildOutputCleanupRegistry: BuildOutputCleanupRegistry, createDefaultSourceSets: Boolean, diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ProjectLocalConfigurations.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ProjectLocalConfigurations.kt new file mode 100644 index 00000000000..4bbe44ab783 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ProjectLocalConfigurations.kt @@ -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 { + override fun execute(details: CompatibilityCheckDetails) { + details.compatible() + } + } + + class ProjectLocalDisambiguation : AttributeDisambiguationRule { + override fun execute(details: MultipleCandidatesDetails) = 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) \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinUsages.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinUsages.kt index 0ef36a937c5..979f073cf32 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinUsages.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinUsages.kt @@ -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 }) {