[MPP] Fix dependencies of a platform compilation source set

Leave empty new visible source sets in a metadata transformation
for a platform (leaf) source set.

Motivation:
* Platform source sets should get their deps from the compilation
* Metadata dependencies in platform source set shouldn't be used
as transitive library dependencies won't be correct in this case

KT-52216
This commit is contained in:
Pavel Kirpichenkov
2022-05-17 14:20:30 +03:00
committed by Space
parent b472ccd358
commit 4d023e873d
13 changed files with 232 additions and 10 deletions
@@ -101,7 +101,7 @@ class HierarchicalMppIT : KGPBaseTest() {
val thirdPartyLibApiVisibility = reports.filter { report ->
report.groupAndModule.startsWith("com.example.thirdparty:third-party-lib") && report.scope == "api"
}
val jvmJsSourceSets = setOf("jvmMain", "jsMain", "jvmTest", "jsTest", "jvmAndJsMain", "jvmAndJsTest")
val jvmJsSourceSets = setOf("jvmAndJsMain", "jvmAndJsTest")
thirdPartyLibApiVisibility.forEach {
if (it.sourceSetName in jvmJsSourceSets)
assertTrue("$it") { it.allVisibleSourceSets == setOf("commonMain") }
@@ -771,6 +771,76 @@ class HierarchicalMppIT : KGPBaseTest() {
}
}
@GradleTest
@DisplayName("KT-52216: [TYPE_MISMATCH] Caused by unexpected metadata dependencies of leaf source sets")
fun `test default platform compilation source set has no metadata dependencies`(gradleVersion: GradleVersion) {
with(project("kt-52216", gradleVersion = gradleVersion)) {
build(":lib:publish")
testDependencyTransformations("p1") { reports ->
for (leafSourceSetName in listOf("jvmMain", "jsMain", "linuxX64Main")) {
val report = reports.singleOrNull {
it.sourceSetName == leafSourceSetName && it.scope == "implementation" && it.groupAndModule == "kt52216:lib"
}
assertNotNull(report, "No transformation for $leafSourceSetName implementation")
assert(report.allVisibleSourceSets.isEmpty()) {
"All visible source sets for leaf platform source set should always be empty, but found: ${
report.allVisibleSourceSets.joinToString(prefix = "[", postfix = "]", separator = "; ")
}"
}
assert(report.newVisibleSourceSets.isEmpty()) {
"New visible source sets for leaf platform source set should always be empty, but found: ${
report.newVisibleSourceSets.joinToString(prefix = "[", postfix = "]", separator = "; ")
}"
}
}
}
testDependencyTransformations("p2") { reports ->
val commonReport = reports.singleOrNull {
it.sourceSetName == "commonMain" && it.scope == "implementation" && it.groupAndModule == "kt52216:lib"
}
assertNotNull(commonReport, "No transformation for commonMain implementation")
assert(commonReport.allVisibleSourceSets.singleOrNull() == "commonMain") {
"All visible source sets of commonMain don't include library's commonMain"
}
assert(commonReport.newVisibleSourceSets.singleOrNull() == "commonMain") {
"New visible source sets of commonMain don't include library's commonMain"
}
for (targetName in listOf("jvm", "js", "linuxX64")) {
val intermediateReport = reports.singleOrNull {
it.sourceSetName == "${targetName}Intermediate" && it.scope == "implementation" && it.groupAndModule == "kt52216:lib"
}
val leafReport = reports.singleOrNull {
it.sourceSetName == "${targetName}Main" && it.scope == "implementation" && it.groupAndModule == "kt52216:lib"
}
assertNotNull(intermediateReport, "No transformation for ${targetName}Intermediate implementation")
assertNotNull(leafReport, "No transformation for ${targetName}Main implementation")
assert(intermediateReport.allVisibleSourceSets.singleOrNull() == "commonMain") {
"Intermediate transformation should contain commonMain in all visible source sets, but it doesn't for target: $targetName"
}
assert(leafReport.allVisibleSourceSets.isEmpty()) {
"All visible source sets for leaf platform source set should should be empty, but for target $targetName found: ${
leafReport.allVisibleSourceSets.joinToString(prefix = "[", postfix = "]", separator = "; ")
}"
}
assert(intermediateReport.newVisibleSourceSets.isEmpty()) {
"New visible source sets for intermediate source set should should be empty, but for target $targetName found: ${
leafReport.newVisibleSourceSets.joinToString(prefix = "[", postfix = "]", separator = "; ")
}"
}
assert(leafReport.newVisibleSourceSets.isEmpty()) {
"New visible source sets for leaf platform source set should always be empty, but for target $targetName found: ${
leafReport.newVisibleSourceSets.joinToString(prefix = "[", postfix = "]", separator = "; ")
}"
}
}
}
}
}
private fun TestProject.testDependencyTransformations(
subproject: String? = null,
check: BuildResult.(reports: Iterable<DependencyTransformationReport>) -> Unit
@@ -0,0 +1,25 @@
plugins {
kotlin("multiplatform")
`maven-publish`
}
group = "kt52216"
version = "1.0"
repositories {
mavenLocal()
mavenCentral()
}
kotlin {
jvm()
js()
linuxX64()
}
publishing {
repositories {
maven("$rootDir/../repo")
}
}
@@ -0,0 +1,36 @@
plugins {
kotlin("multiplatform")
}
group = "me.user"
version = "1.0"
repositories {
maven("$rootDir/../repo")
mavenLocal()
mavenCentral()
}
kotlin {
val targets = listOf(
jvm(),
js(),
linuxX64()
)
sourceSets {
val commonMain by getting
for (target in targets) {
getByName(target.leafSourceSetName) {
dependsOn(commonMain)
dependencies {
implementation("kt52216:lib:1.0")
}
}
}
}
}
val org.jetbrains.kotlin.gradle.plugin.KotlinTarget.leafSourceSetName: String
get() = "${name}Main"
@@ -0,0 +1,42 @@
plugins {
kotlin("multiplatform")
}
group = "me.user"
version = "1.0"
repositories {
maven("$rootDir/../repo")
mavenLocal()
mavenCentral()
}
kotlin {
val targets = listOf(
jvm(),
js(),
linuxX64()
)
sourceSets {
val commonMain by getting {
dependencies {
implementation("kt52216:lib:1.0")
}
}
for (target in targets) {
val leafSourceSet = getByName(target.leafSourceSetName)
create(target.intermediateSourceSetName) {
leafSourceSet.dependsOn(this)
dependsOn(commonMain)
}
}
}
}
val org.jetbrains.kotlin.gradle.plugin.KotlinTarget.leafSourceSetName: String
get() = "${name}Main"
val org.jetbrains.kotlin.gradle.plugin.KotlinTarget.intermediateSourceSetName: String
get() = "${name}Intermediate"
@@ -0,0 +1,5 @@
rootProject.name = "kt-52216"
include(":lib")
include(":p1")
include(":p2")
@@ -13,6 +13,8 @@ import org.gradle.api.artifacts.result.ResolvedComponentResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.attributes.Attribute
import org.gradle.api.file.FileCollection
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.mpp.MetadataDependencyResolution.ChooseVisibleSourceSets.MetadataProvider.Companion.asMetadataProvider
import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope
@@ -34,7 +36,7 @@ internal sealed class MetadataDependencyResolution(
override fun toString(): String {
val verb = when (this) {
is KeepOriginalDependency -> "keep"
is ExcludeAsUnrequested -> "exclude"
is Exclude -> "exclude"
is ChooseVisibleSourceSets -> "choose"
}
return "$verb, dependency = $dependency"
@@ -45,10 +47,27 @@ internal sealed class MetadataDependencyResolution(
projectDependency: Project?
) : MetadataDependencyResolution(dependency, projectDependency)
class ExcludeAsUnrequested(
sealed class Exclude(
dependency: ResolvedComponentResult,
projectDependency: Project?
) : MetadataDependencyResolution(dependency, projectDependency)
) : MetadataDependencyResolution(dependency, projectDependency) {
class Unrequested(
dependency: ResolvedComponentResult,
projectDependency: Project?
) : Exclude(dependency, projectDependency)
/**
* Resolution for metadata dependencies of leaf platform source sets.
* They are excluded since platform source sets should receive
* platform dependencies from corresponding compilations and should not get metadata ones.
* See KT-52216
*/
class PublishedPlatformSourceSetDependency(
dependency: ResolvedComponentResult,
val visibleTransitiveDependencies: Set<ResolvedDependencyResult>,
) : Exclude(dependency, null)
}
class ChooseVisibleSourceSets internal constructor(
dependency: ResolvedComponentResult,
@@ -157,7 +176,8 @@ internal class GranularMetadataTransformation(
is MetadataDependencyResolution.KeepOriginalDependency ->
resolvedDependency.dependencies.filterIsInstance<ResolvedDependencyResult>()
is MetadataDependencyResolution.ChooseVisibleSourceSets -> dependencyResult.visibleTransitiveDependencies
is MetadataDependencyResolution.ExcludeAsUnrequested -> error("a visited dependency is erroneously considered unrequested")
is MetadataDependencyResolution.Exclude.PublishedPlatformSourceSetDependency -> dependencyResult.visibleTransitiveDependencies
is MetadataDependencyResolution.Exclude.Unrequested -> error("a visited dependency is erroneously considered unrequested")
}
resolvedDependencyQueue.addAll(
@@ -170,7 +190,7 @@ internal class GranularMetadataTransformation(
if (resolvedDependency.selected !in visitedDependencies) {
// val files = resolvedDependency.moduleArtifacts.map { it.file }
result.add(
MetadataDependencyResolution.ExcludeAsUnrequested(
MetadataDependencyResolution.Exclude.Unrequested(
resolvedDependency.selected,
(resolvedDependency.selected.id as? ProjectComponentIdentifier)
?.takeIf { it.build.isCurrentBuild() }
@@ -243,6 +263,9 @@ internal class GranularMetadataTransformation(
.filterIsInstance<ResolvedDependencyResult>()
.filterTo(mutableSetOf()) { ModuleIds.fromComponent(project, it.selected) in requestedTransitiveDependencies }
if (kotlinSourceSet in project.multiplatformExtension.platformCompilationSourceSets && resolvedToProject == null)
return MetadataDependencyResolution.Exclude.PublishedPlatformSourceSetDependency(module, transitiveDependenciesToVisit)
val visibleSourceSetsExcludingDependsOn = allVisibleSourceSets.filterTo(mutableSetOf()) { it !in sourceSetsVisibleInParents }
val metadataProvider = when (mppDependencyMetadataExtractor) {
@@ -345,3 +368,9 @@ internal fun requestedDependencies(
val otherContributingSourceSets = dependsOnClosureWithInterCompilationDependencies(project, sourceSet)
return listOf(sourceSet, *otherContributingSourceSets.toTypedArray()).flatMap(::collectScopedDependenciesFromSourceSet)
}
private val KotlinMultiplatformExtension.platformCompilationSourceSets: Set<KotlinSourceSet>
get() = targets.filterNot { it is KotlinMetadataTarget }
.flatMap { target -> target.compilations }
.flatMap { it.kotlinSourceSetsIncludingDefault }
.toSet()
@@ -134,7 +134,7 @@ internal class GradleKpmFragmentGranularMetadataResolver(
// FIXME this code is based on whole components; use module IDs with classifiers instead
val resultSourceComponents = results.mapTo(mutableSetOf()) { it.dependency }
resolvedComponentsByModuleId.values.minus(resultSourceComponents).forEach {
results.add(MetadataDependencyResolution.ExcludeAsUnrequested(it, it.toProjectOrNull(project)))
results.add(MetadataDependencyResolution.Exclude.Unrequested(it, it.toProjectOrNull(project)))
}
return results
@@ -174,9 +174,12 @@ class DefaultKotlinSourceSet(
val (group, name) = groupAndName
val projectPath = resolution.projectDependency?.path
when (resolution) {
// No metadata transformation leads to original dependency being used during import
is MetadataDependencyResolution.KeepOriginalDependency -> null
is MetadataDependencyResolution.ExcludeAsUnrequested ->
// We should pass empty transformation for excluded dependencies.
// No transformation at all will result in a composite metadata jar being used as a dependency.
is MetadataDependencyResolution.Exclude ->
MetadataDependencyTransformation(group, name, projectPath, null, emptySet(), emptyMap())
is MetadataDependencyResolution.ChooseVisibleSourceSets -> {
@@ -409,7 +409,7 @@ class KotlinMetadataTargetConfigurator :
// Run this action immediately before the configuration first takes part in dependency resolution:
configuration.withDependencies {
val (unrequested, requested) = metadataDependencyResolutions
.partition { it is MetadataDependencyResolution.ExcludeAsUnrequested }
.partition { it is MetadataDependencyResolution.Exclude }
unrequested.forEach {
val (group, name) = it.projectDependency?.run {
@@ -528,7 +528,7 @@ internal fun createMetadataDependencyTransformationClasspath(
val artifactView = fromFiles.incoming.artifactView { view ->
view.componentFilter { id ->
allResolutionsByComponentId[id].let { resolutions ->
resolutions == null || resolutions.any { it !is MetadataDependencyResolution.ExcludeAsUnrequested }
resolutions == null || resolutions.any { it !is MetadataDependencyResolution.Exclude }
}
}
}