Prevent compile tasks from running when nothing changes

#KT-10932 fixed
This commit is contained in:
Alexey Tsvetkov
2016-04-25 17:44:36 +03:00
parent 0bbd2f38dc
commit 93117ed637
8 changed files with 168 additions and 54 deletions
@@ -46,13 +46,13 @@ fun Project.initKapt(
val stubsDir = File(buildDir, "tmp/kapt/$variantName/classFileStubs")
kotlinTask.extensions.extraProperties.set("kaptStubsDir", stubsDir)
javaTask.appendClasspathDynamically(stubsDir)
javaTask.doFirst {
javaTask.classpath += files(stubsDir)
}
kotlinTask.doFirst {
javaTask.doLast {
kotlinAfterJavaTask.source(kotlinTask.source)
kotlinAfterJavaTask.source(kaptManager.javaAptSourceDir)
// we don't want kotlinAfterJavaTask to track modifications in generated class
kotlinAfterJavaTask.classpath -= project.files(javaTask.destinationDir)
}
subpluginEnvironment.addSubpluginArguments(this, kotlinAfterJavaTask)
@@ -61,6 +61,9 @@ fun Project.initKapt(
kotlinTask.logger.kotlinDebug("kapt: Class file stubs are not used")
}
javaTask.appendClasspathDynamically(kaptManager.wrappersDirectory)
javaTask.source(kaptManager.javaAptSourceDir)
if (kaptExtension.inheritedAnnotations) {
kotlinTask.extensions.extraProperties.set("kaptInheritedAnnotations", true)
}
@@ -70,13 +73,16 @@ fun Project.initKapt(
kotlinAfterJavaTask?.source(kaptManager.getGeneratedKotlinSourceDir())
}
var originalJavaCompilerArgs: List<String>? = null
javaTask.doFirst {
originalJavaCompilerArgs = (javaTask as JavaCompile).options.compilerArgs
kaptManager.setupKapt()
kaptManager.generateJavaHackFile()
kotlinAfterJavaTask?.source(kaptManager.getGeneratedKotlinSourceDir())
}
javaTask.doLast {
(javaTask as JavaCompile).options.compilerArgs = originalJavaCompilerArgs
kaptManager.afterJavaCompile()
}
@@ -92,8 +98,8 @@ private fun Project.createKotlinAfterJavaTask(
): AbstractCompile {
val kotlinAfterJavaTask = with (taskFactory(KOTLIN_AFTER_JAVA_TASK_SUFFIX)) {
kotlinDestinationDir = kotlinTask.kotlinDestinationDir
destinationDir = javaTask.destinationDir
classpath = javaTask.classpath
destinationDir = kotlinTask.destinationDir
classpath = kotlinTask.classpath - project.files(javaTask.destinationDir)
this
}
@@ -124,10 +130,11 @@ public class AnnotationProcessingManager(
private val project = task.project
private val random = Random()
val wrappersDirectory = File(aptWorkingDir, "wrappers")
val javaAptSourceDir = File(aptWorkingDir, "java_src")
private companion object {
val JAVA_FQNAME_PATTERN = "^([\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*$".toRegex()
val WRAPPERS_DIRECTORY = "wrappers"
val GEN_ANNOTATION = "__gen/annotation"
private val ANDROID_APT_PLUGIN_ID = "com.neenbedankt.android-apt"
@@ -135,7 +142,7 @@ public class AnnotationProcessingManager(
fun getAnnotationFile(): File {
if (!aptWorkingDir.exists()) aptWorkingDir.mkdirs()
return File(aptWorkingDir, "$WRAPPERS_DIRECTORY/annotations.$taskQualifier.txt")
return File(wrappersDirectory, "annotations.$taskQualifier.txt")
}
fun getGeneratedKotlinSourceDir(): File {
@@ -153,16 +160,13 @@ public class AnnotationProcessingManager(
val annotationProcessorFqNames = lookupAnnotationProcessors(aptFiles)
val stubOutputDir = File(aptWorkingDir, WRAPPERS_DIRECTORY)
generateAnnotationProcessorStubs(javaTask, annotationProcessorFqNames, stubOutputDir)
generateAnnotationProcessorStubs(javaTask, annotationProcessorFqNames, wrappersDirectory)
val processorPath = setOf(stubOutputDir) + aptFiles
val processorPath = setOf(wrappersDirectory) + aptFiles
setProcessorPath(javaTask, (processorPath + javaTask.classpath).joinToString(File.pathSeparator))
javaTask.appendClasspath(stubOutputDir)
addGeneratedSourcesOutputToCompilerArgs(javaTask, aptOutputDir)
appendAnnotationsArguments()
appendAdditionalComplerArgs()
}
@@ -177,7 +181,6 @@ public class AnnotationProcessingManager(
}
fun generateJavaHackFile() {
val javaAptSourceDir = File(aptWorkingDir, "java_src")
val javaHackPackageDir = File(javaAptSourceDir, GEN_ANNOTATION)
if (!javaHackPackageDir.exists()) javaHackPackageDir.mkdirs()
@@ -193,10 +196,6 @@ public class AnnotationProcessingManager(
project.logger.kotlinDebug("kapt: Java file stub generated: $javaHackClFile " +
"(previously existed: $previouslyExisted)")
if (!javaTask.source.contains(javaHackClFile)) {
javaTask.source(javaAptSourceDir)
}
}
private fun appendAnnotationsArguments() {
@@ -241,10 +240,6 @@ public class AnnotationProcessingManager(
addWrappersToCompilerArgs(javaTask, annotationProcessorWrapperFqNames)
}
private fun JavaCompile.appendClasspath(file: File) {
classpath += project.files(file)
}
private fun addWrappersToCompilerArgs(javaTask: JavaCompile, wrapperFqNames: String) {
javaTask.addCompilerArgument("-processor") { prevValue ->
if (prevValue != null) "$prevValue,$wrapperFqNames" else wrapperFqNames
@@ -134,7 +134,7 @@ class Kotlin2JvmSourceSetProcessor(
val javaTask = project.tasks.findByName(sourceSet.compileJavaTaskName) as AbstractCompile?
if (javaTask != null) {
setUpKotlinToJavaDependency(project, kotlinTask, javaTask, logger)
setUpKotlinToJavaDependency(kotlinTask, javaTask, logger)
}
val kotlinAnnotationProcessingDep = cachedKotlinAnnotationProcessingDep ?: run {
@@ -367,7 +367,6 @@ open class KotlinAndroidPlugin @Inject constructor(val scriptHandler: ScriptHand
kotlinTask.kotlinDestinationDir = File(project.buildDir, "tmp/kotlin-classes/$variantDataName")
kotlinTask.destinationDir = javaTask.destinationDir
kotlinTask.description = "Compiles the ${variantDataName} kotlin."
kotlinTask.classpath = javaTask.classpath
kotlinTask.setDependsOn(javaTask.dependsOn)
fun SourceDirectorySet.addSourceDirectories(additionalSourceFiles: Collection<File>) {
@@ -408,13 +407,17 @@ open class KotlinAndroidPlugin @Inject constructor(val scriptHandler: ScriptHand
subpluginEnvironment.addSubpluginArguments(project, kotlinTask)
kotlinTask.doFirst {
// should not be evaluated until right before compileKotlin evaluation since android can change
// java classpath during project evaluation (see prepareComAndroidSupportSupportV42311Library)
val fullClasspath = lazy {
val androidRT = project.files(AndroidGradleWrapper.getRuntimeJars(androidPlugin, androidExt))
val fullClasspath = (javaTask.classpath + androidRT) - project.files(kotlinTask.kotlinDestinationDir)
(it as AbstractCompile).classpath = fullClasspath
(javaTask.classpath + androidRT) - project.files(kotlinTask.kotlinDestinationDir)
}
kotlinTask.classpath = javaTask.classpath
kotlinTask.updateClasspathBeforeTask { fullClasspath.value }
kotlinTask.doFirst {
for (task in project.getTasksByName(kotlinTaskName + KOTLIN_AFTER_JAVA_TASK_SUFFIX, false)) {
(task as AbstractCompile).classpath = project.files(fullClasspath, javaTask.destinationDir)
(task as AbstractCompile).classpath = project.files(fullClasspath.value, javaTask.destinationDir)
}
}
@@ -432,7 +435,7 @@ open class KotlinAndroidPlugin @Inject constructor(val scriptHandler: ScriptHand
}
}
setUpKotlinToJavaDependency(project, kotlinTask, javaTask, logger)
setUpKotlinToJavaDependency(kotlinTask, javaTask, logger)
}
}
@@ -448,7 +451,7 @@ open class KotlinAndroidPlugin @Inject constructor(val scriptHandler: ScriptHand
}
}
private fun setUpKotlinToJavaDependency(project: Project, kotlinTask: AbstractCompile, javaTask: AbstractCompile, logger: Logger) {
private fun setUpKotlinToJavaDependency(kotlinTask: AbstractCompile, javaTask: AbstractCompile, logger: Logger) {
// Since we cannot update classpath statically, java not able to detect changes in the classpath after kotlin compiler.
// Therefore this (probably inefficient since java cannot decide "uptodateness" by the list of changed class files, but told
// explicitly being out of date whenever any kotlin files are compiled
@@ -465,20 +468,15 @@ private fun setUpKotlinToJavaDependency(project: Project, kotlinTask: AbstractCo
}
javaTask.dependsOn(kotlinTask.name)
javaTask.doFirst {
/*
* It's important to modify javaTask.classpath only in doFirst,
* because Android plugin uses ConventionMapping to modify it too (see JavaCompileConfigAction.execute),
* and setting classpath explicitly prevents usage of Android mappings.
* Also classpath setted by Android can be modified after excecution of some tasks (see VarianConfiguration.getCompileClasspath)
* ex. it adds some support libraries jars after execution of prepareComAndroidSupportSupportV42311Library task,
* so it's only safe to modify javaTask.classpath right before its usage
*/
javaTask.classpath += project.files(kotlinTask.property("kotlinDestinationDir"))
}
javaTask.doLast {
javaTask.classpath -= project.files(kotlinTask.property("kotlinDestinationDir"))
}
/*
* It's important to modify javaTask.classpath only in doFirst,
* because Android plugin uses ConventionMapping to modify it too (see JavaCompileConfigAction.execute),
* and setting classpath explicitly prevents usage of Android mappings.
* Also classpath setted by Android can be modified after excecution of some tasks (see VarianConfiguration.getCompileClasspath)
* ex. it adds some support libraries jars after execution of prepareComAndroidSupportSupportV42311Library task,
* so it's only safe to modify javaTask.classpath right before its usage
*/
javaTask.appendClasspathDynamically(kotlinTask.kotlinDestinationDir!!)
}
private fun loadSubplugins(project: Project): SubpluginEnvironment {
@@ -1,10 +1,56 @@
package org.jetbrains.kotlin.gradle.plugin
import org.gradle.api.file.FileCollection
import org.gradle.api.internal.AbstractTask
import org.gradle.api.tasks.compile.AbstractCompile
import java.io.File
import kotlin.reflect.KProperty
/*
* Adding fake up-to-date check to do something before execution, and before input snapshots are taken.
* Note, this hack is sensitive to up-to-date checks order, so it should be added before any custom up-to-date checks
* that could return false.
*
* Should be used only for compileKotlinTask.
*
* // todo: could add configureKotlinTask that always executes before compileKotlin instead
*/
internal fun AbstractCompile.updateClasspathBeforeTask(newClassPath: (oldClassPath: FileCollection) -> FileCollection) {
// Gradle can take inputs snapshots before task run (normally) or after task run (--rerun-tasks or some up-to-date when returned false).
// Fake up-to-date check to update classpath dynamically before inputs snapshot is taken.
// Won't be called in case of some other up-to-date check returned false before this one evaluation
// Won't be called in case of --rerun-tasks
var classpathIsUpdated = false
outputs.upToDateWhen {
classpath = newClassPath(classpath)
classpathIsUpdated = true
true
}
// in case fake up-to-date check was not called (see comment above)
doFirst {
if (!classpathIsUpdated) {
classpath = newClassPath(classpath)
}
}
}
internal fun AbstractCompile.appendClasspathDynamically(file: File) {
var added = false
doFirst {
if (file !in classpath) {
classpath += project.files(file)
added = true
}
}
doLast {
if (added) {
classpath -= project.files(file)
}
}
}
internal var AbstractTask.anyClassesCompiled: Boolean? by TaskPropertyDelegate("anyClassesCompiled")
internal var AbstractTask.kotlinDestinationDir: File? by TaskPropertyDelegate("kotlinDestinationDir")
@@ -86,6 +86,27 @@ fun getSomething() = 10
}
}
@Test
fun testIncrementalBuildWithNoChanges() {
val project = Project("AndroidIncrementalSingleModuleProject", gradleVersion)
val tasksToExecute = arrayOf(
":app:prepareComAndroidSupportAppcompatV72311Library",
":app:prepareComAndroidSupportSupportV42311Library",
":app:compileDebugKotlin",
":app:compileDebugJavaWithJavac"
)
project.build("assembleDebug") {
assertSuccessful()
assertContains(*tasksToExecute)
}
project.build("assembleDebug") {
assertSuccessful()
assertContains(*tasksToExecute.map { it + " UP-TO-DATE" }.toTypedArray())
}
}
@Test
fun testModuleNameAndroid() {
val project = Project("AndroidProject", gradleVersion)
@@ -3,6 +3,7 @@ package org.jetbrains.kotlin.gradle
import org.gradle.api.logging.LogLevel
import org.jetbrains.kotlin.gradle.plugin.CleanUpBuildListener
import org.jetbrains.kotlin.gradle.tasks.USING_EXPERIMENTAL_INCREMENTAL_MESSAGE
import org.jetbrains.kotlin.gradle.util.getFileByName
import org.junit.Test
import java.io.File
import kotlin.test.assertTrue
@@ -221,23 +222,49 @@ class KotlinGradleIT: BaseGradleIT() {
}
}
@Test
fun testKaptSimpleIncrementalBuild() {
doTestKaptIncrementalBuild("kaptSimple", arrayOf(":compileKotlin", ":compileJava"))
}
@Test
fun testKaptStubsIncrementalBuild() {
val project = Project("kaptStubs", GRADLE_VERSION)
doTestKaptIncrementalBuild("kaptStubs", arrayOf(":compileKotlin", ":compileJava", ":compileKotlinAfterJava"))
}
private fun doTestKaptIncrementalBuild(projectName: String, compileTasks: Array<String>) {
val compileTasksUpToDate = compileTasks.map { it + " UP-TO-DATE" }.toTypedArray()
val project = Project(projectName, GRADLE_VERSION)
project.build("build") {
assertSuccessful()
// Modify the Kotlin source file somehow
val someJavaFile = fileInWorkingDir("src/main/java/test.kt")
someJavaFile.appendText(" ")
}
project.projectDir.getFileByName("test.kt").appendText(" ")
project.build("build") {
assertSuccessful()
assertContains(":compileKotlin")
assertContains(":compileJava")
assertNotContains(":compileJava UP-TO-DATE")
assertContains(*compileTasks)
assertNotContains(*compileTasksUpToDate)
}
repeat(2) {
project.build("build") {
assertSuccessful()
assertContains(*compileTasksUpToDate)
}
}
project.build("clean", "build") {
assertSuccessful()
assertContains(*compileTasks)
assertNotContains(*compileTasksUpToDate)
}
repeat(2) {
project.build("build") {
assertSuccessful()
assertContains(*compileTasksUpToDate)
}
}
}
@@ -50,5 +50,6 @@ repositories {
}
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile "org.jetbrains.kotlin:kotlin-stdlib:1.1-SNAPSHOT"
}
@@ -0,0 +1,25 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package javaPackage;
public class JavaClass {
public void test() {
System.out.println(example.TestClassGenerated.class.getName());
}
}