From 09321e5e41da27b4315da581e75757d46c28e9a6 Mon Sep 17 00:00:00 2001 From: Nikita Bobko Date: Mon, 17 May 2021 21:00:25 +0200 Subject: [PATCH] Implement ide-iml-to-gradle-generator This module is used to generate build.gradle.kts files of Kotlin IDE plugin --- .../build.gradle.kts | 16 ++ .../imltogradle/GradleDependencyNotation.kt | 46 +++ .../imltogradle/JpsLikeDependency.kt | 50 ++++ .../kotlin/generators/imltogradle/Main.kt | 264 ++++++++++++++++++ .../kotlin/generators/imltogradle/Util.kt | 100 +++++++ .../flattenExportedTransitiveDependencies.kt | 92 ++++++ settings.gradle | 1 + 7 files changed, 569 insertions(+) create mode 100644 generators/ide-iml-to-gradle-generator/build.gradle.kts create mode 100644 generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/GradleDependencyNotation.kt create mode 100644 generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/JpsLikeDependency.kt create mode 100644 generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/Main.kt create mode 100644 generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/Util.kt create mode 100644 generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/flattenExportedTransitiveDependencies.kt diff --git a/generators/ide-iml-to-gradle-generator/build.gradle.kts b/generators/ide-iml-to-gradle-generator/build.gradle.kts new file mode 100644 index 00000000000..d7fd1702b71 --- /dev/null +++ b/generators/ide-iml-to-gradle-generator/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("jvm") + id("jps-compatible") +} + +dependencies { + implementation(kotlinStdlib("jdk8")) + implementation(intellijDep()) +} + +sourceSets { + "main" { projectDefault() } + "test" {} +} + +val generateIdePluginGradleFiles by generator("org.jetbrains.kotlin.generators.imltogradle.MainKt") diff --git a/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/GradleDependencyNotation.kt b/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/GradleDependencyNotation.kt new file mode 100644 index 00000000000..382c94190f1 --- /dev/null +++ b/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/GradleDependencyNotation.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.generators.imltogradle + +open class GradleDependencyNotation(val dependencyNotation: String, val dependencyConfiguration: String? = null) { + init { + require(dependencyNotation.isNotEmpty()) + require(dependencyConfiguration?.isNotEmpty() ?: true) + } + + companion object { + private const val artifactNameSubregex = """([a-zA-Z\-\._1-9]*?)""" + + private val libPathToGradleNotationRegex = """^lib\/$artifactNameSubregex\.jar$""".toRegex() + private val pluginsPathToGradleNotationRegex = """^plugins\/$artifactNameSubregex\/.*?$""".toRegex() + private val jarToGradleNotationRegex = """^$artifactNameSubregex\.jar$""".toRegex() + + fun fromJarPath(jarPath: String): GradleDependencyNotation? { + if (jarPath == "lib/cds/classesLogAgent.jar") { + return null // TODO remove hack? + } + + if (jarPath.contains("intellij-core.jar")) { + return IntellijCoreGradleDependencyNotation + } + + fun Regex.firstGroup() = matchEntire(jarPath)?.groupValues?.get(1) + + return pluginsPathToGradleNotationRegex.firstGroup()?.let { IntellijPluginDepGradleDependencyNotation(it) } + ?: libPathToGradleNotationRegex.firstGroup()?.let { IntellijDepGradleDependencyNotation(it) } + ?: jarToGradleNotationRegex.firstGroup()?.let { IntellijDepGradleDependencyNotation(it) } + ?: error("Path $jarPath matches none of the regexes") + } + } + + object IntellijCoreGradleDependencyNotation : GradleDependencyNotation("intellijCoreDep()", null) + + data class IntellijPluginDepGradleDependencyNotation(val pluginName: String) : + GradleDependencyNotation("intellijPluginDep(\"$pluginName\", forIde = true)") + + data class IntellijDepGradleDependencyNotation(val jarName: String) : + GradleDependencyNotation("intellijDep(forIde = true)", "{ includeJars(\"$jarName\") }") +} diff --git a/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/JpsLikeDependency.kt b/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/JpsLikeDependency.kt new file mode 100644 index 00000000000..dacc1266270 --- /dev/null +++ b/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/JpsLikeDependency.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.generators.imltogradle + +import org.jetbrains.jps.model.java.JpsJavaDependencyScope + +interface JpsLikeDependency { + fun convertToGradleCall(): String + fun normalizedForComparison(): JpsLikeDependency +} + +class JpsLikeDependencyWithComment(private val base: JpsLikeDependency, private val comment: String) : JpsLikeDependency { + override fun convertToGradleCall(): String { + return "${base.convertToGradleCall()} // $comment" + } + + override fun normalizedForComparison() = base +} + +data class JpsLikeJarDependency( + val dependencyNotation: String, + val scope: JpsJavaDependencyScope, + val dependencyConfiguration: String?, + val exported: Boolean +) : JpsLikeDependency { + override fun convertToGradleCall(): String { + val scopeArg = "JpsDepScope.$scope" + val exportedArg = "exported = true".takeIf { exported } + return "jpsLikeJarDependency(${listOfNotNull(dependencyNotation, scopeArg, dependencyConfiguration, exportedArg).joinToString()})" + } + + override fun normalizedForComparison() = this +} + +data class JpsLikeModuleDependency( + val moduleName: String, + val scope: JpsJavaDependencyScope, + val exported: Boolean +) : JpsLikeDependency { + override fun convertToGradleCall(): String { + val scopeArg = "JpsDepScope.$scope" + val exportedArg = "exported = true".takeIf { exported } + return "jpsLikeModuleDependency(${listOfNotNull("\"$moduleName\"", scopeArg, exportedArg).joinToString()})" + } + + override fun normalizedForComparison() = this +} diff --git a/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/Main.kt b/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/Main.kt new file mode 100644 index 00000000000..aa5612abf00 --- /dev/null +++ b/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/Main.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.generators.imltogradle + +import com.google.gson.JsonParser +import org.jetbrains.jps.model.JpsSimpleElement +import org.jetbrains.jps.model.java.JavaResourceRootType +import org.jetbrains.jps.model.java.JavaSourceRootType +import org.jetbrains.jps.model.java.JpsJavaDependencyScope +import org.jetbrains.jps.model.library.JpsLibrary +import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor +import org.jetbrains.jps.model.library.JpsOrderRootType +import org.jetbrains.jps.model.module.* +import org.jetbrains.kotlin.generators.imltogradle.GradleDependencyNotation.IntellijDepGradleDependencyNotation +import java.io.File +import java.util.* + +private lateinit var intellijModuleNameToGradleDependencyNotationsMapping: Map> +private val KOTLIN_REPO_ROOT = File(".").canonicalFile +private val INTELLIJ_REPO_ROOT = KOTLIN_REPO_ROOT.resolve("kotlin-ide") + +private val intellijModuleNameToGradleDependencyNotationsMappingManual: List> = listOf( + "intellij.platform.jps.build" to GradleDependencyNotation("jpsBuildTest()"), + "intellij.platform.structuralSearch" to IntellijDepGradleDependencyNotation("structuralsearch") // for some reason it's absent in json mapping +) + +// These modules are used in Kotlin plugin and IDEA doesn't publish artifact of these modules +private val intellijModulesForWhichGenerateBuildGradle = listOf( + "intellij.platform.debugger.testFramework", + "intellij.gradle.tests", + "intellij.platform.externalSystem.tests", + "intellij.platform.lang.tests", + "intellij.platform.testExtensions", + "intellij.java.compiler.tests", + "intellij.gradle.toolingExtension.tests" +) + +val jsonUrlPrefixes = mapOf( + "202" to "https://buildserver.labs.intellij.net/guestAuth/repository/download/ijplatform_IjPlatform202_IntellijArtifactMappings/113235432:id", + "203" to "https://buildserver.labs.intellij.net/guestAuth/repository/download/ijplatform_IjPlatform203_IntellijArtifactMappings/117989041:id", +) + +fun main() { + val ijCommunityModuleNameToJpsModuleMapping = INTELLIJ_REPO_ROOT.loadJpsProject().modules.associateBy { it.name } + + val imlFiles = INTELLIJ_REPO_ROOT.walk() + .onEnter { it != INTELLIJ_REPO_ROOT.resolve("out") } + .filter { + it.isFile && it.extension == "iml" && + (it.name.startsWith("kotlin.") || it.nameWithoutExtension in intellijModulesForWhichGenerateBuildGradle) + } + .toList() + + val ideaMajorVersion = KOTLIN_REPO_ROOT.resolve("local.properties").takeIf { it.exists() } + ?.inputStream() + ?.use { Properties().apply { load(it) }.getProperty("attachedIntellijVersion") } + ?: error("Can't find 'attachedIntellijVersion' in 'local.properties'") + + intellijModuleNameToGradleDependencyNotationsMapping = fetchJsonsFromBuildserver(ideaMajorVersion) + .flatMap { jsonStr -> + JsonParser.parseString(jsonStr).asJsonArray.mapNotNull { jsonElement -> + val jsonObject = jsonElement.asJsonObject + val moduleName = jsonObject.get("moduleName")?.asString ?: return@mapNotNull null + val jarPath = jsonObject.get("path")?.asString ?: return@mapNotNull null + moduleName to jarPath + } + } + .filter { (_, jarPath) -> !jarPath.contains("DatabaseTools") && !jarPath.contains("lib/openapi.jar") } + .groupBy( + keySelector = { (_, jarPath) -> jarPath }, + valueTransform = { (moduleName, _) -> moduleName } + ) + .filter { (_, moduleNames) -> + moduleNames.all { it in ijCommunityModuleNameToJpsModuleMapping } // filter out ultimate jars + } + .flatMap { (jarPath, moduleNames) -> moduleNames.map { it to jarPath } } + .mapNotNull { (moduleName, jarPath) -> + moduleName to (GradleDependencyNotation.fromJarPath(jarPath) ?: return@mapNotNull null) + } + .plus(intellijModuleNameToGradleDependencyNotationsMappingManual) + .groupBy( + keySelector = { (moduleName, _) -> moduleName }, + valueTransform = { (_, dependencyNotation) -> dependencyNotation } + ) + + imlFiles.mapNotNull { imlFile -> ijCommunityModuleNameToJpsModuleMapping[imlFile.nameWithoutExtension]?.let { imlFile to it } } + .forEach { (imlFile, jpsModule) -> + imlFile.parentFile.resolve("build.gradle.kts").writeText(convertJpsModule(imlFile, jpsModule)) + } +} + +fun convertJpsLibrary(lib: JpsLibrary, scope: JpsJavaDependencyScope, exported: Boolean): List { + val mavenRepositoryLibraryDescriptor = lib.properties + .safeAs>()?.data?.safeAs() + + return when { + mavenRepositoryLibraryDescriptor == null -> { + lib.getRootUrls(JpsOrderRootType.COMPILED) + .map { File(it.removePrefix("jar://").removeSuffix("!/")).relativeTo(KOTLIN_REPO_ROOT) } + .map { + JpsLikeJarDependency( + "files(rootDir.resolve(\"$it\").canonicalPath)", + scope, + dependencyConfiguration = null, + exported = exported + ) + } + } + lib.name == "kotlinc.kotlin-stdlib-jdk8" -> { + listOf( + JpsLikeJarDependency("kotlinStdlib()", scope, dependencyConfiguration = null, exported = exported), + // TODO remove hack (for some reason we have to specify :kotlin-stdlib-jdk7 explicitly, otherwise compilation doesn't pass) + JpsLikeJarDependency("project(\":kotlin-stdlib-jdk7\")", scope, dependencyConfiguration = null, exported = exported) + ) + } + lib.name.startsWith("kotlinc.") -> { + val artifactId = mavenRepositoryLibraryDescriptor.artifactId + val dependencyNotation = + if (KOTLIN_REPO_ROOT.resolve("prepare/ide-plugin-dependencies/$artifactId").exists()) + "project(\":prepare:ide-plugin-dependencies:$artifactId\")" + else + "project(\":${mavenRepositoryLibraryDescriptor.artifactId}\")" + listOf(JpsLikeJarDependency(dependencyNotation, scope, dependencyConfiguration = null, exported = exported)) + } + else -> { + val dependencyNotation = "\"${mavenRepositoryLibraryDescriptor.mavenId}\"" + listOf(JpsLikeJarDependency(dependencyNotation, scope, dependencyConfiguration = null, exported = exported)) + } + } +} + +fun convertIntellijDependencyNotFollowingTransitive(dep: JpsDependencyDescriptor, exported: Boolean): List { + return when (val moduleOrLibrary = dep.moduleOrLibrary) { + is Either.First -> { + val moduleName = moduleOrLibrary.value.name + if (moduleName in intellijModulesForWhichGenerateBuildGradle) { + listOf(JpsLikeModuleDependency(":kotlin-ide.$moduleName", dep.scope, exported)) + } else { + intellijModuleNameToGradleDependencyNotationsMapping[moduleName] + .also { if (it == null) println("WARNING: Cannot find GradleDependencyNotation for $moduleName") } + ?.map { JpsLikeJarDependency(it.dependencyNotation, dep.scope, it.dependencyConfiguration, exported) } + ?: emptyList() + } + } + is Either.Second -> convertJpsLibrary(moduleOrLibrary.value, dep.scope, exported) + } +} + +fun convertJpsModuleDependency(dep: JpsModuleDependency): List { + val moduleName = dep.moduleReference.moduleName + return when { + moduleName.startsWith("kotlin.") -> { + listOf(JpsLikeModuleDependency(":kotlin-ide.$moduleName", dep.scope, dep.isExported)) + } + moduleName.startsWith("intellij.") -> { + dep.module.orElse { error("Cannot resolve dependency ${dep.moduleReference.moduleName}") } + .flattenExportedTransitiveDependencies() + .map { it.copy(scope = it.scope intersectCompileClasspath dep.scope) } + .filter { it.scope != JpsJavaDependencyScope.RUNTIME } // We are interested only in transitive dependencies which affect compilation + .flatMap { convertIntellijDependencyNotFollowingTransitive(it, dep.isExported).asSequence() } + .mapIndexed { index, jpsLikeDependency -> + if (index != 0) JpsLikeDependencyWithComment(jpsLikeDependency, "Exported transitive dependency") else jpsLikeDependency + } + .toList() + } + else -> error("Cannot convert module dependency to Gradle $dep") + } +} + +fun convertJpsDependencyElement(dep: JpsDependencyElement): List { + return when (dep) { + is JpsModuleDependency -> convertJpsModuleDependency(dep) + is JpsLibraryDependency -> dep.library + .orElse { error("Cannot resolve library reference = ${dep.libraryReference}") } + .let { convertJpsLibrary(it, dep.scope, dep.isExported) } + else -> error("Unknown dependency: $dep") + } +} + +fun convertJpsModuleSourceRoot(imlFile: File, sourceRoot: JpsModuleSourceRoot): String { + return when (sourceRoot.rootType) { + is JavaSourceRootType -> "java.srcDir(\"${sourceRoot.file.relativeTo(imlFile.parentFile)}\")" + is JavaResourceRootType -> "resources.srcDir(\"${sourceRoot.file.relativeTo(imlFile.parentFile)}\")" + else -> error("Unknown sourceRoot = $sourceRoot") + } +} + +fun convertJpsModule(imlFile: File, jpsModule: JpsModule): String { + val (src, test) = jpsModule.sourceRoots + .groupBy { it.rootType.isForTests } + .mapValues { entry -> entry.value.joinToString("\n") { convertJpsModuleSourceRoot(imlFile, it) } } + .let { Pair(it[false] ?: "", it[true] ?: "") } + + val mavenRepos = INTELLIJ_REPO_ROOT.resolve(".idea/jarRepositories.xml").readXml().traverseChildren() + .filter { it.getAttributeValue("name") == "url" } + .map { it.getAttributeValue("value")!! } + .map { "maven { setUrl(\"$it\") }" } + .joinToString("\n") + + fun File.compilerArgsFromIml() = readXml().traverseChildren().singleOrNull { it.name == "compilerSettings" }?.children?.single() + ?.getAttributeValue("value") + + fun File.compilerArgsFromProjectSettings() = readXml().traverseChildren() + .singleOrNull { it.getAttributeValue("name") == "additionalArguments" }?.getAttributeValue("value") + + val compilerArgs = imlFile.compilerArgsFromIml() + .orElse { INTELLIJ_REPO_ROOT.resolve(".idea/kotlinc.xml").compilerArgsFromProjectSettings() } + ?.split(" ") + ?.joinToString { "\"$it\"" } + ?: "" + + val deps = jpsModule.dependencies.flatMap { convertJpsDependencyElement(it) } + .distinctBy { it.normalizedForComparison() } + .joinToString("\n") { it.convertToGradleCall() } + return """ + |// GENERATED build.gradle.kts + |// GENERATED BY ${imlFile.name} + |// USE `./gradlew generateIdeaGradleFiles` TO REGENERATE THIS FILE + | + |plugins { + | kotlin("jvm") + | id("jps-compatible") + |} + | + |repositories { + | $mavenRepos + |} + | + |dependencies { + | implementation(toolsJarApi()) + | $deps + |} + | + |configurations.all { + | exclude(module = "tests-common") // Avoid classes with same FQN clashing + |} + | + |sourceSets { + | "main" { + | $src + | } + | "test" { + | $test + | } + |} + | + |java { + | toolchain { + | languageVersion.set(JavaLanguageVersion.of(11)) + | } + |} + | + |tasks.withType { + | kotlinOptions.freeCompilerArgs = listOf($compilerArgs) + | kotlinOptions.jdkHome = rootProject.extra["JDK_11"] as String + | kotlinOptions.useOldBackend = true // KT-45697 + |} + | + |testsJar() + """.trimMarginWithInterpolations() +} diff --git a/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/Util.kt b/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/Util.kt new file mode 100644 index 00000000000..1201e7601c7 --- /dev/null +++ b/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/Util.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.generators.imltogradle + +import org.jdom.Element +import org.jdom.input.SAXBuilder +import org.jetbrains.jps.model.JpsElementFactory +import org.jetbrains.jps.model.JpsProject +import org.jetbrains.jps.model.java.JpsJavaDependencyScope +import org.jetbrains.jps.model.java.impl.JpsJavaExtensionServiceImpl +import org.jetbrains.jps.model.module.JpsDependencyElement +import org.jetbrains.jps.model.module.JpsLibraryDependency +import org.jetbrains.jps.model.module.JpsModule +import org.jetbrains.jps.model.module.JpsModuleDependency +import org.jetbrains.jps.model.serialization.JpsProjectLoader +import java.io.File +import java.net.URL + +fun String.trimMarginWithInterpolations(): String { + val regex = Regex("""^(\s*\|)(\s*).*$""") + val out = mutableListOf() + var prevIndent = "" + for (line in lines()) { + val matchResult = regex.matchEntire(line) + if (matchResult != null) { + out.add(line.removePrefix(matchResult.groupValues[1])) + prevIndent = matchResult.groupValues[2] + } else { + out.add(prevIndent + line) + } + } + return out.joinToString("\n").trim() +} + +fun File.readXml(): Element { + return inputStream().use { SAXBuilder().build(it).rootElement } +} + +suspend fun SequenceScope.visit(element: Element) { + element.children.forEach { visit(it) } + yield(element) +} +fun Element.traverseChildren(): Sequence { + return sequence { visit(this@traverseChildren) } +} + +inline fun Any?.safeAs(): T? { + return this as? T +} + +val JpsDependencyElement.scope: JpsJavaDependencyScope + get() = JpsJavaExtensionServiceImpl.getInstance().getDependencyExtension(this)?.scope + ?: error("Cannot get dependency scope for $this") + +val JpsDependencyElement.isExported: Boolean + get() = JpsJavaExtensionServiceImpl.getInstance().getDependencyExtension(this)?.isExported + ?: error("Cannot get dependency isExported for $this") + +fun File.loadJpsProject(): JpsProject { + val model = JpsElementFactory.getInstance().createModel() + val project = model.project + JpsProjectLoader.loadProject(project, mapOf(), this.canonicalPath) + return project +} + +sealed class Either { + data class First(val value: A) : Either() + data class Second(val value: B) : Either() +} + +val Either.value: T + get() = when (this) { + is Either.First -> this.value + is Either.Second -> this.value + } + +inline fun T?.orElse(block: () -> T): T = this ?: block() + +val JpsModule.dependencies: List + get() = dependenciesList.dependencies.filter { it is JpsModuleDependency || it is JpsLibraryDependency } + +fun fetchJsonsFromBuildserver(ideaMajorVersion: String): List { + require(ideaMajorVersion.length == 3 && ideaMajorVersion.all { it.isDigit() }) { + "attachedIntellijVersion='$ideaMajorVersion' must be 3 length all digit string" + } + val urlPrefix = jsonUrlPrefixes[ideaMajorVersion] ?: error("'$ideaMajorVersion' platform is absent in mapping") + return listOf( + "$urlPrefix/ideaIU-project-structure-mapping.json", + "$urlPrefix/intellij-core-project-structure-mapping.json" + ).map { url -> + try { + URL(url).readText() + } catch (ex: Throwable) { + error("Can't access $url. Is VPN on?") + } + } +} diff --git a/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/flattenExportedTransitiveDependencies.kt b/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/flattenExportedTransitiveDependencies.kt new file mode 100644 index 00000000000..c9a89448d5b --- /dev/null +++ b/generators/ide-iml-to-gradle-generator/src/org/jetbrains/kotlin/generators/imltogradle/flattenExportedTransitiveDependencies.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.generators.imltogradle + +import org.jetbrains.jps.model.java.JpsJavaDependencyScope +import org.jetbrains.jps.model.library.JpsLibrary +import org.jetbrains.jps.model.module.JpsDependencyElement +import org.jetbrains.jps.model.module.JpsLibraryDependency +import org.jetbrains.jps.model.module.JpsModule +import org.jetbrains.jps.model.module.JpsModuleDependency +import java.util.* + +data class JpsDependencyDescriptor(val moduleOrLibrary: Either, val scope: JpsJavaDependencyScope) { + companion object { + fun from(dep: JpsDependencyElement): JpsDependencyDescriptor? { + val moduleOrLibrary = when (dep) { + is JpsModuleDependency -> dep.module + .orElse { error("Cannot resolve module reference = ${dep.moduleReference}") } + .let { Either.First(it) } + is JpsLibraryDependency -> dep.library + .orElse { error("Cannot resolve library reference = ${dep.libraryReference}") } + .let { Either.Second(it) } + else -> return null + } + return JpsDependencyDescriptor(moduleOrLibrary, dep.scope) + } + } +} + +fun JpsModule.flattenExportedTransitiveDependencies(): Sequence { + val visitedModuleNames = HashSet() + val toVisit = PriorityQueue( + compareBy(JpsDependencyScopeCompileClasspathComparator.reversed()) { it.scope } + .thenComparing(compareBy { it.moduleOrLibrary.value.name }) + ) + + // Dijkstra's modified algorithm: intersectCompileClasspath is like sum + suspend fun SequenceScope.visit(current: JpsDependencyDescriptor) { + when (val moduleOrLibrary = current.moduleOrLibrary) { + is Either.First -> { + val jpsModule = moduleOrLibrary.value + if (!visitedModuleNames.add(jpsModule.name)) { + return + } + yield(current) + val elements = jpsModule.dependencies + .filter { it.isExported } + .map { JpsDependencyDescriptor.from(it)!! } + .map { it.copy(scope = it.scope intersectCompileClasspath current.scope) } + toVisit.addAll(elements) + while (toVisit.isNotEmpty()) { + visit(toVisit.poll()) + } + } + is Either.Second -> yield(current) + } + } + return sequence { + visit( + JpsDependencyDescriptor( + Either.First(this@flattenExportedTransitiveDependencies), + JpsDependencyScopeCompileClasspathComparator.ascending.last() + ) + ) + } +} + +private object JpsDependencyScopeCompileClasspathComparator : Comparator { + // Let's assume that module A depends on module B (A->B notation) + // A[src] notation means "source root" of module A. A[test] notation means "test root" of module A. + // Keeping this in mind let's consider different types of dependencies: + val ascending = listOf( + JpsJavaDependencyScope.RUNTIME, // Doesn't establish any compile time dependencies + JpsJavaDependencyScope.TEST, // Compile time dependencies are: A[test]->B[src], A[test]->B[test] (so TEST > RUNTIME) + JpsJavaDependencyScope.PROVIDED, // Compile time dependencies are like in TEST + A[src]->B[src] (so PROVIDED > TEST) + JpsJavaDependencyScope.COMPILE, /* Compile time dependencies are like in PROVIDED + (so it actually doesn't matter in which order PROVIDED and COMPILE go) */ + ) + + override fun compare(first: JpsJavaDependencyScope?, second: JpsJavaDependencyScope?): Int { + val firstIndex = ascending.indexOf(first).takeIf { it != -1 } ?: error("Unknown $first") + val secondIndex = ascending.indexOf(second).takeIf { it != -1 } ?: error("Unknown $second") + return firstIndex.compareTo(secondIndex) + } +} + +infix fun JpsJavaDependencyScope.intersectCompileClasspath(other: JpsJavaDependencyScope): JpsJavaDependencyScope { + return minOf(this, other, JpsDependencyScopeCompileClasspathComparator) +} diff --git a/settings.gradle b/settings.gradle index 3ae470509d7..1239032892a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -194,6 +194,7 @@ include ":benchmarks", ":compiler:tests-different-jdk", ":compiler:tests-spec", ":generators", + ":generators:ide-iml-to-gradle-generator", ":generators:test-generator", ":tools:kotlinp", ":kotlin-project-model",