[Gradle] jvm+android: Support source dependency resolution to jvm only project

^KT-62033 Verification Pending
This commit is contained in:
Sebastian Sellmair
2023-09-19 13:34:34 +02:00
committed by Space Team
parent 5473db9ac5
commit cb5026b528
2 changed files with 115 additions and 11 deletions
@@ -5,7 +5,12 @@
package org.jetbrains.kotlin.gradle.plugin.ide.dependencyResolvers
import org.gradle.api.Project
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.attributes.Attribute
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinBinaryDependency
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinDependency
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinSourceDependency
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinSourceDependency.Type.Regular
@@ -15,7 +20,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.ide.IdeDependencyResolver
import org.jetbrains.kotlin.gradle.plugin.ide.IdeMultiplatformImport.SourceSetConstraint.Companion.isJvmAndAndroid
import org.jetbrains.kotlin.gradle.plugin.ide.IdeaKotlinSourceCoordinates
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.plugin.mpp.MetadataDependencyResolution
import org.jetbrains.kotlin.gradle.plugin.mpp.isMain
import org.jetbrains.kotlin.gradle.plugin.mpp.projectDependency
@@ -23,27 +28,91 @@ import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.sources.android.AndroidVariantType
import org.jetbrains.kotlin.gradle.plugin.sources.android.type
import org.jetbrains.kotlin.gradle.plugin.sources.internal
import org.jetbrains.kotlin.gradle.utils.copyAttributes
import org.jetbrains.kotlin.gradle.utils.toMap
/**
* ### Special Resolver for the JVM + Android use case:
* This resolver takes care of resolving the 'source dependencies' (aka dependencies to other projects' SourceSets)
* for SourceSets sharing code across a JVM and Android target.
*
* There are two special cases this resolver will archive its purpose:
*
* #### 1: 'This' multiplatform project depends on another multiplatform project:
* In this case, the metadata dependency resolution will return a 'ChooseVisibleSourceSets'.
* However, since we do not compile jvm+android into metadata, the resolution will not contain the desired source sets.
* Instead, the algorithm here will resolve the dependency project, look into it and will pick the corresponding 'jvm + android'
* SourceSets that it can find
*
* #### 2: 'This' multiplatform project depends on a **non** multiplatform dependency project:
* In this case the algorithm will use another approach to determine desirable SourceSets to resolve in the IDE:
* It will use the 'PlatformLike' dependency resolution mechanics to resolve the project dependencies in an 'as if this was jvm' mode.
* In order to set up attributes, the algorithm will copy the jvm platform attributes from the corresponding jvm compilations
* of the current SourceSet.
*/
internal object IdeJvmAndAndroidSourceDependencyResolver : IdeDependencyResolver {
override fun resolve(sourceSet: KotlinSourceSet): Set<IdeaKotlinDependency> {
if (!isJvmAndAndroid(sourceSet)) return emptySet()
if (sourceSet !is DefaultKotlinSourceSet) return emptySet()
return sourceSet.resolveMetadata<MetadataDependencyResolution.ChooseVisibleSourceSets>()
return sourceSet.resolveMetadata<MetadataDependencyResolution>()
/**
* See [IdeVisibleMultiplatformSourceDependencyResolver] on why this could happen
*/
.filter { chooseVisibleSourceSets -> chooseVisibleSourceSets.projectDependency(sourceSet.project) != sourceSet.project }
.flatMap { chooseVisibleSourceSets ->
val projectDependency = chooseVisibleSourceSets.projectDependency(sourceSet.project)
?: return@flatMap emptyList<IdeaKotlinDependency>()
val kotlin = projectDependency.multiplatformExtensionOrNull ?: return@flatMap emptyList<IdeaKotlinDependency>()
kotlin.sourceSets
.filter { sourceSet -> isJvmAndAndroidMain(sourceSet) }
.map { sourceSet -> IdeaKotlinSourceDependency(type = Regular, coordinates = IdeaKotlinSourceCoordinates(sourceSet)) }
.filter { metadataDependencyResolution -> metadataDependencyResolution.projectDependency(sourceSet.project) != sourceSet.project }
.flatMap { metadataDependencyResolution ->
when (metadataDependencyResolution) {
/* Dependency project is multiplatform */
is MetadataDependencyResolution.ChooseVisibleSourceSets -> resolveMultiplatformSourceSets(
metadataDependencyResolution.projectDependency(sourceSet.project) ?: return@flatMap emptyList()
)
/* Dependency project is not multiplatform (jvm/android only) */
is MetadataDependencyResolution.KeepOriginalDependency -> resolveJvmSourceSets(sourceSet)
else -> emptyList()
}
}
.toSet()
}
private fun resolveMultiplatformSourceSets(dependencyProject: Project): Iterable<IdeaKotlinDependency> {
val kotlin = dependencyProject.multiplatformExtensionOrNull ?: return emptyList()
return kotlin.sourceSets
.filter { sourceSet -> isJvmAndAndroidMain(sourceSet) }
.map { sourceSet -> IdeaKotlinSourceDependency(type = Regular, coordinates = IdeaKotlinSourceCoordinates(sourceSet)) }
}
/**
* Pretend that this [sourceSet] is 'jvm' and resolve binaries.
* #### Setting up attributes:
* In order to set up the 'platform like' / 'jvm like' dependency resolution, this algorithm
* will look at all 'jvm' based compilations, uses their 'compileDependencyConfiguration' as reference and
* then uses the intersection of all available attributes
*
* #### componentFilter:
* This resolver will just care about resolving project dependencies.
* Therefore, a componentFilter is added to only resolve project dependencies.
* We expect to resolve project artifact dependencies which can then be matched to the corresponding
* SourceSets on IDE side.
*/
private fun resolveJvmSourceSets(sourceSet: KotlinSourceSet): Iterable<IdeaKotlinDependency> {
return IdeBinaryDependencyResolver(
binaryType = IdeaKotlinBinaryDependency.KOTLIN_COMPILE_BINARY_TYPE,
artifactResolutionStrategy = IdeBinaryDependencyResolver.ArtifactResolutionStrategy.PlatformLikeSourceSet(
setupPlatformResolutionAttributes = {
sourceSet.internal.compilations.filter { it.platformType == KotlinPlatformType.jvm }
.map { compilation -> compilation.internal.configurations.compileDependencyConfiguration.attributes }
.map { attributes -> attributes.toMap().toList().toSet() }
.reduceOrNull { acc, next -> acc intersect next }
.orEmpty()
.forEach { (key, value) -> @Suppress("UNCHECKED_CAST") attribute(key as Attribute<Any>, value as Any) }
},
componentFilter = { id -> id is ProjectComponentIdentifier }
)
).resolve(sourceSet)
}
private fun isJvmAndAndroidMain(sourceSet: KotlinSourceSet): Boolean {
if (!isJvmAndAndroid(sourceSet)) return false
return sourceSet.internal.compilations.filter { it.platformType != KotlinPlatformType.common }.all { compilation ->
@@ -13,7 +13,9 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformSourceSetConventionsIm
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformSourceSetConventionsImpl.commonTest
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformSourceSetConventionsImpl.dependencies
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinBinaryDependency
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinResolvedBinaryDependency
import org.jetbrains.kotlin.gradle.idea.tcs.IdeaKotlinSourceDependency
import org.jetbrains.kotlin.gradle.idea.testFixtures.tcs.*
import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
import org.jetbrains.kotlin.gradle.plugin.ide.dependencyResolvers.IdeJvmAndAndroidPlatformBinaryDependencyResolver
@@ -87,7 +89,7 @@ class IdeJvmAndAndroidDependencyResolutionTest {
}
@Test
fun `test - project to project dependency`() {
fun `test - project to multiplatform project dependency`() {
val root = buildProject { setMultiplatformAndroidSourceSetLayoutVersion(2) }
val producer = buildProject({ withParent(root).withName("producer") }) { configureAndroidAndMultiplatform() }
val consumer = buildProject({ withParent(root).withName("consumer") }) { configureAndroidAndMultiplatform() }
@@ -96,6 +98,11 @@ class IdeJvmAndAndroidDependencyResolutionTest {
producer.evaluate()
consumer.evaluate()
root.allprojects { project ->
project.repositories.mavenLocal()
project.repositories.mavenCentral()
}
consumer.multiplatformExtension.sourceSets.getByName("commonMain").dependencies {
implementation(project(":producer"))
}
@@ -115,6 +122,34 @@ class IdeJvmAndAndroidDependencyResolutionTest {
)
}
@Test
fun `test - project to jvm project dependency`() {
val root = buildProject()
val producer = buildProject({ withParent(root).withName("producer") }) { applyKotlinJvmPlugin() }
val consumer = buildProject({ withParent(root).withName("consumer") }) { configureAndroidAndMultiplatform() }
consumer.multiplatformExtension.sourceSets.commonMain.dependencies {
implementation(producer)
}
root.evaluate()
producer.evaluate()
consumer.evaluate()
consumer.kotlinIdeMultiplatformImport.resolveDependencies("commonMain")
.filter { it !is IdeaKotlinBinaryDependency }
.assertMatches(projectArtifactDependency(IdeaKotlinSourceDependency.Type.Regular, ":producer", FilePathRegex(".*producer.jar")))
consumer.kotlinIdeMultiplatformImport.resolveDependencies("jvmAndAndroidMain")
.filter { it !is IdeaKotlinBinaryDependency }
.assertMatches(
dependsOnDependency(":consumer/commonMain"),
projectArtifactDependency(IdeaKotlinSourceDependency.Type.Regular, ":producer", FilePathRegex(".*producer.jar"))
)
}
@Test
fun `test - KT-59020 - transitive project dependency to self`() {
val root = buildProject { setMultiplatformAndroidSourceSetLayoutVersion(2) }