Gradle: Add dependencies for indirectly implemented modules
#KT-16926 Fixed
This commit is contained in:
+102
-33
@@ -24,16 +24,20 @@ import com.intellij.openapi.externalSystem.model.project.ProjectData
|
||||
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
|
||||
import com.intellij.openapi.roots.DependencyScope
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import org.gradle.tooling.model.idea.IdeaModule
|
||||
import org.gradle.tooling.model.idea.IdeaModuleDependency
|
||||
import org.jetbrains.kotlin.gradle.CompilerArgumentsBySourceSet
|
||||
import org.jetbrains.kotlin.gradle.KotlinGradleModel
|
||||
import org.jetbrains.kotlin.gradle.KotlinGradleModelBuilder
|
||||
import org.jetbrains.kotlin.psi.NotNullableUserDataProperty
|
||||
import org.jetbrains.kotlin.psi.UserDataProperty
|
||||
import org.jetbrains.plugins.gradle.model.ExternalProject
|
||||
import org.jetbrains.plugins.gradle.model.ExternalProjectDependency
|
||||
import org.jetbrains.plugins.gradle.model.FileCollectionDependency
|
||||
import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData
|
||||
import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension
|
||||
import org.jetbrains.plugins.gradle.service.project.GradleProjectResolverUtil.getModuleId
|
||||
import org.jetbrains.plugins.gradle.service.project.GradleProjectResolver
|
||||
import java.util.*
|
||||
|
||||
var DataNode<ModuleData>.isResolved
|
||||
by NotNullableUserDataProperty(Key.create<Boolean>("IS_RESOLVED"), false)
|
||||
@@ -60,10 +64,103 @@ class KotlinGradleProjectResolverExtension : AbstractProjectResolverExtension()
|
||||
override fun populateModuleDependencies(gradleModule: IdeaModule,
|
||||
ideModule: DataNode<ModuleData>,
|
||||
ideProject: DataNode<ProjectData>) {
|
||||
val outputToSourceSet = ideProject.getUserData(GradleProjectResolver.MODULES_OUTPUTS)
|
||||
val sourceSetByName = ideProject.getUserData(GradleProjectResolver.RESOLVED_SOURCE_SETS)
|
||||
|
||||
val gradleModel = resolverCtx.getExtraProject(gradleModule, KotlinGradleModel::class.java)
|
||||
?: return super.populateModuleDependencies(gradleModule, ideModule, ideProject)
|
||||
|
||||
importTransitiveCommonDependencies(gradleModel, ideProject, gradleModule, ideModule)
|
||||
val gradleIdeaProject = gradleModule.project
|
||||
|
||||
fun DataNode<out ModuleData>.getGradleModule() =
|
||||
if (this != ideModule) gradleIdeaProject.modules.firstOrNull { it.gradleProject.path == data.id } else gradleModule
|
||||
|
||||
fun DataNode<out ModuleData>.getDependencies(): Set<DataNode<out ModuleData>> {
|
||||
if (resolverCtx.isResolveModulePerSourceSet) {
|
||||
if (sourceSetByName == null) return emptySet()
|
||||
val externalSourceSet = sourceSetByName[data.id]?.second ?: return emptySet()
|
||||
return externalSourceSet.dependencies.mapNotNullTo(LinkedHashSet()) { dependency ->
|
||||
when (dependency) {
|
||||
is ExternalProjectDependency -> {
|
||||
val targetModuleNode = ExternalSystemApiUtil.findFirstRecursively(ideProject) {
|
||||
(it.data as? ModuleData)?.id == dependency.projectPath
|
||||
} as DataNode<ModuleData>? ?: return@mapNotNullTo null
|
||||
ExternalSystemApiUtil.findAll(targetModuleNode, GradleSourceSetData.KEY).firstOrNull { it.sourceSetName == "main" }
|
||||
}
|
||||
is FileCollectionDependency -> {
|
||||
dependency.files
|
||||
.mapTo(HashSet()) {
|
||||
val path = FileUtil.toSystemIndependentName(it.path)
|
||||
val targetSourceSetId = outputToSourceSet?.get(path)?.first ?: return@mapTo null
|
||||
sourceSetByName[targetSourceSetId]?.first
|
||||
}
|
||||
.singleOrNull()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getGradleModule()?.dependencies?.mapNotNullTo(LinkedHashSet()) {
|
||||
val targetModuleName = (it as? IdeaModuleDependency)?.targetModuleName ?: return@mapNotNullTo null
|
||||
val targetGradleModule = gradleIdeaProject.modules.firstOrNull { it.name == targetModuleName } ?: return@mapNotNullTo null
|
||||
ExternalSystemApiUtil.findFirstRecursively(ideProject) {
|
||||
(it.data as? ModuleData)?.id == targetGradleModule.gradleProject.path
|
||||
} as DataNode<ModuleData>?
|
||||
} ?: emptySet()
|
||||
}
|
||||
|
||||
fun addTransitiveDependenciesOnImplementedModules() {
|
||||
val moduleNodesToProcess = if (resolverCtx.isResolveModulePerSourceSet) {
|
||||
ExternalSystemApiUtil.findAll(ideModule, GradleSourceSetData.KEY)
|
||||
}
|
||||
else listOf(ideModule)
|
||||
|
||||
for (currentModuleNode in moduleNodesToProcess) {
|
||||
val toProcess = LinkedList<DataNode<out ModuleData>>()
|
||||
val processed = HashSet<DataNode<out ModuleData>>()
|
||||
toProcess.add(currentModuleNode)
|
||||
|
||||
while (toProcess.isNotEmpty()) {
|
||||
val moduleNode = toProcess.pollFirst()
|
||||
processed.add(moduleNode)
|
||||
|
||||
val moduleNodeForGradleModel = if (resolverCtx.isResolveModulePerSourceSet) {
|
||||
ExternalSystemApiUtil.findParent(moduleNode, ProjectKeys.MODULE)
|
||||
}
|
||||
else moduleNode
|
||||
val ideaModule = if (moduleNodeForGradleModel != ideModule) {
|
||||
gradleIdeaProject.modules.firstOrNull { it.gradleProject.path == moduleNodeForGradleModel?.data?.id }
|
||||
}
|
||||
else gradleModule
|
||||
|
||||
val implementsInfo = resolverCtx.getExtraProject(ideaModule, KotlinGradleModel::class.java)?.implements
|
||||
val targetModule = implementsInfo?.let { findModule(ideProject, it) }
|
||||
if (targetModule != null) {
|
||||
if (resolverCtx.isResolveModulePerSourceSet) {
|
||||
val targetSourceSetsByName = ExternalSystemApiUtil
|
||||
.findAll(targetModule, GradleSourceSetData.KEY)
|
||||
.associateBy { it.sourceSetName }
|
||||
val targetMainSourceSet = targetSourceSetsByName["main"] ?: targetModule
|
||||
val targetSourceSet = targetSourceSetsByName[currentModuleNode.sourceSetName]
|
||||
if (targetSourceSet != null) {
|
||||
addDependency(currentModuleNode, targetSourceSet)
|
||||
}
|
||||
if (currentModuleNode.sourceSetName == "test" && targetMainSourceSet != targetSourceSet) {
|
||||
addDependency(currentModuleNode, targetMainSourceSet)
|
||||
}
|
||||
}
|
||||
else {
|
||||
addDependency(currentModuleNode, targetModule)
|
||||
}
|
||||
}
|
||||
|
||||
moduleNode.getDependencies().filterTo(toProcess) { it !in processed }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addTransitiveDependenciesOnImplementedModules()
|
||||
|
||||
ideModule.isResolved = true
|
||||
ideModule.hasKotlinPlugin = gradleModel.hasKotlinPlugin
|
||||
@@ -75,17 +172,8 @@ class KotlinGradleProjectResolverExtension : AbstractProjectResolverExtension()
|
||||
super.populateModuleDependencies(gradleModule, ideModule, ideProject)
|
||||
}
|
||||
|
||||
private fun importTransitiveCommonDependencies(gradleModel: KotlinGradleModel, ideProject: DataNode<ProjectData>, gradleModule: IdeaModule, ideModule: DataNode<ModuleData>) {
|
||||
gradleModel.transitiveCommonDependencies.forEach { implementsModuleId ->
|
||||
val targetModule = findModule(ideProject, implementsModuleId) ?: return
|
||||
if (resolverCtx.isResolveModulePerSourceSet) {
|
||||
populateSourceSetDependencies(gradleModule, ideModule, targetModule)
|
||||
}
|
||||
else {
|
||||
addDependency(ideModule, targetModule)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val DataNode<out ModuleData>.sourceSetName
|
||||
get() = (data as? GradleSourceSetData)?.id?.substringAfterLast(':')
|
||||
|
||||
private fun addDependency(ideModule: DataNode<out ModuleData>, targetModule: DataNode<out ModuleData>) {
|
||||
val moduleDependencyData = ModuleDependencyData(ideModule.data, targetModule.data)
|
||||
@@ -97,23 +185,4 @@ class KotlinGradleProjectResolverExtension : AbstractProjectResolverExtension()
|
||||
private fun findModule(ideProject: DataNode<ProjectData>, moduleId: String): DataNode<ModuleData>? {
|
||||
return ideProject.children.find { (it.data as? ModuleData)?.id == moduleId } as DataNode<ModuleData>?
|
||||
}
|
||||
|
||||
private fun populateSourceSetDependencies(gradleModule: IdeaModule,
|
||||
fromModule: DataNode<ModuleData>,
|
||||
targetModule: DataNode<ModuleData>) {
|
||||
val fromSourceSets = ExternalSystemApiUtil.findAll(fromModule, GradleSourceSetData.KEY)
|
||||
.associateBy { it.data.id }
|
||||
val targetSourceSets = ExternalSystemApiUtil.findAll(targetModule, GradleSourceSetData.KEY)
|
||||
.associateBy { it.data.id.substringAfterLast(':') }
|
||||
|
||||
val externalProject = resolverCtx.getExtraProject(gradleModule, ExternalProject::class.java) ?: return
|
||||
for (sourceSet in externalProject.sourceSets.values) {
|
||||
if (sourceSet == null || sourceSet.sources.isEmpty()) continue
|
||||
|
||||
val moduleId = getModuleId(externalProject, sourceSet)
|
||||
val fromModuleDataNode = (if (fromSourceSets.isEmpty()) fromModule else fromSourceSets[moduleId]) ?: continue
|
||||
val targetModuleDataNode = (if (targetSourceSets.isEmpty()) targetModule else targetSourceSets[sourceSet.name]) ?: continue
|
||||
addDependency(fromModuleDataNode, targetModuleDataNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+89
@@ -250,4 +250,93 @@ class MultiplatformProjectImportingTest : GradleImportingTestCase() {
|
||||
assertModuleModuleDepScope("jvm-app_test", "common-lib1_test", DependencyScope.COMPILE)
|
||||
assertModuleModuleDepScope("jvm-app_test", "common-lib2_test", DependencyScope.COMPILE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTransitiveImplement() {
|
||||
createProjectSubFile(
|
||||
"settings.gradle",
|
||||
"include ':project1', ':project2', ':project3'"
|
||||
)
|
||||
|
||||
val kotlinVersion = "1.1.51"
|
||||
|
||||
createProjectSubFile("build.gradle", """
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
project('project1') {
|
||||
apply plugin: 'kotlin-platform-common'
|
||||
|
||||
sourceSets {
|
||||
custom
|
||||
}
|
||||
}
|
||||
|
||||
project('project2') {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin-platform-jvm'
|
||||
|
||||
sourceSets {
|
||||
custom
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implement project(':project1')
|
||||
}
|
||||
}
|
||||
|
||||
project('project3') {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin-platform-jvm'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
sourceSets {
|
||||
custom
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':project2')
|
||||
customCompile project(':project2')
|
||||
testCompile(project(':project2').sourceSets.test.output)
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
importProject()
|
||||
|
||||
assertModuleModuleDepScope("project1_test", "project1_main", DependencyScope.COMPILE)
|
||||
|
||||
assertModuleModuleDepScope("project2_main", "project1_main", DependencyScope.COMPILE)
|
||||
|
||||
assertModuleModuleDepScope("project2_test", "project2_main", DependencyScope.COMPILE)
|
||||
assertModuleModuleDepScope("project2_test", "project1_test", DependencyScope.COMPILE)
|
||||
assertModuleModuleDepScope("project2_test", "project1_main", DependencyScope.COMPILE)
|
||||
|
||||
assertModuleModuleDepScope("project2_custom", "project1_custom", DependencyScope.COMPILE)
|
||||
|
||||
assertModuleModuleDepScope("project3_main", "project2_main", DependencyScope.COMPILE)
|
||||
assertModuleModuleDepScope("project3_main", "project1_main", DependencyScope.COMPILE)
|
||||
|
||||
assertModuleModuleDepScope("project3_test", "project3_main", DependencyScope.COMPILE)
|
||||
assertModuleModuleDepScope("project3_test", "project2_test", DependencyScope.COMPILE)
|
||||
assertModuleModuleDepScope("project3_test", "project2_main", DependencyScope.COMPILE)
|
||||
assertModuleModuleDepScope("project3_test", "project1_test", DependencyScope.COMPILE)
|
||||
assertModuleModuleDepScope("project3_test", "project1_main", DependencyScope.COMPILE)
|
||||
|
||||
assertModuleModuleDepScope("project3_custom", "project1_custom", DependencyScope.COMPILE)
|
||||
assertModuleModuleDepScope("project3_custom", "project2_main", DependencyScope.COMPILE)
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import java.io.Serializable
|
||||
import java.lang.Exception
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.*
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
interface ArgsInfo : Serializable {
|
||||
val currentArguments: List<String>
|
||||
@@ -48,7 +47,6 @@ interface KotlinGradleModel : Serializable {
|
||||
val coroutines: String?
|
||||
val platformPluginId: String?
|
||||
val implements: String?
|
||||
val transitiveCommonDependencies: Set<String>
|
||||
}
|
||||
|
||||
class KotlinGradleModelImpl(
|
||||
@@ -56,8 +54,7 @@ class KotlinGradleModelImpl(
|
||||
override val compilerArgumentsBySourceSet: CompilerArgumentsBySourceSet,
|
||||
override val coroutines: String?,
|
||||
override val platformPluginId: String?,
|
||||
override val implements: String?,
|
||||
override val transitiveCommonDependencies: Set<String>
|
||||
override val implements: String?
|
||||
) : KotlinGradleModel
|
||||
|
||||
abstract class AbstractKotlinGradleModelBuilder : ModelBuilderService {
|
||||
@@ -70,7 +67,6 @@ abstract class AbstractKotlinGradleModelBuilder : ModelBuilderService {
|
||||
"kotlin2js" to "kotlin-platform-js"
|
||||
)
|
||||
val kotlinPluginIds = listOf("kotlin", "kotlin2js", "kotlin-android")
|
||||
val kotlinPlatformCommonPluginId = "kotlin-platform-common"
|
||||
val ABSTRACT_KOTLIN_COMPILE_CLASS = "org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile"
|
||||
|
||||
fun Task.getSourceSetName(): String {
|
||||
@@ -91,52 +87,11 @@ class KotlinGradleModelBuilder : AbstractKotlinGradleModelBuilder() {
|
||||
override fun canBuild(modelName: String?): Boolean = modelName == KotlinGradleModel::class.java.name
|
||||
|
||||
private fun getImplements(project: Project): Project? {
|
||||
val implementsConfiguration = project.configurations.run { findByName("expectedBy") ?: findByName("implement") } ?: return null
|
||||
val implementsConfiguration = project.configurations.run { findByName("implement") } ?: return null
|
||||
val implementsProjectDependency = implementsConfiguration.dependencies.filterIsInstance<ProjectDependency>().firstOrNull()
|
||||
return implementsProjectDependency?.dependencyProject
|
||||
}
|
||||
|
||||
private val Project.isCommon get() = plugins.hasPlugin(kotlinPlatformCommonPluginId)
|
||||
|
||||
private fun transitiveCommonDependencies(startingProject: Project): Set<String> {
|
||||
val toProcess = LinkedList<Pair<Project, Boolean>>()
|
||||
toProcess.add(startingProject to false)
|
||||
val processed = HashSet<String>()
|
||||
val result = HashSet<String>()
|
||||
|
||||
fun enqueueDependencies(project: Project, isReachableViaImpl: Boolean, collectImpls: Boolean) {
|
||||
val isCommonProject = project.isCommon
|
||||
if (isCommonProject && collectImpls) return
|
||||
|
||||
val configName = if (collectImpls) "implement" else "compile"
|
||||
val compileConfiguration = project.configurations.findByName(configName) ?: return
|
||||
val dependencies = compileConfiguration
|
||||
.dependencies
|
||||
.filterIsInstance<ProjectDependency>()
|
||||
.map { it.dependencyProject }
|
||||
|
||||
for (dep in dependencies) {
|
||||
if (dep.path in processed) continue
|
||||
if (!dep.isCommon && (collectImpls || isCommonProject)) continue
|
||||
toProcess.add(dep to (isReachableViaImpl || collectImpls))
|
||||
}
|
||||
}
|
||||
|
||||
while (toProcess.isNotEmpty()) {
|
||||
val (project, isReachableViaImpl) = toProcess.pollFirst()
|
||||
processed.add(project.path)
|
||||
|
||||
if (isReachableViaImpl) {
|
||||
result.add(project.pathOrName())
|
||||
}
|
||||
|
||||
enqueueDependencies(project, isReachableViaImpl, false)
|
||||
enqueueDependencies(project, isReachableViaImpl, true)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// see GradleProjectResolverUtil.getModuleId() in IDEA codebase
|
||||
private fun Project.pathOrName() = if (path == ":") name else path
|
||||
|
||||
@@ -206,15 +161,13 @@ class KotlinGradleModelBuilder : AbstractKotlinGradleModelBuilder() {
|
||||
|
||||
val platform = platformPluginId ?: pluginToPlatform.entries.singleOrNull { project.plugins.findPlugin(it.key) != null }?.value
|
||||
val implementedProject = getImplements(project)
|
||||
val transitiveCommon = transitiveCommonDependencies(project)
|
||||
|
||||
return KotlinGradleModelImpl(
|
||||
kotlinPluginId != null || platformPluginId != null,
|
||||
compilerArgumentsBySourceSet,
|
||||
getCoroutines(project),
|
||||
platform,
|
||||
implementedProject?.pathOrName(),
|
||||
transitiveCommon
|
||||
implementedProject?.pathOrName()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user