diff --git a/plugins/pill/pill-importer/src/JpsCompatiblePluginTasks.kt b/plugins/pill/pill-importer/src/JpsCompatiblePluginTasks.kt index 4c17962eca8..57c6227c0e3 100644 --- a/plugins/pill/pill-importer/src/JpsCompatiblePluginTasks.kt +++ b/plugins/pill/pill-importer/src/JpsCompatiblePluginTasks.kt @@ -12,6 +12,10 @@ import org.gradle.api.tasks.SourceSet import org.gradle.kotlin.dsl.extra import org.jetbrains.kotlin.pill.artifact.ArtifactDependencyMapper import org.jetbrains.kotlin.pill.artifact.ArtifactGenerator +import org.jetbrains.kotlin.pill.model.PDependency +import org.jetbrains.kotlin.pill.model.PLibrary +import org.jetbrains.kotlin.pill.model.POrderRoot +import org.jetbrains.kotlin.pill.model.PProject import shadow.org.jdom2.input.SAXBuilder import shadow.org.jdom2.* import shadow.org.jdom2.output.Format @@ -89,12 +93,12 @@ class JpsCompatiblePluginTasks(private val rootProject: Project, private val pla rootProject.logger.lifecycle("Pill: Setting up project for the '${variant.name.toLowerCase()}' variant...") val modulePrefix = System.getProperty("pill.module.prefix", "") - val parserContext = ParserContext(variant, modulePrefix) + val modelParser = ModelParser(variant, modulePrefix) val dependencyPatcher = DependencyPatcher(rootProject) val dependencyMappers = listOf(dependencyPatcher, ::attachPlatformSources, ::attachAsmSources) - val jpsProject = parse(rootProject, parserContext) + val jpsProject = modelParser.parse(rootProject) .mapDependencies(dependencyMappers) .copy(libraries = dependencyPatcher.libraries) diff --git a/plugins/pill/pill-importer/src/ModelParser.kt b/plugins/pill/pill-importer/src/ModelParser.kt new file mode 100644 index 00000000000..ad4f8f9d5d4 --- /dev/null +++ b/plugins/pill/pill-importer/src/ModelParser.kt @@ -0,0 +1,334 @@ +/* + * Copyright 2010-2020 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.pill + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.tasks.* +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.JavaPluginConvention +import org.gradle.plugins.ide.idea.IdeaPlugin +import org.gradle.api.file.SourceDirectorySet +import org.gradle.api.internal.HasConvention +import org.gradle.api.internal.file.copy.CopySpecInternal +import org.gradle.api.internal.file.copy.SingleParentCopySpec +import org.gradle.jvm.tasks.Jar +import org.gradle.language.jvm.tasks.ProcessResources +import org.jetbrains.kotlin.pill.model.POrderRoot.* +import org.jetbrains.kotlin.pill.model.PSourceRoot.* +import org.jetbrains.kotlin.pill.PillExtensionMirror.* +import org.jetbrains.kotlin.pill.model.* +import java.io.File + +typealias OutputDir = String +typealias GradleProjectPath = String + +class ModelParser(private val variant: Variant, private val modulePrefix: String) { + fun parse(project: Project): PProject { + if (project != project.rootProject) { + error("$project is not a root project") + } + + fun Project.matchesSelectedVariant(): Boolean { + val extension = this.findPillExtensionMirror() ?: return true + val projectVariant = extension.variant ?: Variant.BASE + return projectVariant in variant.includes + } + + val (includedProjects, excludedProjects) = project.allprojects + .partition { it.plugins.hasPlugin("jps-compatible") && it.matchesSelectedVariant() } + + val modules = includedProjects.flatMap { parseModules(it, excludedProjects) } + val artifacts = parseArtifacts(project) + + return PProject("Kotlin", project.projectDir, modules, emptyList(), artifacts) + } + + private fun parseArtifacts(rootProject: Project): Map> { + val artifacts = HashMap>() + val additionalOutputs = HashMap>() + + for (project in rootProject.allprojects) { + val sourceSets = project.sourceSets?.toList() ?: emptyList() + + for (sourceSet in sourceSets) { + val path = makePath(project, sourceSet.name) + + for (output in sourceSet.output.toList()) { + artifacts[output.absolutePath] = listOf(path) + } + + val jarTask = project.tasks.findByName(sourceSet.jarTaskName) as? Jar ?: continue + val embeddedTask = findEmbeddableTask(project, sourceSet) + + for (task in listOfNotNull(jarTask, embeddedTask)) { + val archiveFile = task.archiveFile.get().asFile + artifacts[archiveFile.absolutePath] = listOf(path) + + val additionalOutputsForSourceSet = mutableListOf() + fun process(spec: CopySpecInternal) { + spec.children.forEach { process(it) } + if (spec is SingleParentCopySpec) { + for (sourcePath in spec.sourcePaths) { + if (sourcePath is SourceSetOutput) { + additionalOutputsForSourceSet += sourcePath.classesDirs + sourcePath.resourcesDir?.let { additionalOutputsForSourceSet += it } + } + } + } + } + process(task.rootSpec) + additionalOutputs[archiveFile.absolutePath] = additionalOutputsForSourceSet.map { it.absolutePath } + } + } + } + + for ((sourceSetOutputDir, additionalOutputsForSourceSet) in additionalOutputs) { + val projectPaths = artifacts[sourceSetOutputDir] ?: error("Unknown artifact $sourceSetOutputDir") + val newPaths = projectPaths + additionalOutputsForSourceSet.mapNotNull { artifacts[it] }.flatten() + artifacts[sourceSetOutputDir] = newPaths.distinct() + } + + return artifacts + } + + private fun findEmbeddableTask(project: Project, sourceSet: SourceSet): Jar? { + val jarName = sourceSet.jarTaskName + val embeddable = "embeddable" + val embeddedName = if (jarName == "jar") embeddable else jarName.dropLast("jar".length) + embeddable.capitalize() + return project.tasks.findByName(embeddedName) as? Jar + } + + private fun makePath(project: Project, sourceSetName: String): GradleProjectPath { + return project.path + "/" + sourceSetName + } + + private fun parseModules(project: Project, excludedProjects: List): List { + val modules = mutableListOf() + + fun getModuleFile(name: String): File { + val relativePath = File(project.projectDir, "$modulePrefix$name.iml").toRelativeString(project.rootProject.projectDir) + return File(project.rootProject.projectDir, ".idea/modules/$relativePath") + } + + val embeddedDependencies = project.configurations.findByName(EMBEDDED_CONFIGURATION_NAME) + ?.let { parseDependencies(it) } ?: emptyList() + + val sourceSets = parseSourceSets(project).sortedBy { it.forTests } + for (sourceSet in sourceSets) { + val sourceRoots = mutableListOf() + + for (dir in sourceSet.sourceDirectories) { + sourceRoots += PSourceRoot(dir, if (sourceSet.forTests) Kind.TEST else Kind.PRODUCTION) + } + + for (dir in sourceSet.resourceDirectories) { + sourceRoots += PSourceRoot(dir, if (sourceSet.forTests) Kind.TEST_RESOURCES else Kind.RESOURCES) + } + + if (sourceRoots.isEmpty()) { + continue + } + + val productionModule = if (sourceSet.forTests) modules.firstOrNull { !it.forTests } else null + + val contentRoots = sourceRoots.map { PContentRoot(it.directory, listOf(it), emptyList()) } + + var orderRoots = parseDependencies(project, sourceSet) + if (productionModule != null) { + val productionModuleDependency = PDependency.Module(productionModule.name) + orderRoots = listOf(POrderRoot(productionModuleDependency, Scope.COMPILE, true)) + orderRoots + } + + val name = project.pillModuleName + "." + sourceSet.name + + modules += PModule( + name = modulePrefix + name, + path = makePath(project, sourceSet.name), + forTests = sourceSet.forTests, + rootDirectory = sourceRoots.first().directory, + moduleFile = getModuleFile(name), + contentRoots = contentRoots, + orderRoots = orderRoots, + kotlinOptions = sourceSet.kotlinOptions, + moduleForProductionSources = productionModule, + embeddedDependencies = embeddedDependencies + ) + } + + val mainModuleFileRelativePath = when (project) { + project.rootProject -> File(project.rootProject.projectDir, modulePrefix + project.rootProject.name + ".iml") + else -> getModuleFile(project.pillModuleName) + } + + modules += PModule( + name = modulePrefix + project.pillModuleName, + path = project.path, + forTests = false, + rootDirectory = project.projectDir, + moduleFile = mainModuleFileRelativePath, + contentRoots = listOf(PContentRoot(project.projectDir, listOf(), getExcludedDirs(project, excludedProjects))), + orderRoots = emptyList(), + kotlinOptions = null, + moduleForProductionSources = null, + embeddedDependencies = emptyList() + ) + + return modules + } + + private fun getExcludedDirs(project: Project, excludedProjects: List): List { + fun getJavaExcludedDirs() = project.plugins.findPlugin(IdeaPlugin::class.java) + ?.model?.module?.excludeDirs?.toList() ?: emptyList() + + fun getPillExcludedDirs() = project.findPillExtensionMirror()?.excludedDirs ?: emptyList() + + return getPillExcludedDirs() + getJavaExcludedDirs() + project.buildDir + + (if (project == project.rootProject) excludedProjects.map { it.buildDir } else emptyList()) + } + + private fun parseSourceSets(project: Project): List { + if (!project.plugins.hasPlugin(JavaPlugin::class.java)) { + return emptyList() + } + + val kotlinTasksBySourceSet = project.tasks.names + .filter { it.startsWith("compile") && it.endsWith("Kotlin") } + .map { project.tasks.getByName(it) } + .associateBy { it.invokeInternal("getSourceSetName") } + + val gradleSourceSets = project.sourceSets?.toList() ?: emptyList() + val sourceSets = mutableListOf() + + for (sourceSet in gradleSourceSets) { + val kotlinCompileTask = kotlinTasksBySourceSet[sourceSet.name] + + fun Any.getKotlin(): SourceDirectorySet { + val kotlinMethod = javaClass.getMethod("getKotlin") + kotlinMethod.isAccessible = true + return kotlinMethod(this) as SourceDirectorySet + } + + val kotlinSourceDirectories = (sourceSet as HasConvention).convention + .plugins["kotlin"]?.getKotlin()?.srcDirs ?: emptySet() + + val sourceDirectories = (sourceSet.java.sourceDirectories.files + kotlinSourceDirectories).toList() + + val resourceDirectoriesFromSourceSet = sourceSet.resources.sourceDirectories.files + val resourceDirectoriesFromTask = parseResourceRootsProcessedByProcessResourcesTask(project, sourceSet) + + val resourceDirectories = (resourceDirectoriesFromSourceSet + resourceDirectoriesFromTask) + .distinct().filter { it !in sourceDirectories } + + sourceSets += PSourceSet( + name = sourceSet.name, + forTests = sourceSet.isTestSourceSet, + sourceDirectories = sourceDirectories, + resourceDirectories = resourceDirectories, + kotlinOptions = kotlinCompileTask?.let { getKotlinOptions(it) }, + compileClasspathConfigurationName = sourceSet.compileClasspathConfigurationName, + runtimeClasspathConfigurationName = sourceSet.runtimeClasspathConfigurationName + ) + } + + return sourceSets + } + + private fun parseResourceRootsProcessedByProcessResourcesTask(project: Project, sourceSet: SourceSet): List { + val isMainSourceSet = sourceSet.name == SourceSet.MAIN_SOURCE_SET_NAME + + val taskNameBase = "processResources" + val taskName = if (isMainSourceSet) taskNameBase else sourceSet.name + taskNameBase.capitalize() + val task = project.tasks.findByName(taskName) as? ProcessResources ?: return emptyList() + + val roots = mutableListOf() + fun collectRoots(spec: CopySpecInternal) { + if (spec is SingleParentCopySpec && spec.children.none()) { + roots += spec.sourcePaths.map { File(project.projectDir, it.toString()) }.filter { it.exists() } + return + } + + spec.children.forEach(::collectRoots) + } + collectRoots(task.rootSpec) + return roots + } + + private fun getKotlinOptions(kotlinCompileTask: Any): PSourceRootKotlinOptions? { + val compileArguments = run { + val method = kotlinCompileTask::class.java.getMethod("getSerializedCompilerArguments") + method.isAccessible = true + @Suppress("UNCHECKED_CAST") + method.invoke(kotlinCompileTask) as List + } + + fun parseBoolean(name: String) = compileArguments.contains("-$name") + fun parseString(name: String) = compileArguments.dropWhile { it != "-$name" }.drop(1).firstOrNull() + + fun isOptionForScriptingCompilerPlugin(option: String): Boolean { + return option.startsWith("-Xplugin=") && option.contains("kotlin-scripting-compiler-embeddable") + } + + val extraArguments = compileArguments.filter { + it.startsWith("-X") && !isOptionForScriptingCompilerPlugin(it) + } + + return PSourceRootKotlinOptions( + parseBoolean("no-stdlib"), + parseBoolean("no-reflect"), + parseString("module-name"), + parseString("api-version"), + parseString("language-version"), + parseString("jvm-target"), + extraArguments + ) + } + + private fun parseDependencies(project: Project, sourceSet: PSourceSet): List { + val roots = mutableListOf() + + fun process(name: String, scope: Scope) { + val configuration = project.configurations.findByName(name) ?: return + roots += parseDependencies(configuration).map { POrderRoot(it, scope) } + } + + process(sourceSet.compileClasspathConfigurationName, Scope.PROVIDED) + process(sourceSet.runtimeClasspathConfigurationName, Scope.RUNTIME) + + if (sourceSet.forTests) { + process("jpsTest", Scope.TEST) + } + + return roots + } + + private fun parseDependencies(configuration: Configuration): List { + return configuration.resolve().map { file -> + val library = PLibrary(file.name, listOf(file)) + return@map PDependency.ModuleLibrary(library) + } + } +} + +private val SourceSet.isTestSourceSet: Boolean + get() = name == SourceSet.TEST_SOURCE_SET_NAME + || name.endsWith("Test") + || name.endsWith("Tests") + +private fun Any.invokeInternal(name: String, instance: Any = this): Any? { + val method = javaClass.methods.single { it.name.startsWith(name) && it.parameterTypes.isEmpty() } + method.isAccessible = true + return method.invoke(instance) +} + +val Project.pillModuleName: String + get() = path.removePrefix(":").replace(':', '.') + +val Project.sourceSets: SourceSetContainer? + get() { + val convention = project.convention.findPlugin(JavaPluginConvention::class.java) ?: return null + return convention.sourceSets + } diff --git a/plugins/pill/pill-importer/src/PathContext.kt b/plugins/pill/pill-importer/src/PathContext.kt index 98b6adcb6f0..70b043fe675 100644 --- a/plugins/pill/pill-importer/src/PathContext.kt +++ b/plugins/pill/pill-importer/src/PathContext.kt @@ -6,6 +6,8 @@ package org.jetbrains.kotlin.pill import org.gradle.api.Project +import org.jetbrains.kotlin.pill.model.PModule +import org.jetbrains.kotlin.pill.model.PProject import java.io.File private val USER_HOME_DIR_PATH = System.getProperty("user.home").withSlash() diff --git a/plugins/pill/pill-importer/src/artifact/ArtifactDependencyMapper.kt b/plugins/pill/pill-importer/src/artifact/ArtifactDependencyMapper.kt index c71e72b5679..ed1a322aab5 100644 --- a/plugins/pill/pill-importer/src/artifact/ArtifactDependencyMapper.kt +++ b/plugins/pill/pill-importer/src/artifact/ArtifactDependencyMapper.kt @@ -5,7 +5,7 @@ package org.jetbrains.kotlin.pill.artifact -import org.jetbrains.kotlin.pill.PDependency +import org.jetbrains.kotlin.pill.model.PDependency interface ArtifactDependencyMapper { fun map(dependency: PDependency): List diff --git a/plugins/pill/pill-importer/src/artifact/ArtifactGenerator.kt b/plugins/pill/pill-importer/src/artifact/ArtifactGenerator.kt index a17b0530b63..bfb9e506762 100644 --- a/plugins/pill/pill-importer/src/artifact/ArtifactGenerator.kt +++ b/plugins/pill/pill-importer/src/artifact/ArtifactGenerator.kt @@ -9,6 +9,8 @@ import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.kotlin.dsl.extra import org.jetbrains.kotlin.pill.* +import org.jetbrains.kotlin.pill.model.PDependency +import org.jetbrains.kotlin.pill.model.PLibrary import java.io.File class ArtifactGenerator(private val dependencyMapper: ArtifactDependencyMapper) { diff --git a/plugins/pill/pill-importer/src/model/PLibrary.kt b/plugins/pill/pill-importer/src/model/PLibrary.kt new file mode 100644 index 00000000000..8b568558cbb --- /dev/null +++ b/plugins/pill/pill-importer/src/model/PLibrary.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2020 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.pill.model + +import java.io.File + +data class PLibrary( + val name: String, + val classes: List, + val javadoc: List = emptyList(), + val sources: List = emptyList(), + val annotations: List = emptyList(), + val dependencies: List = emptyList(), + val originalName: String = name +) { + fun attachSource(file: File): PLibrary { + return this.copy(sources = this.sources + listOf(file)) + } +} \ No newline at end of file diff --git a/plugins/pill/pill-importer/src/model/PModule.kt b/plugins/pill/pill-importer/src/model/PModule.kt new file mode 100644 index 00000000000..0b55f675439 --- /dev/null +++ b/plugins/pill/pill-importer/src/model/PModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2010-2020 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.pill.model + +import org.jetbrains.kotlin.pill.GradleProjectPath +import java.io.File + +data class PModule( + val name: String, + val path: GradleProjectPath, + val forTests: Boolean, + val rootDirectory: File, + val moduleFile: File, + val contentRoots: List, + val orderRoots: List, + val kotlinOptions: PSourceRootKotlinOptions?, + val moduleForProductionSources: PModule? = null, + val embeddedDependencies: List +) + +data class PContentRoot( + val path: File, + val sourceRoots: List, + val excludedDirectories: List +) + +data class PSourceRoot(val directory: File, val kind: Kind) { + enum class Kind { PRODUCTION, TEST, RESOURCES, TEST_RESOURCES } +} + +data class PSourceRootKotlinOptions( + val noStdlib: Boolean?, + val noReflect: Boolean?, + val moduleName: String?, + val apiVersion: String?, + val languageVersion: String?, + val jvmTarget: String?, + val extraArguments: List +) \ No newline at end of file diff --git a/plugins/pill/pill-importer/src/model/POrderRoot.kt b/plugins/pill/pill-importer/src/model/POrderRoot.kt new file mode 100644 index 00000000000..a8cafae4195 --- /dev/null +++ b/plugins/pill/pill-importer/src/model/POrderRoot.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2020 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.pill.model + +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() +} \ No newline at end of file diff --git a/plugins/pill/pill-importer/src/model/PProject.kt b/plugins/pill/pill-importer/src/model/PProject.kt new file mode 100644 index 00000000000..7da17678301 --- /dev/null +++ b/plugins/pill/pill-importer/src/model/PProject.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2010-2020 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.pill.model + +import org.jetbrains.kotlin.pill.GradleProjectPath +import org.jetbrains.kotlin.pill.OutputDir +import java.io.File + +data class PProject( + val name: String, + val rootDirectory: File, + val modules: List, + val libraries: List, + val artifacts: Map> +) \ No newline at end of file diff --git a/plugins/pill/pill-importer/src/model/PSourceSet.kt b/plugins/pill/pill-importer/src/model/PSourceSet.kt new file mode 100644 index 00000000000..81810c3a299 --- /dev/null +++ b/plugins/pill/pill-importer/src/model/PSourceSet.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2010-2020 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.pill.model + +import java.io.File + +data class PSourceSet( + val name: String, + val forTests: Boolean, + val sourceDirectories: List, + val resourceDirectories: List, + val kotlinOptions: PSourceRootKotlinOptions?, + val compileClasspathConfigurationName: String, + val runtimeClasspathConfigurationName: String +) \ No newline at end of file diff --git a/plugins/pill/pill-importer/src/parser.kt b/plugins/pill/pill-importer/src/parser.kt deleted file mode 100644 index 1aa08a1475e..00000000000 --- a/plugins/pill/pill-importer/src/parser.kt +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright 2010-2020 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.pill - -import org.gradle.api.Project -import org.gradle.api.artifacts.Configuration -import org.gradle.api.tasks.* -import org.gradle.api.plugins.JavaPlugin -import org.gradle.api.plugins.JavaPluginConvention -import org.gradle.plugins.ide.idea.IdeaPlugin -import org.gradle.api.file.SourceDirectorySet -import org.gradle.api.internal.HasConvention -import org.gradle.api.internal.file.copy.CopySpecInternal -import org.gradle.api.internal.file.copy.SingleParentCopySpec -import org.gradle.jvm.tasks.Jar -import org.gradle.language.jvm.tasks.ProcessResources -import org.jetbrains.kotlin.pill.POrderRoot.* -import org.jetbrains.kotlin.pill.PSourceRoot.* -import org.jetbrains.kotlin.pill.PillExtensionMirror.* -import java.io.File - -class ParserContext(val variant: Variant, val modulePrefix: String) - -typealias OutputDir = String -typealias GradleProjectPath = String - -data class PProject( - val name: String, - val rootDirectory: File, - val modules: List, - val libraries: List, - val artifacts: Map> -) - -data class PModule( - val name: String, - val path: GradleProjectPath, - val forTests: Boolean, - val rootDirectory: File, - val moduleFile: File, - val contentRoots: List, - val orderRoots: List, - val kotlinOptions: PSourceRootKotlinOptions?, - val moduleForProductionSources: PModule? = null, - val embeddedDependencies: List -) - -data class PContentRoot( - val path: File, - val sourceRoots: List, - val excludedDirectories: List -) - -data class PSourceSet( - val name: String, - val forTests: Boolean, - val sourceDirectories: List, - val resourceDirectories: List, - val kotlinOptions: PSourceRootKotlinOptions?, - val compileClasspathConfigurationName: String, - val runtimeClasspathConfigurationName: String -) - -data class PSourceRoot(val directory: File, val kind: Kind) { - enum class Kind { PRODUCTION, TEST, RESOURCES, TEST_RESOURCES } -} - -data class PSourceRootKotlinOptions( - val noStdlib: Boolean?, - val noReflect: Boolean?, - val moduleName: String?, - val apiVersion: String?, - val languageVersion: String?, - val jvmTarget: String?, - val extraArguments: List -) - -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, - val javadoc: List = emptyList(), - val sources: List = emptyList(), - val annotations: List = emptyList(), - val dependencies: List = emptyList(), - val originalName: String = name -) { - fun attachSource(file: File): PLibrary { - return this.copy(sources = this.sources + listOf(file)) - } -} - -fun parse(project: Project, context: ParserContext): PProject = with(context) { - if (project != project.rootProject) { - error("$project is not a root project") - } - - fun Project.matchesSelectedVariant(): Boolean { - val extension = this.findPillExtensionMirror() ?: return true - val projectVariant = extension.variant ?: Variant.BASE - return projectVariant in context.variant.includes - } - - val (includedProjects, excludedProjects) = project.allprojects - .partition { it.plugins.hasPlugin("jps-compatible") && it.matchesSelectedVariant() } - - val modules = includedProjects.flatMap { parseModules(it, excludedProjects) } - val artifacts = parseArtifacts(project) - - return PProject("Kotlin", project.projectDir, modules, emptyList(), artifacts) -} - -private fun parseArtifacts(rootProject: Project): Map> { - val artifacts = HashMap>() - val additionalOutputs = HashMap>() - - for (project in rootProject.allprojects) { - val sourceSets = project.sourceSets?.toList() ?: emptyList() - - for (sourceSet in sourceSets) { - val path = makePath(project, sourceSet.name) - - for (output in sourceSet.output.toList()) { - artifacts[output.absolutePath] = listOf(path) - } - - val jarTask = project.tasks.findByName(sourceSet.jarTaskName) as? Jar ?: continue - val embeddedTask = findEmbeddableTask(project, sourceSet) - - for (task in listOfNotNull(jarTask, embeddedTask)) { - val archiveFile = task.archiveFile.get().asFile - artifacts[archiveFile.absolutePath] = listOf(path) - - val additionalOutputsForSourceSet = mutableListOf() - fun process(spec: CopySpecInternal) { - spec.children.forEach { process(it) } - if (spec is SingleParentCopySpec) { - for (sourcePath in spec.sourcePaths) { - if (sourcePath is SourceSetOutput) { - additionalOutputsForSourceSet += sourcePath.classesDirs - sourcePath.resourcesDir?.let { additionalOutputsForSourceSet += it } - } - } - } - } - process(task.rootSpec) - additionalOutputs[archiveFile.absolutePath] = additionalOutputsForSourceSet.map { it.absolutePath } - } - } - } - - for ((sourceSetOutputDir, additionalOutputsForSourceSet) in additionalOutputs) { - val projectPaths = artifacts[sourceSetOutputDir] ?: error("Unknown artifact $sourceSetOutputDir") - val newPaths = projectPaths + additionalOutputsForSourceSet.mapNotNull { artifacts[it] }.flatten() - artifacts[sourceSetOutputDir] = newPaths.distinct() - } - - return artifacts -} - -private fun findEmbeddableTask(project: Project, sourceSet: SourceSet): Jar? { - val jarName = sourceSet.jarTaskName - val embeddable = "embeddable" - val embeddedName = if (jarName == "jar") embeddable else jarName.dropLast("jar".length) + embeddable.capitalize() - return project.tasks.findByName(embeddedName) as? Jar -} - -private fun makePath(project: Project, sourceSetName: String): GradleProjectPath { - return project.path + "/" + sourceSetName -} - -private fun ParserContext.parseModules(project: Project, excludedProjects: List): List { - val modules = mutableListOf() - - fun getModuleFile(name: String): File { - val relativePath = File(project.projectDir, "$modulePrefix$name.iml").toRelativeString(project.rootProject.projectDir) - return File(project.rootProject.projectDir, ".idea/modules/$relativePath") - } - - val embeddedDependencies = project.configurations.findByName(EMBEDDED_CONFIGURATION_NAME) - ?.let { parseDependencies(it) } ?: emptyList() - - val sourceSets = parseSourceSets(project).sortedBy { it.forTests } - for (sourceSet in sourceSets) { - val sourceRoots = mutableListOf() - - for (dir in sourceSet.sourceDirectories) { - sourceRoots += PSourceRoot(dir, if (sourceSet.forTests) Kind.TEST else Kind.PRODUCTION) - } - - for (dir in sourceSet.resourceDirectories) { - sourceRoots += PSourceRoot(dir, if (sourceSet.forTests) Kind.TEST_RESOURCES else Kind.RESOURCES) - } - - if (sourceRoots.isEmpty()) { - continue - } - - val productionModule = if (sourceSet.forTests) modules.firstOrNull { !it.forTests } else null - - val contentRoots = sourceRoots.map { PContentRoot(it.directory, listOf(it), emptyList()) } - - var orderRoots = parseDependencies(project, sourceSet) - if (productionModule != null) { - val productionModuleDependency = PDependency.Module(productionModule.name) - orderRoots = listOf(POrderRoot(productionModuleDependency, Scope.COMPILE, true)) + orderRoots - } - - val name = project.pillModuleName + "." + sourceSet.name - - modules += PModule( - name = modulePrefix + name, - path = makePath(project, sourceSet.name), - forTests = sourceSet.forTests, - rootDirectory = sourceRoots.first().directory, - moduleFile = getModuleFile(name), - contentRoots = contentRoots, - orderRoots = orderRoots, - kotlinOptions = sourceSet.kotlinOptions, - moduleForProductionSources = productionModule, - embeddedDependencies = embeddedDependencies - ) - } - - val mainModuleFileRelativePath = when (project) { - project.rootProject -> File(project.rootProject.projectDir, modulePrefix + project.rootProject.name + ".iml") - else -> getModuleFile(project.pillModuleName) - } - - modules += PModule( - name = modulePrefix + project.pillModuleName, - path = project.path, - forTests = false, - rootDirectory = project.projectDir, - moduleFile = mainModuleFileRelativePath, - contentRoots = listOf(PContentRoot(project.projectDir, listOf(), getExcludedDirs(project, excludedProjects))), - orderRoots = emptyList(), - kotlinOptions = null, - moduleForProductionSources = null, - embeddedDependencies = emptyList() - ) - - return modules -} - -private fun getExcludedDirs(project: Project, excludedProjects: List): List { - fun getJavaExcludedDirs() = project.plugins.findPlugin(IdeaPlugin::class.java) - ?.model?.module?.excludeDirs?.toList() ?: emptyList() - - fun getPillExcludedDirs() = project.findPillExtensionMirror()?.excludedDirs ?: emptyList() - - return getPillExcludedDirs() + getJavaExcludedDirs() + project.buildDir + - (if (project == project.rootProject) excludedProjects.map { it.buildDir } else emptyList()) -} - -private fun parseSourceSets(project: Project): List { - if (!project.plugins.hasPlugin(JavaPlugin::class.java)) { - return emptyList() - } - - val kotlinTasksBySourceSet = project.tasks.names - .filter { it.startsWith("compile") && it.endsWith("Kotlin") } - .map { project.tasks.getByName(it) } - .associateBy { it.invokeInternal("getSourceSetName") } - - val gradleSourceSets = project.sourceSets?.toList() ?: emptyList() - val sourceSets = mutableListOf() - - for (sourceSet in gradleSourceSets) { - val kotlinCompileTask = kotlinTasksBySourceSet[sourceSet.name] - - fun Any.getKotlin(): SourceDirectorySet { - val kotlinMethod = javaClass.getMethod("getKotlin") - kotlinMethod.isAccessible = true - return kotlinMethod(this) as SourceDirectorySet - } - - val kotlinSourceDirectories = (sourceSet as HasConvention).convention - .plugins["kotlin"]?.getKotlin()?.srcDirs ?: emptySet() - - val sourceDirectories = (sourceSet.java.sourceDirectories.files + kotlinSourceDirectories).toList() - - val resourceDirectoriesFromSourceSet = sourceSet.resources.sourceDirectories.files - val resourceDirectoriesFromTask = parseResourceRootsProcessedByProcessResourcesTask(project, sourceSet) - - val resourceDirectories = (resourceDirectoriesFromSourceSet + resourceDirectoriesFromTask) - .distinct().filter { it !in sourceDirectories } - - sourceSets += PSourceSet( - name = sourceSet.name, - forTests = sourceSet.isTestSourceSet, - sourceDirectories = sourceDirectories, - resourceDirectories = resourceDirectories, - kotlinOptions = kotlinCompileTask?.let { getKotlinOptions(it) }, - compileClasspathConfigurationName = sourceSet.compileClasspathConfigurationName, - runtimeClasspathConfigurationName = sourceSet.runtimeClasspathConfigurationName - ) - } - - return sourceSets -} - -private fun parseResourceRootsProcessedByProcessResourcesTask(project: Project, sourceSet: SourceSet): List { - val isMainSourceSet = sourceSet.name == SourceSet.MAIN_SOURCE_SET_NAME - - val taskNameBase = "processResources" - val taskName = if (isMainSourceSet) taskNameBase else sourceSet.name + taskNameBase.capitalize() - val task = project.tasks.findByName(taskName) as? ProcessResources ?: return emptyList() - - val roots = mutableListOf() - fun collectRoots(spec: CopySpecInternal) { - if (spec is SingleParentCopySpec && spec.children.none()) { - roots += spec.sourcePaths.map { File(project.projectDir, it.toString()) }.filter { it.exists() } - return - } - - spec.children.forEach(::collectRoots) - } - collectRoots(task.rootSpec) - return roots -} - -private val SourceSet.isTestSourceSet: Boolean - get() = name == SourceSet.TEST_SOURCE_SET_NAME - || name.endsWith("Test") - || name.endsWith("Tests") - -private fun getKotlinOptions(kotlinCompileTask: Any): PSourceRootKotlinOptions? { - val compileArguments = run { - val method = kotlinCompileTask::class.java.getMethod("getSerializedCompilerArguments") - method.isAccessible = true - @Suppress("UNCHECKED_CAST") - method.invoke(kotlinCompileTask) as List - } - - fun parseBoolean(name: String) = compileArguments.contains("-$name") - fun parseString(name: String) = compileArguments.dropWhile { it != "-$name" }.drop(1).firstOrNull() - - fun isOptionForScriptingCompilerPlugin(option: String): Boolean { - return option.startsWith("-Xplugin=") && option.contains("kotlin-scripting-compiler-embeddable") - } - - val extraArguments = compileArguments.filter { - it.startsWith("-X") && !isOptionForScriptingCompilerPlugin(it) - } - - return PSourceRootKotlinOptions( - parseBoolean("no-stdlib"), - parseBoolean("no-reflect"), - parseString("module-name"), - parseString("api-version"), - parseString("language-version"), - parseString("jvm-target"), - extraArguments - ) -} - -private fun Any.invokeInternal(name: String, instance: Any = this): Any? { - val method = javaClass.methods.single { it.name.startsWith(name) && it.parameterTypes.isEmpty() } - method.isAccessible = true - return method.invoke(instance) -} - -private fun parseDependencies(project: Project, sourceSet: PSourceSet): List { - val roots = mutableListOf() - - fun process(name: String, scope: Scope) { - val configuration = project.configurations.findByName(name) ?: return - roots += parseDependencies(configuration).map { POrderRoot(it, scope) } - } - - process(sourceSet.compileClasspathConfigurationName, Scope.PROVIDED) - process(sourceSet.runtimeClasspathConfigurationName, Scope.RUNTIME) - - if (sourceSet.forTests) { - process("jpsTest", Scope.TEST) - } - - return roots -} - -private fun parseDependencies(configuration: Configuration): List { - return configuration.resolve().map { file -> - val library = PLibrary(file.name, listOf(file)) - return@map PDependency.ModuleLibrary(library) - } -} - -val Project.pillModuleName: String - get() = path.removePrefix(":").replace(':', '.') - -val Project.sourceSets: SourceSetContainer? - get() { - val convention = project.convention.findPlugin(JavaPluginConvention::class.java) ?: return null - return convention.sourceSets - } diff --git a/plugins/pill/pill-importer/src/render.kt b/plugins/pill/pill-importer/src/render.kt index b29e3ea6ddf..b9630f117cd 100644 --- a/plugins/pill/pill-importer/src/render.kt +++ b/plugins/pill/pill-importer/src/render.kt @@ -5,6 +5,7 @@ package org.jetbrains.kotlin.pill +import org.jetbrains.kotlin.pill.model.* import java.io.File class PFile(val path: File, private val text: String) {