diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/dependencyResolvers/IdeJvmAndAndroidSourceDependencyResolver.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/dependencyResolvers/IdeJvmAndAndroidSourceDependencyResolver.kt index 363faefa0a3..791830c022d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/dependencyResolvers/IdeJvmAndAndroidSourceDependencyResolver.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/ide/dependencyResolvers/IdeJvmAndAndroidSourceDependencyResolver.kt @@ -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 { if (!isJvmAndAndroid(sourceSet)) return emptySet() if (sourceSet !is DefaultKotlinSourceSet) return emptySet() - return sourceSet.resolveMetadata() + + return sourceSet.resolveMetadata() /** * 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() - val kotlin = projectDependency.multiplatformExtensionOrNull ?: return@flatMap emptyList() - 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 { + 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 { + 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, 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 -> diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/dependencyResolutionTests/tcs/IdeJvmAndAndroidDependencyResolutionTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/dependencyResolutionTests/tcs/IdeJvmAndAndroidDependencyResolutionTest.kt index 462dee92669..9266daa5429 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/dependencyResolutionTests/tcs/IdeJvmAndAndroidDependencyResolutionTest.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/dependencyResolutionTests/tcs/IdeJvmAndAndroidDependencyResolutionTest.kt @@ -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) }