Initial Pill implementation

Pill is a Gradle-to-JPS model generator for compiler + compiler plugins + IDE project parts.
The generated project model does not know anything about Gradle and is fully operable using only JPS.
This commit is contained in:
Yan Zhulanow
2018-02-14 23:50:45 +03:00
parent 0ebcd0ad40
commit af60bcfb47
9 changed files with 739 additions and 0 deletions
+1
View File
@@ -30,6 +30,7 @@ build/
.idea/libraries/Gradle*.xml
.idea/libraries/Maven*.xml
.idea/modules
.idea/libraries
.idea/modules.xml
.idea/gradle.xml
.idea/compiler.xml
+15
View File
@@ -0,0 +1,15 @@
@file:Suppress("PackageDirectoryMismatch")
package org.jetbrains.kotlin.pill
import org.gradle.api.artifacts.*
class DependencyMapper(
val group: String,
val module: String,
vararg val configurations: String,
val mapping: (ResolvedDependency) -> MappedDependency?
)
class MappedDependency(val main: PDependency, val deferred: List<PDependency> = emptyList())
class ParserContext(val dependencyMappers: List<DependencyMapper>)
+355
View File
@@ -0,0 +1,355 @@
@file:Suppress("PackageDirectoryMismatch")
package org.jetbrains.kotlin.pill
import org.gradle.api.Project
import org.gradle.api.artifacts.*
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.kotlin.dsl.configure
import org.gradle.plugins.ide.idea.IdeaPlugin
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.internal.HasConvention
import org.jetbrains.kotlin.pill.POrderRoot.*
import org.jetbrains.kotlin.pill.PSourceRoot.*
import java.io.File
import java.util.LinkedList
data class PProject(
val name: String,
val rootDirectory: File,
val modules: List<PModule>,
val libraries: List<PLibrary>
)
data class PModule(
val name: String,
val bundleName: String,
val rootDirectory: File,
val contentRoots: List<PContentRoot>,
val orderRoots: List<POrderRoot>,
val moduleForProductionSources: PModule? = null,
val group: String? = null
)
data class PContentRoot(
val path: File,
val forTests: Boolean,
val sourceRoots: List<PSourceRoot>,
val excludedDirectories: List<File>
)
data class PSourceRoot(
val path: File,
val kind: Kind
) {
enum class Kind {
PRODUCTION, TEST, RESOURCES, TEST_RESOURCES;
val isResources get() = this == RESOURCES || this == TEST_RESOURCES
}
}
data class POrderRoot(
val dependency: PDependency,
val scope: Scope,
val isExported: Boolean = false,
val isProductionOnTestDependency: Boolean = false
) {
enum class Scope { COMPILE, TEST, RUNTIME, PROVIDED }
}
sealed class PDependency {
data class Module(val name: String) : PDependency()
data class Library(val name: String) : PDependency()
data class ModuleLibrary(val library: PLibrary) : PDependency()
}
data class PLibrary(
val name: String,
val classes: List<File>,
val javadoc: List<File> = emptyList(),
val sources: List<File> = emptyList(),
val annotations: List<File> = emptyList(),
val dependencies: List<PLibrary> = emptyList()
) {
fun attachSource(file: File): PLibrary {
return this.copy(sources = this.sources + listOf(file))
}
}
fun parse(project: Project, context: ParserContext): PProject = with (context) {
val modules = project.allprojects
.filter { it.plugins.hasPlugin(JpsCompatiblePlugin::class.java) }
.flatMap { parseModules(it) }
return PProject("Kotlin", project.rootProject.projectDir, modules, emptyList())
}
private val CONFIGURATION_MAPPING = mapOf(
listOf("compile") to Scope.COMPILE,
listOf("compileOnly") to Scope.PROVIDED,
listOf("runtime") to Scope.RUNTIME
)
private val TEST_CONFIGURATION_MAPPING = mapOf(
listOf("compile", "testCompile") to Scope.COMPILE,
listOf("compileOnly", "testCompileOnly") to Scope.PROVIDED,
listOf("runtime", "testRuntime") to Scope.RUNTIME
)
private val SOURCE_SET_MAPPING = mapOf(
"main" to Kind.PRODUCTION,
"test" to Kind.TEST
)
private fun ParserContext.parseModules(project: Project): List<PModule> {
val (productionContentRoots, testContentRoots) = parseContentRoots(project).partition { !it.forTests }
val modules = mutableListOf<PModule>()
fun getJavaExcludedDirs() = project.plugins.findPlugin(IdeaPlugin::class.java)
?.model?.module?.excludeDirs?.toList() ?: emptyList()
val allExcludedDirs = getJavaExcludedDirs() + project.buildDir
var productionSourcesModule: PModule? = null
for ((nameSuffix, roots) in mapOf("src" to productionContentRoots, "test" to testContentRoots)) {
if (roots.isEmpty()) {
continue
}
val mainRoot = roots.first()
var dependencies = parseDependencies(project, mainRoot.forTests)
if (productionContentRoots.isNotEmpty() && mainRoot.forTests) {
val productionModuleDependency = PDependency.Module(project.name + ".src")
dependencies += POrderRoot(productionModuleDependency, Scope.COMPILE, true)
}
val module = PModule(
project.name + "." + nameSuffix,
project.name,
mainRoot.path,
roots,
dependencies,
productionSourcesModule
)
modules += module
if (!mainRoot.forTests) {
productionSourcesModule = module
}
}
modules += PModule(
project.name,
project.name,
project.projectDir,
listOf(PContentRoot(project.projectDir, false, emptyList(), allExcludedDirs)),
if (modules.isEmpty()) parseDependencies(project, false) else emptyList()
)
return modules
}
private fun parseContentRoots(project: Project): List<PContentRoot> {
val sourceRoots = parseSourceRoots(project).groupBy { it.kind }
fun getRoots(kind: PSourceRoot.Kind) = sourceRoots[kind] ?: emptyList()
val productionSourceRoots = getRoots(Kind.PRODUCTION) + getRoots(Kind.RESOURCES)
val testSourceRoots = getRoots(Kind.TEST) + getRoots(Kind.TEST_RESOURCES)
fun createContentRoots(sourceRoots: List<PSourceRoot>, forTests: Boolean): List<PContentRoot> {
return sourceRoots.map { PContentRoot(it.path, forTests, listOf(it), emptyList()) }
}
return createContentRoots(productionSourceRoots, forTests = false) +
createContentRoots(testSourceRoots, forTests = true)
}
private fun parseSourceRoots(project: Project): List<PSourceRoot> {
if (!project.plugins.hasPlugin(JavaPlugin::class.java)) {
return emptyList()
}
val sourceRoots = mutableListOf<PSourceRoot>()
project.configure<JavaPluginConvention> {
for ((sourceSetName, kind) in SOURCE_SET_MAPPING) {
val sourceSet = sourceSets.findByName(sourceSetName) ?: continue
fun Any.getKotlin(): SourceDirectorySet {
val kotlinMethod = javaClass.getMethod("getKotlin")
val oldIsAccessible = kotlinMethod.isAccessible
try {
kotlinMethod.isAccessible = true
return kotlinMethod(this) as SourceDirectorySet
} finally {
kotlinMethod.isAccessible = oldIsAccessible
}
}
val kotlinSourceDirectories = (sourceSet as HasConvention).convention
.plugins["kotlin"]?.getKotlin()?.srcDirs
?: emptySet()
val directories = (sourceSet.java.sourceDirectories.files + kotlinSourceDirectories).toList()
.filter { it.exists() }
.takeIf { it.isNotEmpty() }
?: continue
for (resourceRoot in sourceSet.resources.sourceDirectories.files) {
if (!resourceRoot.exists() || resourceRoot in directories) {
continue
}
val resourceRootKind = when (kind) {
Kind.PRODUCTION -> Kind.RESOURCES
Kind.TEST -> Kind.TEST_RESOURCES
else -> error("Invalid source root kind $kind")
}
sourceRoots += PSourceRoot(resourceRoot, resourceRootKind)
}
for (directory in directories) {
sourceRoots += PSourceRoot(directory, kind)
}
}
}
return sourceRoots
}
private fun ParserContext.parseDependencies(project: Project, forTests: Boolean): List<POrderRoot> {
val configurationMapping = if (forTests) TEST_CONFIGURATION_MAPPING else CONFIGURATION_MAPPING
with(project.configurations) {
val moduleRoots = mutableListOf<POrderRoot>()
val libraryRoots = mutableListOf<POrderRoot>()
val deferredRoots = mutableListOf<POrderRoot>()
fun collectConfigurations(): List<Pair<ResolvedConfiguration, Scope>> {
val configurations = mutableListOf<Pair<ResolvedConfiguration, Scope>>()
for ((configurationNames, scope) in configurationMapping) {
for (configurationName in configurationNames) {
val configuration = findByName(configurationName)?.also { it.resolve() } ?: continue
configurations += Pair(configuration.resolvedConfiguration, scope)
}
}
return configurations
}
nextDependency@ for ((configuration, scope, dependency) in collectConfigurations().collectDependencies()) {
for (mapper in dependencyMappers) {
if (dependency.moduleGroup == mapper.group
&& dependency.moduleName == mapper.module
&& dependency.configuration in mapper.configurations
) {
val mappedDependency = mapper.mapping(dependency)
if (mappedDependency != null) {
val orderRoot = POrderRoot(mappedDependency.main, scope)
if (mappedDependency.main is PDependency.Module) {
moduleRoots += orderRoot
} else {
libraryRoots += orderRoot
}
for (deferredDep in mappedDependency.deferred) {
deferredRoots += POrderRoot(deferredDep, scope)
}
}
continue@nextDependency
}
}
if (dependency.configuration == "runtimeElements") {
moduleRoots += POrderRoot(PDependency.Module(dependency.moduleName + ".src"), scope)
} else if (dependency.configuration == "tests-jar") {
moduleRoots += POrderRoot(
PDependency.Module(dependency.moduleName + ".test"),
scope,
isProductionOnTestDependency = true
)
} else {
val classes = dependency.moduleArtifacts.map { it.file }
val library = PLibrary(dependency.moduleName, classes)
libraryRoots += POrderRoot(PDependency.ModuleLibrary(library), scope)
}
}
return removeDuplicates(moduleRoots + libraryRoots + deferredRoots)
}
}
private fun removeDuplicates(roots: List<POrderRoot>): List<POrderRoot> {
val dependenciesByScope = roots.groupBy { it.scope }.mapValues { it.value.mapTo(mutableSetOf()) { it.dependency } }
fun depenciesFor(scope: Scope) = dependenciesByScope[scope] ?: emptySet<PDependency>()
val result = mutableSetOf<POrderRoot>()
for (root in roots.distinct()) {
val scope = root.scope
val dependency = root.dependency
if (root in result) {
continue
}
if ((scope == Scope.PROVIDED || scope == Scope.RUNTIME) && dependency in depenciesFor(Scope.COMPILE)) {
continue
}
if (scope == Scope.PROVIDED && dependency in depenciesFor(Scope.RUNTIME)) {
result += POrderRoot(dependency, Scope.COMPILE)
continue
}
if (scope == Scope.RUNTIME && dependency in depenciesFor(Scope.PROVIDED)) {
result += POrderRoot(dependency, Scope.COMPILE)
continue
}
result += root
}
return result.toList()
}
private data class DependencyInfo(val configuration: ResolvedConfiguration, val scope: Scope, val dependency: ResolvedDependency)
private fun List<Pair<ResolvedConfiguration, Scope>>.collectDependencies(): List<DependencyInfo> {
val dependencies = mutableListOf<DependencyInfo>()
val unprocessed = LinkedList<DependencyInfo>()
val existing = mutableSetOf<Pair<Scope, ResolvedDependency>>()
for ((configuration, scope) in this) {
for (dependency in configuration.firstLevelModuleDependencies) {
unprocessed += DependencyInfo(configuration, scope, dependency)
}
}
while (unprocessed.isNotEmpty()) {
val info = unprocessed.removeAt(0)
dependencies += info
val data = Pair(info.scope, info.dependency)
existing += data
for (child in info.dependency.children) {
if (child.moduleGroup != "org.jetbrains.kotlin" && Pair(info.scope, child) in existing) {
continue
}
unprocessed += DependencyInfo(info.configuration, info.scope, child)
}
}
return dependencies
}
@@ -0,0 +1,34 @@
@file:Suppress("PackageDirectoryMismatch")
package org.jetbrains.kotlin.pill
import java.io.File
interface PathContext {
operator fun invoke(file: File): String
fun url(file: File): Pair<String, String> {
val path = when {
file.isFile && file.extension.toLowerCase() == "jar" -> "jar://" + this(file) + "!/"
else -> "file://" + this(file)
}
return Pair("url", path)
}
}
class ProjectContext(val project: PProject) : PathContext {
override fun invoke(file: File): String {
return file.absolutePath
.replace(project.rootDirectory.absolutePath, "\$PROJECT_DIR\$")
}
}
class ModuleContext(val project: PProject, val module: PModule) : PathContext {
override fun invoke(file: File): String {
if (!file.startsWith(project.rootDirectory)) {
return file.absolutePath
}
return "\$MODULE_DIR\$/" + file.toRelativeString(module.rootDirectory)
}
}
+133
View File
@@ -0,0 +1,133 @@
package org.jetbrains.kotlin.pill
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
class JpsCompatiblePlugin : Plugin<Project> {
override fun apply(project: Project) {}
}
class JpsCompatibleRootPlugin : Plugin<Project> {
companion object {
private fun mapper(module: String, vararg configurations: String): DependencyMapper {
return DependencyMapper("org.jetbrains.kotlin", module, *configurations) { MappedDependency(PDependency.Library(module)) }
}
private val dependencyMappers = listOf(
mapper("protobuf-relocated", "default"),
mapper("kotlin-test-junit", "distJar", "runtimeElements"),
mapper("kotlin-script-runtime", "distJar", "runtimeElements"),
mapper("kotlin-reflect", "distJar", "runtimeElements"),
mapper("kotlin-test-jvm", "distJar", "runtimeElements"),
DependencyMapper("org.jetbrains.kotlin", "kotlin-stdlib", "distJar", "runtimeElements") {
MappedDependency(
PDependency.Library("kotlin-stdlib"),
listOf(PDependency.Library("annotations-13.0"))
)
},
DependencyMapper("org.jetbrains.kotlin", "kotlin-reflect-api", "runtimeElements") {
MappedDependency(PDependency.Library("kotlin-reflect"))
},
DependencyMapper("org.jetbrains.kotlin", "kotlin-compiler-embeddable", "runtimeJar") { null },
DependencyMapper("org.jetbrains.kotlin", "kotlin-stdlib-js", "distJar") { null },
DependencyMapper("org.jetbrains.kotlin", "kotlin-compiler", "runtimeJar") { null },
DependencyMapper("org.jetbrains.kotlin", "compiler", "runtimeElements") { null }
)
fun getProjectLibraries(project: PProject): List<PLibrary> {
fun distJar(name: String) = File(project.rootDirectory, "dist/kotlinc/lib/$name.jar")
fun projectFile(path: String) = File(project.rootDirectory, path)
return listOf(
PLibrary(
"kotlin-stdlib",
classes = listOf(distJar("kotlin-stdlib")),
sources = listOf(distJar("kotlin-stdlib-sources"))
),
PLibrary(
"kotlin-reflect",
classes = listOf(distJar("kotlin-reflect")),
sources = listOf(distJar("kotlin-reflect-sources"))
),
PLibrary(
"annotations-13.0",
classes = listOf(distJar("annotations-13.0"))
),
PLibrary(
"kotlin-test-jvm",
classes = listOf(distJar("kotlin-test")),
sources = listOf(distJar("kotlin-test-sources"))
),
PLibrary(
"kotlin-test-junit",
classes = listOf(distJar("kotlin-test-junit")),
sources = listOf(distJar("kotlin-test-junit-sources"))
),
PLibrary(
"kotlin-script-runtime",
classes = listOf(distJar("kotlin-script-runtime")),
sources = listOf(distJar("kotlin-script-runtime-sources"))
),
PLibrary(
"protobuf-relocated",
classes = listOf(projectFile("custom-dependencies/protobuf-relocated/build/libs/protobuf-java-relocated-2.6.1.jar"))
)
)
}
}
override fun apply(project: Project) {
project.tasks.create("pill") {
this.doLast { pill(project) }
}
}
private lateinit var platformVersion: String
private lateinit var platformDir: File
private fun pill(project: Project) {
platformVersion = project.rootProject.extensions.extraProperties.get("versions.intellijSdk").toString()
platformDir = File(project.projectDir, "buildSrc/prepare-deps/intellij-sdk/build/repo/kotlin.build.custom.deps/$platformVersion")
val jpsProject = parse(project, ParserContext(dependencyMappers))
.mapLibraries(attachPlatformSources(platformVersion))
val files = render(jpsProject, getProjectLibraries(jpsProject))
File(project.rootProject.projectDir, ".idea/libraries").deleteRecursively()
for (file in files) {
val stubFile = file.path
stubFile.parentFile.mkdirs()
stubFile.writeText(file.text)
}
}
fun attachPlatformSources(platformVersion: String) = fun(library: PLibrary): PLibrary {
val platformSourcesJar = File(platformDir, "sources/ideaIC-$platformVersion-sources.jar")
if (library.classes.any { it.startsWith(platformDir) }) {
return library.attachSource(platformSourcesJar)
}
return library
}
private fun PProject.mapLibraries(vararg mappers: (PLibrary) -> PLibrary): PProject {
fun mapLibrary(root: POrderRoot): POrderRoot {
val dependency = root.dependency
if (dependency is PDependency.ModuleLibrary) {
val newLibrary = mappers.fold(dependency.library) { lib, mapper -> mapper(lib) }
return root.copy(dependency = dependency.copy(library = newLibrary))
}
return root
}
val modules = this.modules.map { it.copy(orderRoots = it.orderRoots.map(::mapLibrary)) }
return this.copy(modules = modules)
}
}
+149
View File
@@ -0,0 +1,149 @@
@file:Suppress("PackageDirectoryMismatch")
package org.jetbrains.kotlin.pill
import java.io.File
class PFile(val path: File, val text: String)
fun PFile(path: File, xml: xml) = PFile(path, xml.toString())
fun render(project: PProject, extraLibraries: List<PLibrary> = emptyList()): List<PFile> {
val files = mutableListOf<PFile>()
files += renderModulesFile(project)
project.modules.forEach { files += renderModule(project, it) }
(project.libraries + extraLibraries).forEach { files += renderLibrary(project, it) }
return files
}
private fun renderModulesFile(project: PProject) = PFile(
File(project.rootDirectory, ".idea/modules.xml"),
xml("project", "version" to 4) {
xml("component", "name" to "ProjectModuleManager") {
xml("modules") {
val pathContext = ProjectContext(project)
for (module in project.modules) {
val moduleFilePath = pathContext(module.moduleFile)
if (module.group != null) {
xml("module", "fileurl" to "file://$moduleFilePath", "filepath" to moduleFilePath, "group" to module.group)
} else {
xml("module", "fileurl" to "file://$moduleFilePath", "filepath" to moduleFilePath)
}
}
}
}
}
)
private fun renderModule(project: PProject, module: PModule) = PFile(
module.moduleFile,
xml("module",
"type" to "JAVA_MODULE",
"version" to 4
) {
val moduleForProductionSources = module.moduleForProductionSources
if (moduleForProductionSources != null) {
xml("component", "name" to "TestModuleProperties", "production-module" to moduleForProductionSources.name)
}
xml("component", "name" to "NewModuleRootManager", "inherit-compiler-output" to "true") {
xml("exclude-output")
val pathContext = ModuleContext(project, module)
for (contentRoot in module.contentRoots) {
xml("content", pathContext.url(contentRoot.path)) {
for (sourceRoot in contentRoot.sourceRoots) {
var args = arrayOf(pathContext.url(sourceRoot.path))
args += when (sourceRoot.kind) {
PSourceRoot.Kind.PRODUCTION -> ("isTestSource" to "false")
PSourceRoot.Kind.TEST -> ("isTestSource" to "true")
PSourceRoot.Kind.RESOURCES -> ("type" to "java-resource")
PSourceRoot.Kind.TEST_RESOURCES -> ("type" to "java-test-resource")
}
xml("sourceFolder", *args)
}
for (excludedDir in contentRoot.excludedDirectories) {
xml("excludeFolder", pathContext.url(excludedDir))
}
}
}
xml("orderEntry", "type" to "inheritedJdk")
for (orderRoot in module.orderRoots) {
val dependency = orderRoot.dependency
var args = when (dependency) {
is PDependency.ModuleLibrary -> arrayOf(
"type" to "module-library"
)
is PDependency.Module -> arrayOf(
"type" to "module",
"module-name" to dependency.name
)
is PDependency.Library -> arrayOf(
"type" to "library",
"name" to dependency.name,
"level" to "project"
)
else -> error("Unsupported dependency type: $dependency")
}
if (dependency is PDependency.Module && orderRoot.isProductionOnTestDependency) {
args += ("production-on-test" to "")
}
args += ("scope" to orderRoot.scope.toString())
if (orderRoot.isExported) {
args += ("exported" to "")
}
xml("orderEntry", *args) {
if (dependency is PDependency.ModuleLibrary) {
add(renderLibraryToXml(dependency.library, pathContext, named = false))
}
}
}
}
}
)
private fun renderLibrary(project: PProject, library: PLibrary): PFile {
val pathContext = ProjectContext(project)
// TODO find how IDEA escapes library names
val escapedName = library.renderName().replace(" ", "_").replace(".", "_").replace("-", "_")
return PFile(
File(project.rootDirectory, ".idea/libraries/$escapedName.xml"),
xml("component", "name" to "libraryTable") {
add(renderLibraryToXml(library, pathContext))
})
}
private fun renderLibraryToXml(library: PLibrary, pathContext: PathContext, named: Boolean = true): xml {
val args = if (named) arrayOf("name" to library.renderName()) else emptyArray()
return xml("library", *args) {
xml("CLASSES") {
library.classes.forEach { xml("root", pathContext.url(it)) }
}
xml("JAVADOC") {
library.javadoc.forEach { xml("root", pathContext.url(it)) }
}
xml("SOURCES") {
library.sources.forEach { xml("root", pathContext.url(it)) }
}
}
}
fun PLibrary.renderName() = name?.takeIf { it != "unspecified" } ?: classes.first().nameWithoutExtension
+50
View File
@@ -0,0 +1,50 @@
@file:Suppress("PackageDirectoryMismatch")
package org.jetbrains.kotlin.pill
import shadow.org.jdom2.Document
import shadow.org.jdom2.Element
import shadow.org.jdom2.output.Format
import shadow.org.jdom2.output.XMLOutputter
class xml(val name: String, vararg val args: Pair<String, Any>, block: xml.() -> Unit = {}) {
private companion object {
fun makeXml(name: String, vararg args: Pair<String, Any>, block: xml.() -> Unit = {}): xml {
return xml(name, *args, block = block)
}
}
private val children = mutableListOf<xml>()
init {
@Suppress("UNUSED_EXPRESSION")
block()
}
fun xml(name: String, vararg args: Pair<String, Any>, block: xml.() -> Unit = {}) {
children += makeXml(name, *args, block = block)
}
fun add(xml: xml) {
children += xml
}
private fun toElement(): Element {
val element = Element(name)
for (arg in args) {
element.setAttribute(arg.first, arg.second.toString())
}
for (child in children) {
element.addContent(child.toElement())
}
return element
}
override fun toString(): String {
val document = Document().also { it.rootElement = toElement() }
val output = XMLOutputter().also { it.format = Format.getPrettyFormat() }
return output.outputString(document)
}
}
@@ -0,0 +1 @@
implementation-class=org.jetbrains.kotlin.pill.JpsCompatibleRootPlugin
@@ -0,0 +1 @@
implementation-class=org.jetbrains.kotlin.pill.JpsCompatiblePlugin