Add Kotlin-specific Usage attribute values

When a non-Kotlin (JVM) project resolves an MPP library dependency,
it expects only one variant per Java Usage value ('java-api',
'java-runtime-jars'). If there's more than one, it will report
ambiguity and fail.

So we need to avoid creating non-JVM variants with Java Usage values
to ensure that non-Kotlin consumers can resolve the dependencies on
an MPP library with a single JVM target (if there's more than one
JVM target, there will still be ambiguity, but that's quite what one
would expect and what can be solved with custom attributes).

Instead, we define two Kotlin-specific Usage values, 'kotlin-api' and
'kotlin-runtime', so that:

* input configurations with these Usage values can consume plain Java
  artifacts, for compatibility with plain old Kotlin modules

* if a consumer does not request a Kotlin usage and receives the two,
  let it use the runtime one as it is more complete

We then use the two Usage values on those Kotlin input and output
configurations which are not JVM-related, and leave the Java Usages
for the JVM targets.
This commit is contained in:
Sergey Igushkin
2018-09-21 22:43:19 +03:00
parent 76b51b1058
commit 1abcfc76df
11 changed files with 167 additions and 26 deletions
@@ -584,6 +584,25 @@ class NewMultiplatformIT : BaseGradleIT() {
}
}
@Test
fun testConsumeMppLibraryFromNonKotlinProject() {
val libRepo = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
build("publish") { assertSuccessful() }
projectDir.resolve("repo")
}
with(Project("sample-app-without-kotlin", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
gradleBuildScript().appendText("\nrepositories { maven { url '${libRepo.toURI()}' } }")
build("assemble") {
assertSuccessful()
assertTasksExecuted(":compileJava")
assertFileExists("build/classes/java/main/A.class")
}
}
}
@Test
fun testNativeTests() = with(Project("new-mpp-native-tests", gradleVersion)) {
val testTasks = listOf("macos64Test", "linux64Test", "mingw64Test")
@@ -0,0 +1,7 @@
plugins {
id 'java'
}
dependencies {
implementation 'com.example:sample-lib:1.0'
}
@@ -0,0 +1,12 @@
/*
* 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.
*/
import com.example.lib.CommonKt;
public class A {
public void useKotlinClass() {
CommonKt.id(123);
}
}
@@ -6,6 +6,7 @@ import com.android.builder.model.SourceProvider
import groovy.lang.Closure
import org.gradle.api.*
import org.gradle.api.artifacts.ExternalDependency
import org.gradle.api.attributes.Usage
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.FileCollection
import org.gradle.api.file.SourceDirectorySet
@@ -436,8 +437,15 @@ internal abstract class AbstractKotlinPlugin(
// Don't set the attributes for common module; otherwise their 'common' platform won't be compatible with the one in
// platform-specific modules
if (kotlinTarget.platformType != KotlinPlatformType.common) {
project.configurations.getByName(kotlinTarget.apiElementsConfigurationName).usesPlatformOf(kotlinTarget)
project.configurations.getByName(kotlinTarget.runtimeElementsConfigurationName).usesPlatformOf(kotlinTarget)
project.configurations.getByName(kotlinTarget.apiElementsConfigurationName).run {
attributes.attribute(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerApiUsage(kotlinTarget))
usesPlatformOf(kotlinTarget)
}
project.configurations.getByName(kotlinTarget.runtimeElementsConfigurationName).run {
attributes.attribute(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerRuntimeUsage(kotlinTarget))
usesPlatformOf(kotlinTarget)
}
}
}
@@ -26,11 +26,11 @@ import org.gradle.internal.reflect.Instantiator
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry
import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMultiplatformPlugin
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSetFactory
import org.jetbrains.kotlin.gradle.plugin.sources.KotlinSourceSetFactory
import org.jetbrains.kotlin.gradle.tasks.KOTLIN_COMPILER_EMBEDDABLE
import org.jetbrains.kotlin.gradle.tasks.KOTLIN_MODULE_GROUP
import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast
import java.io.FileNotFoundException
import java.util.*
import javax.inject.Inject
@@ -79,6 +79,7 @@ abstract class KotlinBasePluginWrapper(
private fun setupAttributeMatchingStrategy(project: Project) = with(project.dependencies.attributesSchema) {
KotlinPlatformType.setupAttributesMatchingStrategy(this)
KotlinUsages.setupAttributesMatchingStrategy(this)
}
internal abstract fun getPlugin(
@@ -172,7 +172,7 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
isVisible = false
isCanBeResolved = false
isCanBeConsumed = true
attributes.attribute<Usage>(USAGE_ATTRIBUTE, project.usageByName(Usage.JAVA_API))
attributes.attribute<Usage>(USAGE_ATTRIBUTE, KotlinUsages.producerApiUsage(target))
extendsFrom(configurations.maybeCreate(mainCompilation.apiConfigurationName))
if (mainCompilation is KotlinCompilationToRunnableFiles) {
val runtimeConfiguration = configurations.maybeCreate(mainCompilation.deprecatedRuntimeConfigurationName)
@@ -187,7 +187,7 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
isVisible = false
isCanBeConsumed = true
isCanBeResolved = false
attributes.attribute<Usage>(USAGE_ATTRIBUTE, project.usageByName(Usage.JAVA_RUNTIME_JARS))
attributes.attribute<Usage>(USAGE_ATTRIBUTE, KotlinUsages.producerRuntimeUsage(target))
val runtimeConfiguration = configurations.maybeCreate(mainCompilation.deprecatedRuntimeConfigurationName)
extendsFrom(implementationConfiguration, runtimeOnlyConfiguration, runtimeConfiguration)
usesPlatformOf(target)
@@ -277,7 +277,7 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
usesPlatformOf(target)
isVisible = false
isCanBeConsumed = false
attributes.attribute(USAGE_ATTRIBUTE, compilation.target.project.usageByName(Usage.JAVA_API))
attributes.attribute(USAGE_ATTRIBUTE, KotlinUsages.consumerApiUsage(compilation.target))
description = "Compile classpath for $compilation."
}
@@ -303,7 +303,7 @@ abstract class AbstractKotlinTargetConfigurator<KotlinTargetType : KotlinTarget>
isVisible = false
isCanBeConsumed = false
isCanBeResolved = true
attributes.attribute(USAGE_ATTRIBUTE, compilation.target.project.usageByName(Usage.JAVA_RUNTIME))
attributes.attribute(USAGE_ATTRIBUTE, KotlinUsages.consumerRuntimeUsage(compilation.target))
description = "Runtime classpath of $compilation."
}
}
@@ -31,11 +31,6 @@ class KotlinSoftwareComponent(
kotlinTargets.map { it.component }.toSet()
override fun getName(): String = name
companion object {
fun kotlinApiUsage(project: Project) = project.usageByName(Usage.JAVA_API)
fun kotlinRuntimeUsage(project: Project) = project.usageByName(Usage.JAVA_RUNTIME)
}
}
open class KotlinVariant(
@@ -93,10 +88,10 @@ internal class KotlinPlatformUsageContext(
) : UsageContext {
override fun getUsage(): Usage = usage
override fun getName(): String = kotlinTarget.targetName + when (usage.name) {
Usage.JAVA_API -> "-api"
Usage.JAVA_RUNTIME -> "-runtime"
else -> error("unexpected usage")
override fun getName(): String = kotlinTarget.targetName + when (dependencyConfigurationName) {
kotlinTarget.apiElementsConfigurationName -> "-api"
kotlinTarget.runtimeElementsConfigurationName -> "-runtime"
else -> error("unexpected configuration")
}
private val configuration: Configuration
@@ -0,0 +1,89 @@
/*
* 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.mpp
import org.gradle.api.attributes.*
import org.gradle.api.attributes.Usage.*
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.androidJvm
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.jvm
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.usageByName
import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast
object KotlinUsages {
const val KOTLIN_API = "kotlin-api"
const val KOTLIN_RUNTIME = "kotlin-runtime"
val values = setOf(KOTLIN_API, KOTLIN_RUNTIME)
private val jvmPlatformTypes: Set<KotlinPlatformType> = setOf(jvm, androidJvm)
internal fun consumerApiUsage(target: KotlinTarget) = target.project.usageByName(
when (target.platformType) {
in jvmPlatformTypes -> JAVA_API
else -> KOTLIN_API
}
)
internal fun consumerRuntimeUsage(target: KotlinTarget) = target.project.usageByName(
when (target.platformType) {
in jvmPlatformTypes -> JAVA_RUNTIME
else -> KOTLIN_RUNTIME
}
)
internal fun producerApiUsage(target: KotlinTarget) = target.project.usageByName(
when (target.platformType) {
in jvmPlatformTypes -> JAVA_API
else -> KOTLIN_API
}
)
internal fun producerRuntimeUsage(target: KotlinTarget) = target.project.usageByName(
when (target.platformType) {
in jvmPlatformTypes -> JAVA_RUNTIME_JARS
else -> KOTLIN_RUNTIME
}
)
private class KotlinJavaRuntimeJarsCompatibility : AttributeCompatibilityRule<Usage> {
// When Gradle resolves a plain old JAR dependency with no metadata attached, the Usage attribute of that dependency
// is 'java-runtime-jars'. This rule tells Gradle that Kotlin consumers can consume plain old JARs:
override fun execute(details: CompatibilityCheckDetails<Usage>) = with(details) {
when {
consumerValue?.name == KOTLIN_API && producerValue?.name == JAVA_API -> compatible()
consumerValue?.name in values && producerValue?.name == JAVA_RUNTIME_JARS -> compatible()
}
}
}
private class KotlinUsagesDisambiguation : AttributeDisambiguationRule<Usage> {
override fun execute(details: MultipleCandidatesDetails<Usage?>) = with(details) {
val candidateNames = candidateValues.map { it?.name }.toSet()
// 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)) {
details.closestMatch(candidateValues.single { it?.name == KOTLIN_RUNTIME }!!)
}
if (JAVA_API in candidateNames && JAVA_RUNTIME_JARS in candidateNames && values.none { it in candidateNames }) {
details.closestMatch(candidateValues.single { it?.name == JAVA_API }!!)
}
if (JAVA_RUNTIME_CLASSES in candidateNames && JAVA_RUNTIME_RESOURCES in candidateNames && KOTLIN_RUNTIME in candidateNames) {
closestMatch(candidateValues.single { it?.name == KOTLIN_RUNTIME }!!)
}
}
}
internal fun setupAttributesMatchingStrategy(attributesSchema: AttributesSchema) {
if (isGradleVersionAtLeast(4, 0)) {
attributesSchema.attribute(Usage.USAGE_ATTRIBUTE) { strategy ->
strategy.compatibilityRules.add(KotlinJavaRuntimeJarsCompatibility::class.java)
strategy.disambiguationRules.add(KotlinUsagesDisambiguation::class.java)
}
}
}
}
@@ -12,6 +12,8 @@ import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.AttributeContainer
import org.gradle.api.attributes.Usage.JAVA_API
import org.gradle.api.attributes.Usage.JAVA_RUNTIME_JARS
import org.gradle.api.internal.component.UsageContext
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.publish.maven.MavenPublication
@@ -53,20 +55,24 @@ abstract class AbstractKotlinTarget (
KotlinVariant(this)
}
override fun createUsageContexts(): Set<UsageContext> =
setOf(
override fun createUsageContexts(): Set<UsageContext> {
// Here, `JAVA_API` and `JAVA_RUNTIME_JARS` are used intentionally as Gradle needs this for
// ordering of the usage contexts (prioritizing the dependencies);
// These Java usages should not be replaced with the custom Kotlin usages.
return listOfNotNull(
KotlinPlatformUsageContext(
project, this, KotlinSoftwareComponent.kotlinApiUsage(project), apiElementsConfigurationName
)
) + if (compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) is KotlinCompilationToRunnableFiles)
setOf(
project, this, project.usageByName(JAVA_API),
apiElementsConfigurationName
),
if (compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) is KotlinCompilationToRunnableFiles)
KotlinPlatformUsageContext(
project,
this,
KotlinSoftwareComponent.kotlinRuntimeUsage(project),
project, this, project.usageByName(JAVA_RUNTIME_JARS),
runtimeElementsConfigurationName
)
) else emptyList()
else null
).toSet()
}
@Suppress("UNCHECKED_CAST")
internal val publicationConfigureActions =
@@ -11,6 +11,8 @@ import org.gradle.api.attributes.Usage
import org.gradle.api.internal.file.FileResolver
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
import org.jetbrains.kotlin.gradle.plugin.usageByName
import java.io.File
internal abstract class KotlinSourceSetFactory<T : KotlinSourceSet> internal constructor(
@@ -67,6 +69,7 @@ internal class DefaultKotlinSourceSetFactory(
dependencyConfigurationWithMetadata.forEach { (configurationName, metadataName) ->
project.configurations.maybeCreate(metadataName).apply {
attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.common)
attributes.attribute(Usage.USAGE_ATTRIBUTE, project.usageByName(KotlinUsages.KOTLIN_API))
isVisible = false
isCanBeConsumed = false
extendsFrom(project.configurations.maybeCreate(configurationName))