From 1abcfc76df272f012d3c5897bb0ca944acb63e2b Mon Sep 17 00:00:00 2001 From: Sergey Igushkin Date: Fri, 21 Sep 2018 22:43:19 +0300 Subject: [PATCH] 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. --- .../kotlin/gradle/NewMultiplatformIT.kt | 19 ++++ .../sample-app-without-kotlin/build.gradle | 7 ++ .../sample-app-without-kotlin/settings.gradle | 1 + .../src/main/java/A.java | 12 +++ .../kotlin/gradle/plugin/KotlinPlugin.kt | 12 ++- .../gradle/plugin/KotlinPluginWrapper.kt | 3 +- .../gradle/plugin/KotlinTargetConfigurator.kt | 8 +- .../plugin/mpp/KotlinSoftwareComponent.kt | 13 +-- .../kotlin/gradle/plugin/mpp/KotlinUsages.kt | 89 +++++++++++++++++++ .../kotlin/gradle/plugin/mpp/kotlinTargets.kt | 26 +++--- .../plugin/sources/KotlinSourceSetFactory.kt | 3 + 11 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/build.gradle create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/settings.gradle create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/src/main/java/A.java create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinUsages.kt diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt index c22cf69a0fe..f9571cadee2 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt @@ -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") diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/build.gradle b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/build.gradle new file mode 100644 index 00000000000..1fba99ae41c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/build.gradle @@ -0,0 +1,7 @@ +plugins { + id 'java' +} + +dependencies { + implementation 'com.example:sample-lib:1.0' +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/settings.gradle b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/settings.gradle new file mode 100644 index 00000000000..9dddddd9eb7 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/settings.gradle @@ -0,0 +1 @@ +enableFeaturePreview('GRADLE_METADATA') \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/src/main/java/A.java b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/src/main/java/A.java new file mode 100644 index 00000000000..bfc074b5232 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-lib-and-app/sample-app-without-kotlin/src/main/java/A.java @@ -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); + } +} \ No newline at end of file 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 501fbdb9030..4136b59d3c8 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 @@ -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) + } } } 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 ad215daff08..9cd13b38a70 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 @@ -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( 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 80bef6f6210..08f7c289ae5 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 @@ -172,7 +172,7 @@ abstract class AbstractKotlinTargetConfigurator isVisible = false isCanBeResolved = false isCanBeConsumed = true - attributes.attribute(USAGE_ATTRIBUTE, project.usageByName(Usage.JAVA_API)) + attributes.attribute(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 isVisible = false isCanBeConsumed = true isCanBeResolved = false - attributes.attribute(USAGE_ATTRIBUTE, project.usageByName(Usage.JAVA_RUNTIME_JARS)) + attributes.attribute(USAGE_ATTRIBUTE, KotlinUsages.producerRuntimeUsage(target)) val runtimeConfiguration = configurations.maybeCreate(mainCompilation.deprecatedRuntimeConfigurationName) extendsFrom(implementationConfiguration, runtimeOnlyConfiguration, runtimeConfiguration) usesPlatformOf(target) @@ -277,7 +277,7 @@ abstract class AbstractKotlinTargetConfigurator 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 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." } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt index 7212f7d691d..eb69ada5210 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinSoftwareComponent.kt @@ -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 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 new file mode 100644 index 00000000000..a1da8c11de8 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinUsages.kt @@ -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 = 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 { + // 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) = 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 { + override fun execute(details: MultipleCandidatesDetails) = 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) + } + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinTargets.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinTargets.kt index da8664a3665..244aeda1cf7 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinTargets.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinTargets.kt @@ -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 = - setOf( + override fun createUsageContexts(): Set { + // 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 = diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/KotlinSourceSetFactory.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/KotlinSourceSetFactory.kt index 0ecae740812..b2e94327666 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/KotlinSourceSetFactory.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/KotlinSourceSetFactory.kt @@ -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 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))