From 8a08fef2b37cf8d82ec195b334cb1fb4270897e8 Mon Sep 17 00:00:00 2001 From: Sergey Igushkin Date: Mon, 27 Jan 2020 22:01:03 +0300 Subject: [PATCH] Export implementation dependencies of Native-shared source sets as API As the Kotlin/Native compiler always requires transitive dependencies to be present among the libraries during compilation, it is necessary to export the implementation dependencies of Native-shared source sets as if they were API dependencies. To do that, add the extendsFrom-relationship between the apiElements configuration of the metadata target (so that Gradle adds the dependencies to the transitive dependencies graph) and also add the dependencies to the project structure metadata (so that the consumer can find out that it's only their corresponding Native-shared source sets that need these particular transitive dependencies) --- .../jetbrains/kotlin/gradle/KlibBasedMppIT.kt | 66 ++++++++++++++++--- .../mpp/KotlinProjectStructureMetadata.kt | 19 +++++- .../KotlinMetadataTargetConfigurator.kt | 33 ++++++++++ 3 files changed, 109 insertions(+), 9 deletions(-) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KlibBasedMppIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KlibBasedMppIT.kt index bcc02ff62be..2f5f2e8c936 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KlibBasedMppIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KlibBasedMppIT.kt @@ -8,7 +8,6 @@ package org.jetbrains.kotlin.gradle import org.jetbrains.kotlin.gradle.util.modify import org.jetbrains.kotlin.konan.target.HostManager import org.junit.Assume -import org.junit.Ignore import java.io.File import kotlin.test.Test import kotlin.test.assertFalse @@ -20,18 +19,22 @@ import kotlin.test.assertFalse class KlibBasedMppIT : BaseGradleIT() { override val defaultGradleVersion = GradleVersionRequired.AtLeast("6.0") + companion object { + private const val MODULE_GROUP = "com.example" + } + @Test fun testBuildWithProjectDependency() = testBuildWithDependency { gradleBuildScript().appendText("\n" + """ dependencies { - commonMainImplementation(project("$embeddedModuleName")) + commonMainImplementation(project("$dependencyModuleName")) } """.trimIndent()) } @Test fun testBuildWithPublishedDependency() = testBuildWithDependency { - build(":$embeddedModuleName:publish") { + build(":$dependencyModuleName:publish") { assertSuccessful() } @@ -40,23 +43,23 @@ class KlibBasedMppIT : BaseGradleIT() { maven("${'$'}rootDir/repo") } dependencies { - commonMainImplementation("com.example:$embeddedModuleName:1.0") + commonMainImplementation("$MODULE_GROUP:$dependencyModuleName:1.0") } """.trimIndent()) // prevent Gradle from linking the above dependency to the project: - gradleBuildScript(embeddedModuleName).appendText("\ngroup = \"some.other.group\"") + gradleBuildScript(dependencyModuleName).appendText("\ngroup = \"some.other.group\"") } - private val embeddedModuleName = "project-dep" + private val dependencyModuleName = "project-dep" private fun testBuildWithDependency(configureDependency: Project.() -> Unit) = with(Project("common-klib-lib-and-app")) { Assume.assumeTrue(HostManager.hostIsMac) - embedProject(Project("common-klib-lib-and-app"), renameTo = embeddedModuleName) + embedProject(Project("common-klib-lib-and-app"), renameTo = dependencyModuleName) gradleBuildScript().modify(::transformBuildScriptWithPluginsDsl) - projectDir.resolve(embeddedModuleName + "/src").walkTopDown().filter { it.extension == "kt" }.forEach { file -> + projectDir.resolve(dependencyModuleName + "/src").walkTopDown().filter { it.extension == "kt" }.forEach { file -> file.modify { it.replace("package com.h0tk3y.hmpp.klib.demo", "package com.projectdep") } } @@ -110,4 +113,51 @@ class KlibBasedMppIT : BaseGradleIT() { } } } + + private val transitiveDepModuleName = "transitive-dep" + + @Test + fun testKotlinNativeImplPublishedDeps() = + testKotlinNativeImplementationDependencies { + build(":$transitiveDepModuleName:publish", ":$dependencyModuleName:publish") { + assertSuccessful() + } + + gradleBuildScript().appendText("\n" + """ + repositories { + maven("${'$'}rootDir/repo") + } + dependencies { + commonMainImplementation("$MODULE_GROUP:$dependencyModuleName:1.0") + } + """.trimIndent() + ) + + listOf(transitiveDepModuleName, dependencyModuleName).forEach { + // prevent Gradle from linking the above dependency to the project: + gradleBuildScript(it).appendText("\ngroup = \"com.some.other.group\"") + } + } + + @Test + fun testKotlinNativeImplProjectDeps() = + testKotlinNativeImplementationDependencies { + gradleBuildScript().appendText("\ndependencies { \"commonMainImplementation\"(project(\":$dependencyModuleName\")) }") + } + + private fun testKotlinNativeImplementationDependencies( + setupDependencies: Project.() -> Unit + ) = with(Project("common-klib-lib-and-app")) { + embedProject(Project("common-klib-lib-and-app"), renameTo = transitiveDepModuleName) + embedProject(Project("common-klib-lib-and-app"), renameTo = dependencyModuleName) + gradleBuildScript().modify(::transformBuildScriptWithPluginsDsl) + gradleBuildScript(dependencyModuleName).appendText("\ndependencies { \"commonMainImplementation\"(project(\":$transitiveDepModuleName\")) }") + + setupDependencies(this@with) + + val compileNativeMetadataTaskName = "compileIosMainKotlinMetadata" + build(":$compileNativeMetadataTaskName") { + assertSuccessful() + } + } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinProjectStructureMetadata.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinProjectStructureMetadata.kt index 1c465ad282e..a02ee35a907 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinProjectStructureMetadata.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinProjectStructureMetadata.kt @@ -11,6 +11,9 @@ import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Nested import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider +import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope +import org.jetbrains.kotlin.gradle.plugin.sources.getSourceSetHierarchy +import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope import org.jetbrains.kotlin.gradle.targets.metadata.getPublishedPlatformCompilations import org.w3c.dom.Document import org.w3c.dom.Element @@ -88,7 +91,21 @@ internal fun buildKotlinProjectStructureMetadata(project: Project): KotlinProjec sourceSet.name to sourceSet.dependsOn.filter { it in sourceSetsWithMetadataCompilations }.map { it.name }.toSet() }, sourceSetModuleDependencies = sourceSetsWithMetadataCompilations.keys.associate { sourceSet -> - sourceSet.name to project.configurations.getByName(sourceSet.apiConfigurationName).allDependencies.map { + /** + * Currently, Kotlin/Native dependencies must include the implementation dependencies, too. These dependencies must also be + * published as API dependencies of the metadata module to get into the resolution result, see + * [KotlinMetadataTargetConfigurator.exportDependenciesForPublishing]. + */ + val isNativeSharedSourceSet = sourceSetsWithMetadataCompilations[sourceSet] is KotlinSharedNativeCompilation + val sourceSetExportedDependencies = when { + isNativeSharedSourceSet -> sourceSet.getSourceSetHierarchy().flatMap { hierarchySourceSet -> + listOf(KotlinDependencyScope.API_SCOPE, KotlinDependencyScope.IMPLEMENTATION_SCOPE).flatMap { scope -> + project.sourceSetDependencyConfigurationByScope(hierarchySourceSet, scope).allDependencies.toList() + } + }.distinct() + else -> project.configurations.getByName(sourceSet.apiConfigurationName).allDependencies + } + sourceSet.name to sourceSetExportedDependencies.map { ModuleDependencyIdentifier(it.group.orEmpty(), it.name) }.toSet() }, diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt index 28a46922fcd..f7993aba502 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt @@ -177,6 +177,10 @@ class KotlinMetadataTargetConfigurator(kotlinPluginVersion: String) : ) } + sourceSetsWithMetadataCompilations.values.forEach { compilation -> + exportDependenciesForPublishing(compilation) + } + val generateMetadata = createGenerateProjectStructureMetadataTask() allMetadataJar.from(project.files(Callable { @@ -186,6 +190,35 @@ class KotlinMetadataTargetConfigurator(kotlinPluginVersion: String) : } } + private fun exportDependenciesForPublishing( + compilation: AbstractKotlinCompilation<*> + ) { + val sourceSet = compilation.defaultSourceSet + val isSharedNativeCompilation = compilation is KotlinSharedNativeCompilation + val target = compilation.target + with(target.project) { + val apiElementsConfiguration = configurations.getByName(target.apiElementsConfigurationName) + + // With the metadata target, we publish all API dependencies of all the published source sets together: + apiElementsConfiguration.extendsFrom(sourceSetDependencyConfigurationByScope(sourceSet, KotlinDependencyScope.API_SCOPE)) + + /** For Kotlin/Native-shared source sets, we also add the implementation dependencies to apiElements, because Kotlin/Native + * can't have any implementation dependencies, all dependencies used for compilation must be shipped along with the klib. + * It's OK that they are exposed as API dependencies here, because at the consumer side, they are dispatched to the + * consumer's source sets in a granular way, so they will only be visible in the source sets that see the sharedn-Native + * source set. + * See also: [buildKotlinProjectStructureMetadata], where these dependencies must be included into the source set exported deps. + */ + if (isSharedNativeCompilation) { + sourceSet.getSourceSetHierarchy().forEach { hierarchySourceSet -> + apiElementsConfiguration.extendsFrom( + sourceSetDependencyConfigurationByScope(hierarchySourceSet, KotlinDependencyScope.IMPLEMENTATION_SCOPE) + ) + } + } + } + } + private fun createMergedAllSourceSetsConfigurations(target: KotlinMetadataTarget): Unit = with(target.project) { listOf(ALL_COMPILE_METADATA_CONFIGURATION_NAME, ALL_RUNTIME_METADATA_CONFIGURATION_NAME).forEach { configurationName -> project.configurations.create(configurationName).apply {