Gradle: Add dependencies for indirectly implemented modules

#KT-16926 Fixed
This commit is contained in:
Alexey Sedunov
2017-10-13 18:46:21 +03:00
parent 0a808528b7
commit a2a8335bab
3 changed files with 194 additions and 83 deletions
@@ -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)
}
}
}
@@ -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()
)
}
}