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 {