blob: 82506ace0f9f61bb5d5a93969da9dea8f402085f [file]
/*
* 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.
*/
@file:Suppress("DEPRECATION") // Suppress deprecation warning of HasConvention
package org.jetbrains.kotlin.pill
import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectContainer
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.JavaPluginExtension
import org.gradle.plugins.ide.idea.IdeaPlugin
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.internal.file.copy.CopySpecInternal
import org.gradle.api.internal.file.copy.SingleParentCopySpec
import org.gradle.api.provider.Property
import org.gradle.jvm.tasks.Jar
import org.gradle.jvm.toolchain.JavaToolchainService
import org.gradle.kotlin.dsl.extra
import org.gradle.kotlin.dsl.findByType
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.model.*
import java.io.File
import java.util.*
typealias OutputDir = String
typealias GradleProjectPath = String
class ModelParser(private val modulePrefix: String, private val globalExcludedDirectories: List<File>) {
fun parse(project: Project): PProject {
if (project != project.rootProject) {
error("$project is not a root project")
}
val (includedProjects, excludedProjects) = project.allprojects
.partition { it == project || it.plugins.hasPlugin("jps-compatible") }
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<String, List<GradleProjectPath>> {
val artifacts = HashMap<OutputDir, List<GradleProjectPath>>()
val additionalOutputs = HashMap<OutputDir, List<OutputDir>>()
for (project in rootProject.allprojects) {
for (sourceSet in computeAllSourceSets(project)) {
val path = makePath(project, sourceSet.name)
if (sourceSet is SourceSetWrapper.JavaSourceSet) {
for (output in sourceSet.gradleSourceSet.output.toList()) {
artifacts[output.absolutePath] = listOf(path)
}
}
val jarTask = sourceSet.findJarTask(project)
val embeddedTask = sourceSet.findEmbeddedTask(project)
for (task in listOfNotNull(jarTask, embeddedTask)) {
val archiveFile = task.archiveFile.get().asFile
artifacts[archiveFile.absolutePath] = listOf(path)
val additionalOutputsForSourceSet = mutableListOf<File>()
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 makePath(project: Project, sourceSetName: String): GradleProjectPath {
return project.path + "/" + sourceSetName
}
private fun parseModules(project: Project, excludedProjects: List<Project>): List<PModule> {
val modules = mutableListOf<PModule>()
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 javaLanguageVersion = getJavaLanguageVersion(project)
val sourceSets = parseSourceSets(project).sortedBy { it.forTests }
for (sourceSet in sourceSets) {
val sourceRoots = mutableListOf<PSourceRoot>()
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)
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,
javaLanguageVersion = javaLanguageVersion,
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(), computeAllExcludedDirectories(project, excludedProjects))),
orderRoots = emptyList(),
javaLanguageVersion = null,
kotlinOptions = null,
moduleForProductionSources = null,
embeddedDependencies = emptyList()
)
return modules
}
private fun getJavaLanguageVersion(project: Project): Int? {
val javaPluginExtension = project.extensions.findByType<JavaPluginExtension>() ?: return null
val javaToolchainService = project.extensions.findByType<JavaToolchainService>() ?: return null
return javaToolchainService.launcherFor(javaPluginExtension.toolchain).orNull?.metadata?.languageVersion?.asInt()
}
private fun computeAllExcludedDirectories(project: Project, excludedProjects: List<Project>): List<File> {
val javaExcludedDirectories = project.plugins.findPlugin(IdeaPlugin::class.java)
?.model?.module?.excludeDirs?.toList().orEmpty()
val excludedProjectDirectories = if (project == project.rootProject) excludedProjects.map { it.buildDir } else emptyList()
return globalExcludedDirectories + javaExcludedDirectories + project.buildDir + excludedProjectDirectories
}
private fun parseSourceSets(project: Project): List<PSourceSet> {
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") as Property<*>).get() as String }
val gradleSourceSets = project.sourceSets?.toList() ?: emptyList()
val sourceSets = mutableListOf<PSourceSet>()
for (sourceSet in gradleSourceSets) {
val kotlinCompileTask = kotlinTasksBySourceSet[sourceSet.name]
val kotlinSourceDirectories = (sourceSet.extensions.findByName("kotlin") as? SourceDirectorySet)?.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 = SourceSetWrapper.JavaSourceSet(sourceSet).isTest,
sourceDirectories = sourceDirectories,
resourceDirectories = resourceDirectories,
kotlinOptions = kotlinCompileTask?.let { getKotlinOptions(it, project) },
compileClasspathConfigurationName = sourceSet.compileClasspathConfigurationName,
runtimeClasspathConfigurationName = sourceSet.runtimeClasspathConfigurationName
)
}
return sourceSets
}
private fun parseResourceRootsProcessedByProcessResourcesTask(project: Project, sourceSet: SourceSet): List<File> {
val isMainSourceSet = sourceSet.name == SourceSet.MAIN_SOURCE_SET_NAME
val taskNameBase = "processResources"
val taskName = if (isMainSourceSet) taskNameBase else sourceSet.name + taskNameBase.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
val task = project.tasks.findByName(taskName) as? ProcessResources ?: return emptyList()
val roots = mutableListOf<File>()
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, project: Project): PSourceRootKotlinOptions {
val compileArguments = run {
val method = kotlinCompileTask::class.java.getMethod("getSerializedCompilerArguments")
method.isAccessible = true
@Suppress("UNCHECKED_CAST")
method.invoke(kotlinCompileTask) as List<String>
}
val extraArguments = getExtraArguments(compileArguments)
val pluginClasspath = mutableListOf<String>()
if (project.plugins.hasPlugin("org.jetbrains.kotlin.plugin.serialization")) {
pluginClasspath += "\$KOTLIN_BUNDLED\$/lib/kotlinx-serialization-compiler-plugin.jar"
}
fun parseBoolean(name: String) = compileArguments.contains("-$name")
fun parseString(name: String) = compileArguments.dropWhile { it != "-$name" }.drop(1).firstOrNull()
return PSourceRootKotlinOptions(
parseBoolean("no-stdlib"),
parseBoolean("no-reflect"),
parseString("module-name"),
parseString("api-version"),
parseString("language-version"),
parseString("jvm-target").takeUnless { it == "1.6" },
extraArguments,
pluginClasspath
)
}
private fun parseDependencies(project: Project, sourceSet: PSourceSet): List<POrderRoot> {
val roots = mutableListOf<POrderRoot>()
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<PDependency> {
return configuration.resolve().map { file ->
val library = PLibrary(file.name, listOf(file))
return@map PDependency.ModuleLibrary(library)
}
}
private companion object {
private val SKIPPED_STANDALONE_ARGUMENTS =
listOf("-no-stdlib", "-no-reflect", "-Xfriend-paths=", "-Xbuild-file=", "-Xplugin=")
private val SKIPPED_ARGUMENTS_WITH_VALUE =
listOf("-classpath", "-d", "-module-name", "-api-version", "-language-version", "-jvm-target", "-P")
private fun getExtraArguments(args: List<String>): List<String> {
val result = mutableListOf<String>()
var index = 0
while (index < args.size) {
val arg = args[index]
if (SKIPPED_STANDALONE_ARGUMENTS.any { arg.startsWith(it) }) {
index += 1
continue
} else if (SKIPPED_ARGUMENTS_WITH_VALUE.any { arg.startsWith(it) }) {
index += 2 // Skip also the value
continue
}
result += arg
index += 1
}
return result
}
}
}
/**
* Returns both Java and Kotlin source sets, including multiplatform ones.
*/
internal fun computeAllSourceSets(project: Project): List<SourceSetWrapper> {
return buildList {
val existingNames = HashSet<String>()
for (javaSourceSet in project.sourceSets.orEmpty()) {
if (existingNames.add(javaSourceSet.name)) {
add(SourceSetWrapper.JavaSourceSet(javaSourceSet))
}
}
val kotlinExtension = project.extensions.findByName("kotlin")
if (kotlinExtension != null) {
// Calls 'KotlinSourceSetContainer.getSourceSets()'
val kotlinSourceSets = kotlinExtension.javaClass.getMethod("getSourceSets").invoke(kotlinExtension)
if (kotlinSourceSets is NamedDomainObjectContainer<*>) {
for (kotlinSourceSet in kotlinSourceSets) {
val name = (kotlinSourceSet as Named).name
if (existingNames.add(name)) {
add(SourceSetWrapper.KotlinSourceSet(kotlinSourceSet))
}
}
}
}
}
}
sealed class SourceSetWrapper(val name: String) {
class KotlinSourceSet(private val sourceSet: Named) : SourceSetWrapper(sourceSet.name) {
override val jarTaskCandidates: List<String>
get() {
// Production source sets (such as 'jvmMain') have a task without the 'Main' suffix, e.g. 'jvmJar'
return listOf("${baseName}Jar")
}
override fun findEmbeddedTask(project: Project): Jar? {
return null
}
}
class JavaSourceSet(val gradleSourceSet: SourceSet) : SourceSetWrapper(gradleSourceSet.name) {
override val jarTaskCandidates: List<String>
get() = buildList {
add(gradleSourceSet.jarTaskName)
when (gradleSourceSet.name) {
SourceSet.MAIN_SOURCE_SET_NAME -> add("jvmJar")
SourceSet.TEST_SOURCE_SET_NAME -> add("jvmJarForTests")
}
}
override val isTest: Boolean
get() {
if (gradleSourceSet.extra.has("jpsKind") && gradleSourceSet.extra.get("jpsKind") == SourceSet.TEST_SOURCE_SET_NAME) {
return true
}
return super.isTest
}
override fun findEmbeddedTask(project: Project): Jar? {
val jarName = gradleSourceSet.jarTaskName
val embeddable = "embeddable"
val embeddedName = if (jarName == "jar") embeddable else jarName.dropLast("jar".length) +
embeddable.replaceFirstChar { it.uppercase() }
return project.tasks.findByName(embeddedName) as? Jar
}
}
val baseName: String
get() = name.removeSuffix("Main")
open val isTest: Boolean
get() {
return name == SourceSet.TEST_SOURCE_SET_NAME
|| name == "testFixtures"
|| name.endsWith("Test")
|| name.endsWith("Tests")
}
protected abstract val jarTaskCandidates: List<String>
fun findJarTask(project: Project): Jar? {
return jarTaskCandidates.firstNotNullOfOrNull { project.tasks.findByName(it) } as? Jar
}
abstract fun findEmbeddedTask(project: Project): Jar?
override fun toString(): String {
return name
}
}
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(':', '.')
private val Project.sourceSets: SourceSetContainer?
get() {
val javaExtension = project.extensions.findByType<JavaPluginExtension>() ?: return null
return javaExtension.sourceSets
}