Introduce new API for Kotlin compiler support Gradle plugins

The old API was too specific for the Kotlin internal use cases and
leaked some abstractions due to the missing target-compilation project
model. One of such leaking abstractions was the Gradle tasks being
exposed to the subplugin, which required the tasks to instantiate
in order for the subplugin to apply. This violated task configuration
avoidance, so the old API could never work in compliance with TCA.

Introduce the new KotlinCompilerPluginSupportPlugin API that uses the
compilations instead of the tasks and is TCA-compliant.
This commit is contained in:
Sergey Igushkin
2020-06-06 00:20:21 +03:00
parent 0b2d96c1ef
commit 96ed30a449
4 changed files with 194 additions and 127 deletions
@@ -16,7 +16,10 @@
package org.jetbrains.kotlin.gradle.plugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.compile.AbstractCompile
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions
import java.io.File
@@ -62,6 +65,12 @@ enum class FilesOptionKind {
/** Defines a subplugin option that should be excluded from Gradle input/output checks */
open class InternalSubpluginOption(key: String, value: String) : SubpluginOption(key, value)
// Deprecated because most calls require the tasks to be instantiated, which is not compatible with Gradle task configuration avoidance.
@Deprecated(
message = "This interface will be removed due to performance considerations. " +
"Please use the KotlinCompilerPluginSupportPlugin interface instead.",
replaceWith = ReplaceWith("KotlinCompilerPluginSupportPlugin")
)
interface KotlinGradleSubplugin<in KotlinCompile : AbstractCompile> {
fun isApplicable(project: Project, task: AbstractCompile): Boolean
@@ -75,8 +84,8 @@ interface KotlinGradleSubplugin<in KotlinCompile : AbstractCompile> {
): List<SubpluginOption>
fun getSubpluginKotlinTasks(
project: Project,
kotlinCompile: KotlinCompile
project: Project,
kotlinCompile: KotlinCompile
): List<AbstractCompile> = emptyList()
fun getCompilerPluginId(): String
@@ -85,6 +94,37 @@ interface KotlinGradleSubplugin<in KotlinCompile : AbstractCompile> {
fun getNativeCompilerPluginArtifact(): SubpluginArtifact? = null
}
/**
* Gradle plugin implementing support for a Kotlin compiler plugin.
*
* In order to be discovered, it should be applied to the project as an ordinary Gradle [Plugin] before the
* Kotlin plugin inspects the project model in an afterEvaluate handler.
*
* The default implementation of [apply]
* doesn't do anything, but it can be overridden.
*
* Then its [isApplicable] is checked against compilations of the project, and if it returns true,
* then [applyToCompilation] may be called later.
*/
interface KotlinCompilerPluginSupportPlugin : Plugin<Project> {
override fun apply(target: Project) = Unit
fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean
/**
* Configures the compiler plugin to be incorporated into a specific [kotlinCompilation].
* This function is only called on [kotlinCompilation]s approved by [isApplicable].
* The [Provider] returned from this function may never get queried if the compilation is avoided in the current build.
*/
fun applyToCompilation(
kotlinCompilation: KotlinCompilation<*>
): Provider<List<SubpluginOption>>
fun getCompilerPluginId(): String
fun getPluginArtifact(): SubpluginArtifact
fun getNativeCompilerPluginArtifact(): SubpluginArtifact? = null
}
open class SubpluginArtifact(val groupId: String, val artifactId: String, val version: String? = null)
class JetBrainsSubpluginArtifact(artifactId: String) : SubpluginArtifact(groupId = "org.jetbrains.kotlin", artifactId = artifactId)
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.plugin
import com.android.build.gradle.*
import com.android.build.gradle.api.AndroidSourceSet
import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.SourceKind
import com.android.builder.model.SourceProvider
import org.gradle.api.*
import org.gradle.api.artifacts.ExternalDependency
@@ -29,7 +30,6 @@ import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.Upload
import org.gradle.api.tasks.compile.AbstractCompile
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.tasks.Jar
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry
import org.jetbrains.kotlin.gradle.dsl.*
@@ -207,20 +207,9 @@ internal class Kotlin2JvmSourceSetProcessor(
ScriptingGradleSubplugin.configureForSourceSet(project, kotlinCompilation.compilationName)
// TODO: here, the tasks are always triggered for configuration; once the subplugins are able to work without task instances,
// ensure that task configuration is properly avoided here;
project.whenEvaluated {
val kotlinTaskInstance = kotlinTask.get()
val javaTask = javaSourceSet?.let { project.tasks.findByName(it.compileJavaTaskName) as JavaCompile }
val subpluginEnvironment = SubpluginEnvironment.loadSubplugins(project, kotlinPluginVersion)
val appliedPlugins = subpluginEnvironment.addSubpluginOptions(
project, kotlinTaskInstance, javaTask, null, null, kotlinCompilation
)
appliedPlugins
.flatMap { it.getSubpluginKotlinTasks(project, kotlinTaskInstance) }
.forEach { plugin -> kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> plugin.source(sourceSet.kotlin) } }
subpluginEnvironment.addSubpluginOptions(project, kotlinCompilation)
javaSourceSet?.let { java ->
val javaTask = project.tasks.withType<AbstractCompile>().named(java.compileJavaTaskName)
@@ -274,29 +263,22 @@ internal class Kotlin2JsSourceSetProcessor(
}
// outputFile can be set later during the configuration phase, get it only after the phase:
project.runOnceAfterEvaluated("Kotlin2JsSourceSetProcessor.doTargetSpecificProcessing", kotlinTask) {
val kotlinTaskInstance = kotlinTask.get()
kotlinTaskInstance.kotlinOptions.outputFile = kotlinTaskInstance.outputFile.absolutePath
val outputDir = kotlinTaskInstance.outputFile.parentFile
project.whenEvaluated {
kotlinTask.configure { kotlinTaskInstance ->
kotlinTaskInstance.kotlinOptions.outputFile = kotlinTaskInstance.outputFile.absolutePath
val outputDir = kotlinTaskInstance.outputFile.parentFile
if (outputDir.isParentOf(project.rootDir))
throw InvalidUserDataException(
"The output directory '$outputDir' (defined by outputFile of $kotlinTaskInstance) contains or " +
"matches the project root directory '${project.rootDir}'.\n" +
"Gradle will not be able to build the project because of the root directory lock.\n" +
"To fix this, consider using the default outputFile location instead of providing it explicitly."
)
kotlinTaskInstance.destinationDir = outputDir
}
val subpluginEnvironment: SubpluginEnvironment = SubpluginEnvironment.loadSubplugins(project, kotlinPluginVersion)
val appliedPlugins = subpluginEnvironment.addSubpluginOptions(
project, kotlinTaskInstance, null, null, null, kotlinCompilation
)
if (outputDir.isParentOf(project.rootDir))
throw InvalidUserDataException(
"The output directory '$outputDir' (defined by outputFile of $kotlinTaskInstance) contains or " +
"matches the project root directory '${project.rootDir}'.\n" +
"Gradle will not be able to build the project because of the root directory lock.\n" +
"To fix this, consider using the default outputFile location instead of providing it explicitly."
)
kotlinTaskInstance.destinationDir = outputDir
appliedPlugins
.flatMap { it.getSubpluginKotlinTasks(project, kotlinTaskInstance) }
.forEach { task -> kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> task.source(sourceSet.kotlin) } }
subpluginEnvironment.addSubpluginOptions(project, kotlinCompilation)
}
}
}
@@ -352,17 +334,17 @@ internal class KotlinJsIrSourceSetProcessor(
}
}
project.whenEvaluated {
val subpluginEnvironment: SubpluginEnvironment = SubpluginEnvironment.loadSubplugins(project, kotlinPluginVersion)
subpluginEnvironment.addSubpluginOptions(project, kotlinCompilation)
}
// outputFile can be set later during the configuration phase, get it only after the phase:
project.runOnceAfterEvaluated("KotlinJsIrSourceSetProcessor.doTargetSpecificProcessing", kotlinTask) {
val kotlinTaskInstance = kotlinTask.get()
kotlinTaskInstance.kotlinOptions.outputFile = kotlinTaskInstance.outputFile.absolutePath
val outputDir = kotlinTaskInstance.outputFile.parentFile
val subpluginEnvironment: SubpluginEnvironment = SubpluginEnvironment.loadSubplugins(project, kotlinPluginVersion)
val appliedPlugins = subpluginEnvironment.addSubpluginOptions(
project, kotlinTaskInstance, null, null, null, kotlinCompilation
)
if (outputDir.isParentOf(project.rootDir))
throw InvalidUserDataException(
"The output directory '$outputDir' (defined by outputFile of $kotlinTaskInstance) contains or " +
@@ -372,10 +354,6 @@ internal class KotlinJsIrSourceSetProcessor(
)
kotlinTaskInstance.destinationDir = outputDir
appliedPlugins
.flatMap { it.getSubpluginKotlinTasks(project, kotlinTaskInstance) }
.forEach { task -> kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> task.source(sourceSet.kotlin) } }
}
}
}
@@ -394,15 +372,9 @@ internal class KotlinCommonSourceSetProcessor(
project.locateTask<Task>(kotlinCompilation.target.artifactsTaskName)?.dependsOn(kotlinTask)
}
project.runOnceAfterEvaluated("KotlinCommonSourceSetProcessor.doTargetSpecificProcessing", kotlinTask) {
val kotlinTaskInstance = kotlinTask.get()
project.whenEvaluated {
val subpluginEnvironment: SubpluginEnvironment = SubpluginEnvironment.loadSubplugins(project, kotlinPluginVersion)
val appliedPlugins = subpluginEnvironment.addSubpluginOptions(
project, kotlinTaskInstance, null, null, null, kotlinCompilation
)
appliedPlugins
.flatMap { it.getSubpluginKotlinTasks(project, kotlinTaskInstance) }
.forEach { kotlinCompilation.allKotlinSourceSets.forEach { sourceSet -> it.source(sourceSet.kotlin) } }
subpluginEnvironment.addSubpluginOptions(project, kotlinCompilation)
}
}
@@ -779,7 +751,6 @@ abstract class AbstractAndroidProjectHandler(private val kotlinConfigurationTool
abstract fun getLibraryOutputTask(variant: BaseVariant): Any?
protected abstract fun getSourceProviders(variantData: BaseVariant): Iterable<SourceProvider>
protected abstract fun getAllJavaSources(variantData: BaseVariant): Iterable<File>
protected abstract fun getJavaTask(variantData: BaseVariant): AbstractCompile?
protected abstract fun addJavaSourceDirectoryToVariantModel(variantData: BaseVariant, javaSourceDirectory: File): Unit
@@ -871,7 +842,7 @@ abstract class AbstractAndroidProjectHandler(private val kotlinConfigurationTool
postprocessVariant(variant, compilation, project, ext, plugin)
val subpluginEnvironment = SubpluginEnvironment.loadSubplugins(project, kotlinConfigurationTools.kotlinPluginVersion)
applySubplugins(project, compilation, variant, subpluginEnvironment)
subpluginEnvironment.addSubpluginOptions(project, compilation)
}
checkAndroidAnnotationProcessorDependencyUsage(project)
@@ -1027,45 +998,32 @@ abstract class AbstractAndroidProjectHandler(private val kotlinConfigurationTool
compilation.associateWith(testedCompilation)
}
val kotlinTask = compilation.compileKotlinTask
configureSources(kotlinTask, variantData, compilation)
val kotlinTask = compilation.compileKotlinTaskProvider
processAndroidKotlinAndJavaSources(
compilation,
addKotlinSources = { kotlinSourceSet -> compilation.source(kotlinSourceSet) },
addJavaSources = { sources -> compilation.compileKotlinTaskProvider.configure { it.source(sources) } }
)
wireKotlinTasks(project, compilation, androidPlugin, androidExt, variantData, javaTask, kotlinTask)
}
}
private fun applySubplugins(
project: Project,
compilation: KotlinCompilation<*>,
variantData: BaseVariant,
subpluginEnvironment: SubpluginEnvironment
) {
val kotlinTask = project.tasks.getByName(compilation.compileKotlinTaskName) as KotlinCompile
val javaTask = getJavaTask(variantData)
private fun getAllJavaSources(variantData: BaseVariant): Iterable<File> =
variantData.getSourceFolders(SourceKind.JAVA).map { it.dir }
val appliedPlugins = subpluginEnvironment.addSubpluginOptions(
project, kotlinTask, javaTask, wrapVariantDataForKapt(variantData), this, null
)
internal fun processAndroidKotlinAndJavaSources(
compilation: KotlinJvmAndroidCompilation,
addKotlinSources: (KotlinSourceSet) -> Unit,
addJavaSources: (Iterable<File>) -> Unit
) {
val variantData = compilation.androidVariant
appliedPlugins.flatMap { it.getSubpluginKotlinTasks(project, kotlinTask) }
.forEach { configureSources(it, variantData, null) }
for (provider in variantData.sourceSets) {
val kotlinSourceSet = provider.getConvention(KOTLIN_DSL_NAME) as? KotlinSourceSet ?: continue
addKotlinSources(kotlinSourceSet)
}
private fun configureSources(compileTask: AbstractCompile, variantData: BaseVariant, compilation: KotlinCompilation<*>?) {
val logger = compileTask.project.logger
for (provider in getSourceProviders(variantData)) {
val kotlinSourceSet = provider.getConvention(KOTLIN_DSL_NAME) as? KotlinSourceSet ?: continue
if (compilation != null) {
compilation.source(kotlinSourceSet)
} else {
compileTask.source(kotlinSourceSet.kotlin)
}
}
for (javaSrcDir in getAllJavaSources(variantData)) {
compileTask.source(javaSrcDir)
logger.kotlinDebug("Source directory $javaSrcDir was added to kotlin source for ${compileTask.name}")
}
}
addJavaSources(getAllJavaSources(variantData))
}
internal fun configureJavaTask(
@@ -1,20 +1,30 @@
package org.jetbrains.kotlin.gradle.plugin
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.compile.AbstractCompile
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
import org.gradle.api.tasks.compile.JavaCompile
import org.jetbrains.kotlin.gradle.dsl.KotlinCompile
import org.jetbrains.kotlin.gradle.logging.kotlinDebug
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaCompilation
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
import org.jetbrains.kotlin.gradle.tasks.CompilerPluginOptions
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
import org.jetbrains.kotlin.gradle.tasks.thisTaskProvider
import java.util.*
class SubpluginEnvironment(
private val subplugins: List<KotlinGradleSubplugin<AbstractCompile>>,
private val subplugins: List<KotlinCompilerPluginSupportPlugin>,
private val kotlinPluginVersion: String
) {
companion object {
fun loadSubplugins(project: Project, kotlinPluginVersion: String): SubpluginEnvironment =
try {
@Suppress("DEPRECATION") // support for the deprecated plugin API
val klass = KotlinGradleSubplugin::class.java
val buildscriptClassloader = project.buildscript.classLoader
val klassFromBuildscript = try {
@@ -29,10 +39,14 @@ class SubpluginEnvironment(
klass.classLoader
}
val subplugins = ServiceLoader.load(KotlinGradleSubplugin::class.java, classloader)
.map { @Suppress("UNCHECKED_CAST") (it as KotlinGradleSubplugin<AbstractCompile>) }
val result = project.plugins.filterIsInstance<KotlinCompilerPluginSupportPlugin>()
SubpluginEnvironment(subplugins, kotlinPluginVersion)
@Suppress("DEPRECATION", "UNCHECKED_CAST")
val compatibilitySubplugins = ServiceLoader.load(klass, classloader).map {
LegacyKotlinCompilerPluginSupportPlugin(it as KotlinGradleSubplugin<AbstractCompile>)
}
SubpluginEnvironment(result + compatibilitySubplugins, kotlinPluginVersion)
} catch (e: NoClassDefFoundError) {
// Skip plugin loading if KotlinGradleSubplugin is not defined.
// It is true now for tests in kotlin-gradle-plugin-core.
@@ -41,35 +55,13 @@ class SubpluginEnvironment(
}
}
fun <C : CommonCompilerArguments> addSubpluginOptions(
project: Project,
kotlinTask: AbstractKotlinCompile<C>,
javaTask: AbstractCompile? = null,
variantData: Any? = null,
androidProjectHandler: AbstractAndroidProjectHandler? = null,
kotlinCompilation: KotlinCompilation<*>? = null
): List<KotlinGradleSubplugin<AbstractKotlinCompile<C>>> = addSubpluginOptions(
project,
kotlinTask,
kotlinTask.pluginOptions,
javaTask,
variantData,
androidProjectHandler,
kotlinCompilation
)
fun addSubpluginOptions(
project: Project,
kotlinTask: AbstractCompile,
pluginOptions: CompilerPluginOptions,
javaTask: AbstractCompile? = null,
variantData: Any? = null,
androidProjectHandler: AbstractAndroidProjectHandler? = null,
kotlinCompilation: KotlinCompilation<*>? = null
): List<KotlinGradleSubplugin<AbstractCompile>> {
val appliedSubplugins = subplugins.filter { it.isApplicable(project, kotlinTask) }
kotlinCompilation: KotlinCompilation<*>
): List<KotlinCompilerPluginSupportPlugin> {
val appliedSubplugins = subplugins.filter { it.isApplicable(kotlinCompilation) }
for (subplugin in appliedSubplugins) {
if (!subplugin.isApplicable(project, kotlinTask)) continue
if (!subplugin.isApplicable(kotlinCompilation)) continue
val pluginId = subplugin.getCompilerPluginId()
project.logger.kotlinDebug { "Loading subplugin $pluginId" }
@@ -82,24 +74,104 @@ class SubpluginEnvironment(
project.addMavenDependency(NATIVE_COMPILER_PLUGIN_CLASSPATH_CONFIGURATION_NAME, artifact)
}
val subpluginOptions =
subplugin.apply(project, kotlinTask, javaTask, variantData, androidProjectHandler, kotlinCompilation)
val subpluginOptionsProvider = subplugin.applyToCompilation(kotlinCompilation)
val subpluginId = subplugin.getCompilerPluginId()
kotlinTask.registerSubpluginOptionsAsInputs(subpluginId, subpluginOptions)
for (option in subpluginOptions) {
pluginOptions.addPluginArgument(subpluginId, option)
kotlinCompilation.compileKotlinTaskProvider.configure {
val pluginOptions = it.getPluginOptions()
val subpluginOptions = subpluginOptionsProvider.get()
for (option in subpluginOptions) {
pluginOptions.addPluginArgument(subpluginId, option)
}
it.registerSubpluginOptionsAsInputs(subpluginId, subpluginOptions)
}
project.logger.kotlinDebug("Subplugin $pluginId loaded")
if (subplugin is LegacyKotlinCompilerPluginSupportPlugin) {
subplugin.getPluginKotlinTasks(kotlinCompilation).forEach { task ->
addCompilationSourcesToExternalCompileTask(kotlinCompilation, task.thisTaskProvider)
}
}
}
return appliedSubplugins
}
private fun KotlinCompile<*>.getPluginOptions(): CompilerPluginOptions = when (this) {
is AbstractKotlinCompile<*> -> pluginOptions
is KotlinNativeCompile -> compilerPluginOptions
else -> error("Unexpected task ${this.name}, class: ${this.javaClass}")
}
private fun Project.addMavenDependency(configuration: String, artifact: SubpluginArtifact) {
val artifactVersion = artifact.version ?: kotlinPluginVersion
val mavenCoordinate = "${artifact.groupId}:${artifact.artifactId}:$artifactVersion"
project.logger.kotlinDebug { "Adding '$mavenCoordinate' to '$configuration' configuration" }
project.dependencies.add(configuration, mavenCoordinate)
}
}
}
internal fun addCompilationSourcesToExternalCompileTask(compilation: KotlinCompilation<*>, task: TaskProvider<out AbstractCompile>) {
if (compilation is KotlinJvmAndroidCompilation) {
processAndroidKotlinAndJavaSources(
compilation,
addKotlinSources = { sourceSet -> task.configure { it.source(sourceSet.kotlin) } },
addJavaSources = { sources -> task.configure { it.source(sources) }}
)
} else {
task.configure { taskInstance ->
compilation.allKotlinSourceSets.forEach { sourceSet -> taskInstance.source(sourceSet.kotlin) }
}
}
}
internal class LegacyKotlinCompilerPluginSupportPlugin(
@Suppress("deprecation") // support for deprecated API
val oldPlugin: KotlinGradleSubplugin<AbstractCompile>
): KotlinCompilerPluginSupportPlugin {
override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean =
oldPlugin.isApplicable(kotlinCompilation.target.project, kotlinCompilation.compileKotlinTaskProvider.get() as AbstractCompile)
override fun applyToCompilation(
kotlinCompilation: KotlinCompilation<*>
): Provider<List<SubpluginOption>> {
val project = kotlinCompilation.target.project
val androidProjectHandlerOrNull: AbstractAndroidProjectHandler? = if (kotlinCompilation is KotlinJvmAndroidCompilation)
KotlinAndroidPlugin.androidTargetHandler(
checkNotNull(project.getKotlinPluginVersion()),
kotlinCompilation.target as KotlinAndroidTarget
) else null
val variantData = (kotlinCompilation as? KotlinJvmAndroidCompilation)?.androidVariant
val result = oldPlugin.apply(
project,
kotlinCompilation.compileKotlinTask as AbstractCompile,
findJavaTaskForKotlinCompilation(kotlinCompilation)?.get(),
variantData,
androidProjectHandlerOrNull,
if (variantData != null) null else kotlinCompilation
)
return project.provider { result }
}
fun getPluginKotlinTasks(compilation: KotlinCompilation<*>): List<AbstractCompile> {
val project = compilation.target.project
return oldPlugin.getSubpluginKotlinTasks(project, compilation.compileKotlinTask as AbstractCompile)
}
override fun getCompilerPluginId(): String = oldPlugin.getCompilerPluginId()
override fun getPluginArtifact(): SubpluginArtifact = oldPlugin.getPluginArtifact()
override fun getNativeCompilerPluginArtifact(): SubpluginArtifact? = oldPlugin.getNativeCompilerPluginArtifact()
}
internal fun findJavaTaskForKotlinCompilation(compilation: KotlinCompilation<*>): TaskProvider<out JavaCompile>? =
when (compilation) {
is KotlinJvmAndroidCompilation -> compilation.compileJavaTaskProvider
is KotlinWithJavaCompilation -> compilation.compileJavaTaskProvider
is KotlinJvmCompilation -> compilation.compileJavaTaskProvider // may be null for Kotlin-only JVM target in MPP
else -> null
}
@@ -89,9 +89,6 @@ class Android25ProjectHandler(
override fun getSourceProviders(variantData: BaseVariant): Iterable<SourceProvider> =
variantData.sourceSets
override fun getAllJavaSources(variantData: BaseVariant): Iterable<File> =
variantData.getSourceFolders(SourceKind.JAVA).map { it.dir }
override fun getFlavorNames(variant: BaseVariant): List<String> = variant.productFlavors.map { it.name }
override fun getBuildTypeName(variant: BaseVariant): String = variant.buildType.name