Kapt: Allow running kapt using Workers API in Gradle

This commit is contained in:
Yan Zhulanow
2018-05-14 16:37:43 +03:00
parent 2bc45e0484
commit 64ea16fdfc
8 changed files with 349 additions and 104 deletions
@@ -46,12 +46,7 @@ class AndroidExtensionsSubpluginIndicator : Plugin<Project> {
}
private fun addAndroidExtensionsRuntimeIfNeeded(project: Project) {
val kotlinPluginWrapper = project.plugins.findPlugin(KotlinAndroidPluginWrapper::class.java) ?: run {
project.logger.error("'kotlin-android' plugin should be enabled before 'kotlin-android-extensions'")
return
}
val kotlinPluginVersion = kotlinPluginWrapper.kotlinPluginVersion
val kotlinPluginVersion = project.getKotlinPluginVersion() ?: return
project.configurations.all { configuration ->
val name = configuration.name
@@ -21,6 +21,7 @@ import org.gradle.process.CommandLineArgumentProvider
import org.jetbrains.kotlin.gradle.internal.Kapt3GradleSubplugin.Companion.getKaptGeneratedClassesDir
import org.jetbrains.kotlin.gradle.internal.Kapt3GradleSubplugin.Companion.getKaptGeneratedKotlinSourcesDir
import org.jetbrains.kotlin.gradle.internal.Kapt3GradleSubplugin.Companion.getKaptGeneratedSourcesDir
import org.jetbrains.kotlin.gradle.tasks.isWorkerAPISupported
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.tasks.CompilerPluginOptions
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -71,6 +72,11 @@ abstract class KaptVariantData<T>(val variantData: T) {
class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin<KotlinCompile> {
companion object {
private val VERBOSE_OPTION_NAME = "kapt.verbose"
private val USE_WORKER_API = "kapt.use.worker.api"
const val KAPT_WORKER_DEPENDENCIES_CONFIGURATION_NAME = "kotlinKaptWorkerDependencies"
private val KAPT_KOTLIN_GENERATED = "kapt.kotlin.generated"
val MAIN_KAPT_CONFIGURATION_NAME = "kapt"
@@ -88,6 +94,14 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin<KotlinCompile> {
return project.configurations.findByName(getKaptConfigurationName(sourceSetName))
}
fun Project.isKaptVerbose(): Boolean {
return hasProperty(VERBOSE_OPTION_NAME) && property(VERBOSE_OPTION_NAME) == "true"
}
fun Project.isUseWorkerApi(): Boolean {
return isWorkerAPISupported() && hasProperty(USE_WORKER_API) && property(USE_WORKER_API) == "true"
}
fun findMainKaptConfiguration(project: Project) = project.findKaptConfiguration(MAIN_KAPT_CONFIGURATION_NAME)
fun createAptConfigurationIfNeeded(project: Project, sourceSetName: String): Configuration {
@@ -183,7 +197,7 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin<KotlinCompile> {
)
val kaptGenerateStubsTask = context.createKaptGenerateStubsTask()
val kaptTask = context.createKaptKotlinTask()
val kaptTask = context.createKaptKotlinTask(useWorkerApi = project.isUseWorkerApi())
kaptGenerateStubsTask.source(*kaptConfigurations.toTypedArray())
@@ -224,14 +238,26 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin<KotlinCompile> {
pluginOptions += SubpluginOption("processors", annotationProcessors)
}
kotlinSourcesOutputDir.mkdirs()
val apOptions = getAPOptions()
pluginOptions += CompositeSubpluginOption("apoptions", encodeList(apOptions.associate { it.key to it.value }), apOptions)
pluginOptions += SubpluginOption("javacArguments", encodeList(kaptExtension.getJavacOptions()))
addMiscOptions(pluginOptions)
return pluginOptions
}
private fun Kapt3SubpluginContext.getAPOptions(): List<SubpluginOption> {
val androidPlugin = kaptVariantData?.let {
project.extensions.findByName("android") as? BaseExtension
}
val androidOptions = kaptVariantData?.annotationProcessorOptions ?: emptyMap()
kotlinSourcesOutputDir.mkdirs()
val apOptionsFromProviders =
if (isGradleVersionAtLeast(4, 6))
kaptVariantData?.annotationProcessorOptionProviders
@@ -245,16 +271,8 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin<KotlinCompile> {
androidOptions.toList() +
apOptionsFromProviders
val apOptions = apOptionsPairsList.map { SubpluginOption(it.first, it.second) } +
FilesSubpluginOption("kapt.kotlin.generated", listOf(kotlinSourcesOutputDir))
pluginOptions += CompositeSubpluginOption("apoptions", encodeList(apOptions.associate { it.key to it.value }), apOptions)
pluginOptions += SubpluginOption("javacArguments", encodeList(kaptExtension.getJavacOptions()))
addMiscOptions(pluginOptions)
return pluginOptions
return apOptionsPairsList.map { SubpluginOption(it.first, it.second) } +
FilesSubpluginOption(KAPT_KOTLIN_GENERATED, listOf(kotlinSourcesOutputDir))
}
private fun Kapt3SubpluginContext.buildAndAddOptionsTo(task: Task, container: CompilerPluginOptions, aptMode: String) {
@@ -298,15 +316,14 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin<KotlinCompile> {
pluginOptions += SubpluginOption("mapDiagnosticLocations", "${kaptExtension.mapDiagnosticLocations}")
pluginOptions += FilesSubpluginOption("stubs", listOf(getKaptStubsDir()))
if (project.hasProperty(VERBOSE_OPTION_NAME) && project.property(VERBOSE_OPTION_NAME) == "true") {
if (project.isKaptVerbose()) {
pluginOptions += SubpluginOption("verbose", "true")
}
}
private fun Kapt3SubpluginContext.createKaptKotlinTask(): KaptTask {
val kaptTask = project.tasks.create(
getKaptTaskName("kapt"),
KaptTask::class.java)
private fun Kapt3SubpluginContext.createKaptKotlinTask(useWorkerApi: Boolean): KaptTask {
val taskClass = if (useWorkerApi) KaptWithoutKotlincTask::class.java else KaptWithKotlincTask::class.java
val kaptTask = project.tasks.create(getKaptTaskName("kapt"), taskClass)
kaptTask.useBuildCache = kaptExtension.useBuildCache
@@ -333,11 +350,32 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin<KotlinCompile> {
kaptTask.kaptClasspathConfigurations = kaptClasspathConfigurations
kaptVariantData?.annotationProcessorOptionProviders?.let {
kaptTask.annotationProcessorOptionProviders.add(it)
if (kaptTask is KaptWithKotlincTask) {
kaptVariantData?.annotationProcessorOptionProviders?.let {
kaptTask.annotationProcessorOptionProviders.add(it)
}
buildAndAddOptionsTo(kaptTask, kaptTask.pluginOptions, aptMode = "apt")
}
buildAndAddOptionsTo(kaptTask, kaptTask.pluginOptions, aptMode = "apt")
if (kaptTask is KaptWithoutKotlincTask) {
project.configurations.create(KAPT_WORKER_DEPENDENCIES_CONFIGURATION_NAME).apply {
project.getKotlinPluginVersion()?.let { kotlinPluginVersion ->
val kaptDependency = getPluginArtifact().run { "$groupId:$artifactId:$kotlinPluginVersion" }
dependencies.add(project.dependencies.create(kaptDependency))
} ?: project.logger.error("Kotlin plugin should be enabled before 'kotlin-kapt'")
}
with(kaptTask) {
projectDir = project.projectDir
isVerbose = project.isKaptVerbose()
mapDiagnosticLocations = kaptExtension.mapDiagnosticLocations
annotationProcessorFqNames = kaptExtension.processors.split(',').filter { it.isNotEmpty() }
processorOptions = getAPOptions().map { it.key to it.value }.toMap()
javacOptions = kaptExtension.getJavacOptions()
}
}
return kaptTask
}
@@ -0,0 +1,84 @@
package org.jetbrains.kotlin.gradle.internal
import org.gradle.api.GradleException
import org.gradle.api.artifacts.Configuration
import org.gradle.api.file.FileCollection
import org.gradle.api.internal.ConventionTask
import org.gradle.api.tasks.*
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
import org.jetbrains.kotlin.compilerRunner.GradleCompilerEnvironment
import org.jetbrains.kotlin.compilerRunner.GradleCompilerRunner
import org.jetbrains.kotlin.compilerRunner.OutputItemsCollectorImpl
import org.jetbrains.kotlin.gradle.plugin.compareVersionNumbers
import org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME
import org.jetbrains.kotlin.gradle.tasks.*
import org.jetbrains.kotlin.gradle.utils.isJavaFile
import org.jetbrains.kotlin.gradle.utils.isParentOf
import org.jetbrains.kotlin.gradle.utils.toSortedPathsArray
import java.io.File
@CacheableTask
open class KaptTask : ConventionTask() {
init {
cacheOnlyIfEnabledForKotlin()
if (isBuildCacheSupported()) {
val reason = "Caching is disabled by default for kapt because of arbitrary behavior of external " +
"annotation processors. You can enable it by adding 'kapt.useBuildCache = true' to the build script."
outputs.cacheIf(reason) { useBuildCache }
}
}
@get:Internal
internal lateinit var kotlinCompileTask: KotlinCompile
@get:Internal
internal lateinit var stubsDir: File
@get:Classpath @get:InputFiles
val kaptClasspath: FileCollection
get() = project.files(*kaptClasspathConfigurations.toTypedArray())
@get:Classpath @get:InputFiles
val compilerClasspath: List<File> get() = kotlinCompileTask.computedCompilerClasspath
@get:Internal
internal lateinit var kaptClasspathConfigurations: List<Configuration>
@get:OutputDirectory
internal lateinit var classesDir: File
@get:OutputDirectory
lateinit var destinationDir: File
@get:OutputDirectory
lateinit var kotlinSourcesDestinationDir: File
@get:Nested
internal val annotationProcessorOptionProviders: MutableList<Any> = mutableListOf()
@get:Classpath @get:InputFiles
val classpath: FileCollection
get() = kotlinCompileTask.classpath
@get:Internal
var useBuildCache: Boolean = false
@get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE)
val source: Collection<File>
get() {
val result = HashSet<File>()
for (root in javaSourceRoots) {
root.walk().filterTo(result) { it.isJavaFile() }
}
return result
}
protected val javaSourceRoots: Set<File>
get() = (kotlinCompileTask.sourceRootsContainer.sourceRoots + stubsDir)
.filterTo(HashSet(), ::isRootAllowed)
private fun isRootAllowed(file: File): Boolean =
!FileUtil.isAncestor(destinationDir, file, /* strict = */ false) &&
!FileUtil.isAncestor(classesDir, file, /* strict = */ false)
}
@@ -1,65 +1,41 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.gradle.internal
import com.intellij.openapi.util.text.StringUtil
import org.gradle.api.GradleException
import org.gradle.api.artifacts.Configuration
import org.gradle.api.file.FileCollection
import org.gradle.api.internal.ConventionTask
import org.gradle.api.tasks.*
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
import org.jetbrains.kotlin.compilerRunner.GradleCompilerEnvironment
import org.jetbrains.kotlin.compilerRunner.GradleCompilerRunner
import org.jetbrains.kotlin.compilerRunner.OutputItemsCollectorImpl
import org.jetbrains.kotlin.gradle.plugin.compareVersionNumbers
import org.jetbrains.kotlin.gradle.tasks.CompilerPluginOptions
import org.jetbrains.kotlin.gradle.tasks.GradleMessageCollector
import org.jetbrains.kotlin.gradle.tasks.clearOutputDirectories
import org.jetbrains.kotlin.gradle.tasks.throwGradleExceptionIfError
import org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME
import org.jetbrains.kotlin.gradle.tasks.*
import org.jetbrains.kotlin.gradle.utils.isJavaFile
import org.jetbrains.kotlin.gradle.utils.isParentOf
import org.jetbrains.kotlin.gradle.utils.toSortedPathsArray
import java.io.File
@CacheableTask
open class KaptTask : ConventionTask(), CompilerArgumentAwareWithInput<K2JVMCompilerArguments> {
init {
cacheOnlyIfEnabledForKotlin()
if (isBuildCacheSupported()) {
val reason = "Caching is disabled by default for kapt because of arbitrary behavior of external " +
"annotation processors. You can enable it by adding 'kapt.useBuildCache = true' to the build script."
outputs.cacheIf(reason) { useBuildCache }
}
}
open class KaptWithKotlincTask : KaptTask(), CompilerArgumentAwareWithInput<K2JVMCompilerArguments> {
@get:Internal
internal val pluginOptions = CompilerPluginOptions()
@get:Internal
internal lateinit var kotlinCompileTask: KotlinCompile
@get:Classpath
@get:InputFiles
@Suppress("unused")
internal val kotlinTaskPluginClasspaths get() = kotlinCompileTask.pluginClasspath
@get:Internal
internal lateinit var stubsDir: File
@get:Classpath @get:InputFiles
val kaptClasspath: FileCollection
get() = project.files(*kaptClasspathConfigurations.toTypedArray())
@get:Classpath @get:InputFiles
val compilerClasspath: List<File> get() = kotlinCompileTask.computedCompilerClasspath
@get:Internal
internal lateinit var kaptClasspathConfigurations: List<Configuration>
@get:OutputDirectory
internal lateinit var classesDir: File
@get:OutputDirectory
lateinit var destinationDir: File
@get:OutputDirectory
lateinit var kotlinSourcesDestinationDir: File
@get:Nested
internal val annotationProcessorOptionProviders: MutableList<Any> = mutableListOf()
@get:Classpath
@get:InputFiles
val pluginClasspath: FileCollection
get() = project.configurations.getByName(PLUGIN_CLASSPATH_CONFIGURATION_NAME)
override fun createCompilerArgs(): K2JVMCompilerArguments = K2JVMCompilerArguments()
@@ -74,37 +50,10 @@ open class KaptTask : ConventionTask(), CompilerArgumentAwareWithInput<K2JVMComp
args.verbose = project.hasProperty("kapt.verbose") && project.property("kapt.verbose").toString().toBoolean() == true
}
@get:Classpath @get:InputFiles
val classpath: FileCollection
get() = kotlinCompileTask.classpath
@get:Internal
var useBuildCache: Boolean = false
@get:Classpath
@get:InputFiles
val pluginClasspath: FileCollection
get() = project.configurations.getByName(PLUGIN_CLASSPATH_CONFIGURATION_NAME)
@get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE)
val source: Collection<File>
get() {
val result = HashSet<File>()
for (root in javaSourceRoots) {
root.walk().filterTo(result) { it.isJavaFile() }
}
return result
}
private val javaSourceRoots: Set<File>
get() = (kotlinCompileTask.sourceRootsContainer.sourceRoots + stubsDir)
.filterTo(HashSet(), ::isRootAllowed)
private fun isRootAllowed(file: File): Boolean =
!destinationDir.isParentOf(file) && !classesDir.isParentOf(file)
@TaskAction
fun compile() {
logger.debug("Running kapt annotation processing using the Kotlin compiler")
/** Delete everything inside generated sources and classes output directory
* (annotation processing is not incremental) */
clearOutputDirectories()
@@ -130,7 +79,7 @@ open class KaptTask : ConventionTask(), CompilerArgumentAwareWithInput<K2JVMComp
}
private val isAtLeastJava9: Boolean
get() = compareVersionNumbers(getJavaRuntimeVersion(), "9") >= 0
get() = StringUtil.compareVersionNumbers(getJavaRuntimeVersion(), "9") >= 0
private fun getJavaRuntimeVersion(): String {
val rtVersion = System.getProperty("java.runtime.version")
@@ -0,0 +1,167 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.gradle.internal
import org.gradle.api.tasks.*
import org.gradle.api.file.FileCollection
import org.gradle.workers.IsolationMode
import org.gradle.workers.WorkerExecutor
import org.jetbrains.kotlin.gradle.internal.Kapt3KotlinGradleSubplugin.Companion.KAPT_WORKER_DEPENDENCIES_CONFIGURATION_NAME
import org.jetbrains.kotlin.gradle.tasks.clearOutputDirectories
import org.jetbrains.kotlin.gradle.tasks.findKotlinStdlibClasspath
import org.jetbrains.kotlin.gradle.tasks.findToolsJar
import java.io.File
import java.io.Serializable
import java.net.URLClassLoader
import javax.inject.Inject
open class KaptWithoutKotlincTask @Inject constructor(private val workerExecutor: WorkerExecutor) : KaptTask() {
@get:InputFiles
@get:Classpath
@Suppress("unused")
val kaptJars: Collection<File>
get() = project.configurations.getByName(KAPT_WORKER_DEPENDENCIES_CONFIGURATION_NAME).resolve()
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
lateinit var projectDir: File
@get:Input
var isVerbose: Boolean = false
@get:Input
var mapDiagnosticLocations: Boolean = false
@get:Input
lateinit var annotationProcessorFqNames: List<String>
@get:Input
lateinit var processorOptions: Map<String, String>
@get:Input
lateinit var javacOptions: Map<String, String>
@TaskAction
fun compile() {
logger.info("Running kapt annotation processing using the Gradle Worker API")
clearOutputDirectories()
val paths = KaptPathsForWorker(
projectDir,
classpath.files.toList(),
kaptClasspath.files.toList(),
javaSourceRoots.toList(),
destinationDir,
classesDir,
stubsDir
)
val options = KaptOptionsForWorker(
isVerbose,
mapDiagnosticLocations,
annotationProcessorFqNames,
processorOptions,
javacOptions
)
val kaptClasspath = kaptJars + findKotlinStdlibClasspath(project)
workerExecutor.submit(KaptExecution::class.java) { config ->
config.isolationMode = IsolationMode.PROCESS
config.params(options, paths, findToolsJar(), kaptClasspath)
logger.info("Kapt worker classpath: ${config.classpath}")
}
workerExecutor.await()
}
}
private class KaptExecution @Inject constructor(
val options: KaptOptionsForWorker,
val paths: KaptPathsForWorker,
val toolsJar: File?,
val kaptClasspath: List<File>
) : Runnable {
private companion object {
private const val JAVAC_CONTEXT_CLASS = "com.sun.tools.javac.util.Context"
}
override fun run(): Unit = with(options) {
val kaptClasspathUrls = kaptClasspath.map { it.toURI().toURL() }.toTypedArray()
val rootClassLoader = findRootClassLoader()
val classLoaderWithToolsJar = if (toolsJar != null && !javacIsAlreadyHere()) {
toolsJar.let { URLClassLoader(arrayOf(it.toURI().toURL()), rootClassLoader) }
} else {
rootClassLoader
}
val kaptClassLoader = URLClassLoader(kaptClasspathUrls, classLoaderWithToolsJar)
val kaptMethod = Class.forName("org.jetbrains.kotlin.kapt3.base.Kapt", true, kaptClassLoader)
.declaredMethods.single { it.name == "kapt" }
kaptMethod.invoke(
null,
createKaptPaths(kaptClassLoader),
isVerbose,
mapDiagnosticLocations,
annotationProcessorFqNames,
processorOptions,
javacOptions
)
}
private fun javacIsAlreadyHere(): Boolean {
return try {
Class.forName(JAVAC_CONTEXT_CLASS, false, KaptExecution::class.java.classLoader) != null
} catch (e: Throwable) {
false
}
}
private fun createKaptPaths(classLoader: ClassLoader) = with(paths) {
Class.forName("org.jetbrains.kotlin.kapt3.base.KaptPaths", true, classLoader).constructors.single().newInstance(
projectBaseDir,
compileClasspath,
annotationProcessingClasspath,
javaSourceRoots,
sourcesOutputDir,
classFilesOutputDir,
stubsOutputDir,
stubsOutputDir // sic!
)
}
private fun findRootClassLoader(): ClassLoader {
tailrec fun parentOrSelf(classLoader: ClassLoader): ClassLoader {
val parent = classLoader.parent ?: return classLoader
return parentOrSelf(parent)
}
return parentOrSelf(KaptExecution::class.java.classLoader)
}
}
private data class KaptOptionsForWorker(
val isVerbose: Boolean,
val mapDiagnosticLocations: Boolean,
val annotationProcessorFqNames: List<String>,
val processorOptions: Map<String, String>,
val javacOptions: Map<String, String>
) : Serializable
private data class KaptPathsForWorker(
val projectBaseDir: File,
val compileClasspath: List<File>,
val annotationProcessingClasspath: List<File>,
val javaSourceRoots: List<File>,
val sourcesOutputDir: File,
val classFilesOutputDir: File,
val stubsOutputDir: File
) : Serializable
@@ -82,6 +82,15 @@ open class Kotlin2JsPluginWrapper @Inject constructor(fileResolver: FileResolver
Kotlin2JsPlugin(Kotlin2JsTasksProvider(), KotlinSourceSetProviderImpl(fileResolver), kotlinPluginVersion)
}
fun Project.getKotlinPluginVersion(): String? {
val kotlinPluginWrapper = plugins.findPlugin(KotlinAndroidPluginWrapper::class.java) ?: run {
project.logger.error("'kotlin-android' plugin should be enabled before 'kotlin-android-extensions'")
return null
}
return kotlinPluginWrapper.kotlinPluginVersion
}
private fun Any.loadKotlinVersionFromResource(log: Logger): String {
log.kotlinDebug("Loading version information")
val props = Properties()
@@ -23,6 +23,9 @@ import org.jetbrains.kotlin.gradle.utils.outputsCompatible
internal fun isBuildCacheSupported(): Boolean =
gradleVersion >= GradleVersion.version("4.3")
internal fun isWorkerAPISupported(): Boolean =
gradleVersion >= GradleVersion.version("4.3")
internal fun isBuildCacheEnabledForKotlin(): Boolean =
isBuildCacheSupported() &&
System.getProperty(KOTLIN_CACHING_ENABLED_PROPERTY)?.toBoolean() ?: true