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)
This commit is contained in:
Sergey Igushkin
2020-01-27 22:01:03 +03:00
parent 81216ceb51
commit 8a08fef2b3
3 changed files with 109 additions and 9 deletions
@@ -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()
}
}
}
@@ -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()
},
@@ -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 {