Incremental annotation processing with KAPT

Add support for incremental annotation processors in KAPT. These
processors conform to https://docs.gradle.org/current/userguide/java_plugin.html#sec:incremental_annotation_processing
specification.

Support is provided by using javac compiler APIs and
recording the source file structure. At runtime, processors
are instrumented with custom Filer that is used to keep track of generated
files. In order to support classpath changes, stub generation task is
used to generated a list of changed FQCNs, and this is simply used by KAPT.
Both worker and non-worker mode are supported.

 #KT-23880
This commit is contained in:
Ivan Gavrilovic
2019-03-02 12:58:45 +00:00
committed by Alexey Tsvetkov
parent 600a955a51
commit 9f14daa682
56 changed files with 2649 additions and 58 deletions
@@ -0,0 +1,55 @@
/*
* Copyright 2010-2019 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.incapt;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import java.io.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.Set;
/** Simple processor that generates a class for every annotated element (class, field, method). */
public class IncrementalProcessor extends AbstractProcessor {
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton("example.ExampleAnnotation");
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations.isEmpty()) return true;
for (Element element : roundEnv.getElementsAnnotatedWith(annotations.iterator().next())) {
if (element instanceof TypeElement || element instanceof ExecutableElement || element instanceof VariableElement) {
String name = element.getSimpleName().toString();
name = name.substring(0, 1).toUpperCase() + name.substring(1) + "Generated";
String packageName;
if (element instanceof TypeElement) {
packageName = element.getEnclosingElement().getSimpleName().toString();
}
else {
packageName = element.getEnclosingElement().getEnclosingElement().getSimpleName().toString();
}
try (Writer writer = processingEnv.getFiler().createSourceFile(packageName + "." + name, element).openWriter()) {
writer.append("package ").append(packageName).append(";");
writer.append("\npublic class ").append(name).append(" {}");
}
catch (IOException ignored) {
}
}
}
return false;
}
}
@@ -197,7 +197,7 @@ abstract class BaseGradleIT {
val parallelTasksInProject: Boolean? = null
)
data class KaptOptions(val verbose: Boolean, val useWorkers: Boolean)
data class KaptOptions(val verbose: Boolean, val useWorkers: Boolean, val incrementalKapt: Boolean = false, val includeCompileClasspath: Boolean = true)
open inner class Project(
val projectName: String,
@@ -619,6 +619,8 @@ abstract class BaseGradleIT {
options.kaptOptions?.also { kaptOptions ->
add("-Pkapt.verbose=${kaptOptions.verbose}")
add("-Pkapt.use.worker.api=${kaptOptions.useWorkers}")
add("-Pkapt.incremental.apt=${kaptOptions.incrementalKapt}")
add("-Pkapt.include.compile.classpath=${kaptOptions.includeCompileClasspath}")
}
options.parallelTasksInProject?.let {
@@ -6,12 +6,12 @@ import org.jetbrains.kotlin.gradle.util.getFileByName
import org.jetbrains.kotlin.gradle.util.modify
import org.junit.Test
class KaptIncrementalIT : BaseGradleIT() {
open class KaptIncrementalIT : BaseGradleIT() {
companion object {
private val EXAMPLE_ANNOTATION_REGEX = "@(field:)?example.ExampleAnnotation".toRegex()
}
private fun getProject() =
open fun getProject() =
Project(
"kaptIncrementalCompilationProject",
GradleVersionRequired.None
@@ -25,13 +25,13 @@ class KaptIncrementalIT : BaseGradleIT() {
@Test
fun testAddNewLine() {
val project = Project("simple", directoryPrefix = "kapt2")
val project = getProject()
project.build("clean", "build") {
assertSuccessful()
}
project.projectFile("test.kt").modify { "\n$it" }
project.projectFile("useB.kt").modify { "\n$it" }
project.build("build") {
assertSuccessful()
assertTasksExecuted(":kaptGenerateStubsKotlin", ":compileKotlin")
@@ -0,0 +1,86 @@
/*
* Copyright 2010-2019 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
import org.jetbrains.kotlin.gradle.util.modify
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class KaptIncrementalWithAggregatingApt : KaptIncrementalIT() {
override fun getProject() =
Project(
"kaptIncrementalCompilationProject",
GradleVersionRequired.None
).apply {
setupIncrementalAptProject("AGGREGATING")
}
override fun defaultBuildOptions(): BuildOptions =
super.defaultBuildOptions().copy(
incremental = true,
kaptOptions = KaptOptions(
verbose = true,
useWorkers = true,
incrementalKapt = true,
includeCompileClasspath = false
)
)
@Test
fun testIncrementalChanges() {
val project = getProject()
var aptTimestamp = 0L
project.build("clean", "build") {
assertSuccessful()
val classpathHistory =
fileInWorkingDir("build/kotlin/kaptGenerateStubsKotlin/classpath-fq-history").listFiles().asList().single()
val stubsTimestamp = classpathHistory.name.toLong()
aptTimestamp = fileInWorkingDir("build/tmp/kapt3/incApCache/main/last-build-ts.bin").readText().toLong()
assertTrue(stubsTimestamp < aptTimestamp)
}
project.projectFile("useB.kt").modify { current -> "$current\nfun otherFunction() {}" }
project.build("build") {
assertSuccessful()
val newAptTimestamp = fileInWorkingDir("build/tmp/kapt3/incApCache/main/last-build-ts.bin").readText().toLong()
assertTrue(aptTimestamp < newAptTimestamp)
assertEquals(
setOf(
fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/UseBKt.java").absolutePath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/B.java").absolutePath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/baz/UtilKt.java").absolutePath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/foo/A.java").absolutePath
), getProcessedSources(output)
)
}
project.projectFile("JavaClass.java").modify { current ->
val lastBrace = current.lastIndexOf("}")
current.substring(0, lastBrace) + "private void anotherFun() {}\n }"
}
project.build("build") {
assertSuccessful()
assertEquals(
setOf(
project.projectFile("JavaClass.java").absolutePath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/UseBKt.java").absolutePath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/B.java").absolutePath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/baz/UtilKt.java").absolutePath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/foo/A.java").absolutePath
),
getProcessedSources(output)
)
}
}
}
@@ -0,0 +1,113 @@
/*
* Copyright 2010-2019 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
import org.jetbrains.kotlin.gradle.incapt.IncrementalProcessor
import org.jetbrains.kotlin.gradle.util.modify
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class KaptIncrementalWithIsolatingApt : KaptIncrementalIT() {
override fun getProject() =
Project(
"kaptIncrementalCompilationProject",
GradleVersionRequired.None
).apply {
setupIncrementalAptProject("ISOLATING")
}
override fun defaultBuildOptions(): BuildOptions =
super.defaultBuildOptions().copy(
incremental = true,
kaptOptions = KaptOptions(
verbose = true,
useWorkers = true,
incrementalKapt = true,
includeCompileClasspath = false
)
)
@Test
fun testIncrementalChanges() {
val project = getProject()
var aptTimestamp = 0L
project.build("clean", "build") {
assertSuccessful()
val classpathHistory =
fileInWorkingDir("build/kotlin/kaptGenerateStubsKotlin/classpath-fq-history").listFiles().asList().single()
val stubsTimestamp = classpathHistory.name.toLong()
aptTimestamp = fileInWorkingDir("build/tmp/kapt3/incApCache/main/last-build-ts.bin").readText().toLong()
assertTrue(stubsTimestamp < aptTimestamp)
}
project.projectFile("useB.kt").modify { current -> "$current\nfun otherFunction() {}" }
project.build("build") {
assertSuccessful()
val newAptTimestamp = fileInWorkingDir("build/tmp/kapt3/incApCache/main/last-build-ts.bin").readText().toLong()
assertTrue(aptTimestamp < newAptTimestamp)
assertEquals(setOf(fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/UseBKt.java").absolutePath), getProcessedSources(output))
}
project.projectFile("B.kt").modify { current ->
val lastBrace = current.lastIndexOf("}")
current.substring(0, lastBrace) + "fun anotherFun() {}\n }"
}
project.build("build") {
assertSuccessful()
assertEquals(
setOf(
fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/B.java").absolutePath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/UseBKt.java").absolutePath
),
getProcessedSources(output)
)
}
}
}
private const val patternApt = "Processing java sources with annotation processors:"
fun getProcessedSources(output: String): Set<String> {
val logging = output.lines().single { it.contains(patternApt) }
val indexOf = logging.indexOf(patternApt) + patternApt.length
return logging.drop(indexOf).split(",").map { it.trim() }.toSet()
}
fun BaseGradleIT.Project.setupIncrementalAptProject(procType: String) {
setupWorkingDir()
val buildFile = projectDir.resolve("build.gradle")
val content = buildFile.readText()
val processorPath = projectDir.resolve("incrementalProcessor.jar")
ZipOutputStream(processorPath.outputStream()).use {
val path = IncrementalProcessor::class.java.name.replace(".", "/") + ".class"
val inputStream = IncrementalProcessor::class.java.classLoader.getResourceAsStream(path)
it.putNextEntry(ZipEntry(path))
it.write(inputStream.readBytes())
it.closeEntry()
it.putNextEntry(ZipEntry("META-INF/gradle/incremental.annotation.processors"))
it.write("${IncrementalProcessor::class.java.name},$procType".toByteArray())
it.closeEntry()
it.putNextEntry(ZipEntry("META-INF/services/javax.annotation.processing.Processor"))
it.write(IncrementalProcessor::class.java.name.toByteArray())
it.closeEntry()
}
val updatedContent = content.replace(
Regex("^\\s*kapt\\s\"org\\.jetbrain.*$", RegexOption.MULTILINE),
" kapt files(\"$processorPath\")"
)
buildFile.writeText(updatedContent)
}