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") val stubsDir = File(buildDir, "tmp/kapt/$variantName/classFileStubs")
kotlinTask.extensions.extraProperties.set("kaptStubsDir", stubsDir) kotlinTask.extensions.extraProperties.set("kaptStubsDir", stubsDir)
javaTask.appendClasspathDynamically(stubsDir)
javaTask.doFirst { javaTask.doLast {
javaTask.classpath += files(stubsDir)
}
kotlinTask.doFirst {
kotlinAfterJavaTask.source(kotlinTask.source) 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) subpluginEnvironment.addSubpluginArguments(this, kotlinAfterJavaTask)
@@ -61,6 +61,9 @@ fun Project.initKapt(
kotlinTask.logger.kotlinDebug("kapt: Class file stubs are not used") kotlinTask.logger.kotlinDebug("kapt: Class file stubs are not used")
} }
javaTask.appendClasspathDynamically(kaptManager.wrappersDirectory)
javaTask.source(kaptManager.javaAptSourceDir)
if (kaptExtension.inheritedAnnotations) { if (kaptExtension.inheritedAnnotations) {
kotlinTask.extensions.extraProperties.set("kaptInheritedAnnotations", true) kotlinTask.extensions.extraProperties.set("kaptInheritedAnnotations", true)
} }
@@ -70,13 +73,16 @@ fun Project.initKapt(
kotlinAfterJavaTask?.source(kaptManager.getGeneratedKotlinSourceDir()) kotlinAfterJavaTask?.source(kaptManager.getGeneratedKotlinSourceDir())
} }
var originalJavaCompilerArgs: List<String>? = null
javaTask.doFirst { javaTask.doFirst {
originalJavaCompilerArgs = (javaTask as JavaCompile).options.compilerArgs
kaptManager.setupKapt() kaptManager.setupKapt()
kaptManager.generateJavaHackFile() kaptManager.generateJavaHackFile()
kotlinAfterJavaTask?.source(kaptManager.getGeneratedKotlinSourceDir()) kotlinAfterJavaTask?.source(kaptManager.getGeneratedKotlinSourceDir())
} }
javaTask.doLast { javaTask.doLast {
(javaTask as JavaCompile).options.compilerArgs = originalJavaCompilerArgs
kaptManager.afterJavaCompile() kaptManager.afterJavaCompile()
} }
@@ -92,8 +98,8 @@ private fun Project.createKotlinAfterJavaTask(
): AbstractCompile { ): AbstractCompile {
val kotlinAfterJavaTask = with (taskFactory(KOTLIN_AFTER_JAVA_TASK_SUFFIX)) { val kotlinAfterJavaTask = with (taskFactory(KOTLIN_AFTER_JAVA_TASK_SUFFIX)) {
kotlinDestinationDir = kotlinTask.kotlinDestinationDir kotlinDestinationDir = kotlinTask.kotlinDestinationDir
destinationDir = javaTask.destinationDir destinationDir = kotlinTask.destinationDir
classpath = javaTask.classpath classpath = kotlinTask.classpath - project.files(javaTask.destinationDir)
this this
} }
@@ -124,10 +130,11 @@ public class AnnotationProcessingManager(
private val project = task.project private val project = task.project
private val random = Random() private val random = Random()
val wrappersDirectory = File(aptWorkingDir, "wrappers")
val javaAptSourceDir = File(aptWorkingDir, "java_src")
private companion object { private companion object {
val JAVA_FQNAME_PATTERN = "^([\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*$".toRegex() 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" val GEN_ANNOTATION = "__gen/annotation"
private val ANDROID_APT_PLUGIN_ID = "com.neenbedankt.android-apt" private val ANDROID_APT_PLUGIN_ID = "com.neenbedankt.android-apt"
@@ -135,7 +142,7 @@ public class AnnotationProcessingManager(
fun getAnnotationFile(): File { fun getAnnotationFile(): File {
if (!aptWorkingDir.exists()) aptWorkingDir.mkdirs() if (!aptWorkingDir.exists()) aptWorkingDir.mkdirs()
return File(aptWorkingDir, "$WRAPPERS_DIRECTORY/annotations.$taskQualifier.txt") return File(wrappersDirectory, "annotations.$taskQualifier.txt")
} }
fun getGeneratedKotlinSourceDir(): File { fun getGeneratedKotlinSourceDir(): File {
@@ -153,16 +160,13 @@ public class AnnotationProcessingManager(
val annotationProcessorFqNames = lookupAnnotationProcessors(aptFiles) val annotationProcessorFqNames = lookupAnnotationProcessors(aptFiles)
val stubOutputDir = File(aptWorkingDir, WRAPPERS_DIRECTORY) generateAnnotationProcessorStubs(javaTask, annotationProcessorFqNames, wrappersDirectory)
generateAnnotationProcessorStubs(javaTask, annotationProcessorFqNames, stubOutputDir)
val processorPath = setOf(stubOutputDir) + aptFiles val processorPath = setOf(wrappersDirectory) + aptFiles
setProcessorPath(javaTask, (processorPath + javaTask.classpath).joinToString(File.pathSeparator)) setProcessorPath(javaTask, (processorPath + javaTask.classpath).joinToString(File.pathSeparator))
javaTask.appendClasspath(stubOutputDir)
addGeneratedSourcesOutputToCompilerArgs(javaTask, aptOutputDir) addGeneratedSourcesOutputToCompilerArgs(javaTask, aptOutputDir)
appendAnnotationsArguments() appendAnnotationsArguments()
appendAdditionalComplerArgs() appendAdditionalComplerArgs()
} }
@@ -177,7 +181,6 @@ public class AnnotationProcessingManager(
} }
fun generateJavaHackFile() { fun generateJavaHackFile() {
val javaAptSourceDir = File(aptWorkingDir, "java_src")
val javaHackPackageDir = File(javaAptSourceDir, GEN_ANNOTATION) val javaHackPackageDir = File(javaAptSourceDir, GEN_ANNOTATION)
if (!javaHackPackageDir.exists()) javaHackPackageDir.mkdirs() if (!javaHackPackageDir.exists()) javaHackPackageDir.mkdirs()
@@ -193,10 +196,6 @@ public class AnnotationProcessingManager(
project.logger.kotlinDebug("kapt: Java file stub generated: $javaHackClFile " + project.logger.kotlinDebug("kapt: Java file stub generated: $javaHackClFile " +
"(previously existed: $previouslyExisted)") "(previously existed: $previouslyExisted)")
if (!javaTask.source.contains(javaHackClFile)) {
javaTask.source(javaAptSourceDir)
}
} }
private fun appendAnnotationsArguments() { private fun appendAnnotationsArguments() {
@@ -241,10 +240,6 @@ public class AnnotationProcessingManager(
addWrappersToCompilerArgs(javaTask, annotationProcessorWrapperFqNames) addWrappersToCompilerArgs(javaTask, annotationProcessorWrapperFqNames)
} }
private fun JavaCompile.appendClasspath(file: File) {
classpath += project.files(file)
}
private fun addWrappersToCompilerArgs(javaTask: JavaCompile, wrapperFqNames: String) { private fun addWrappersToCompilerArgs(javaTask: JavaCompile, wrapperFqNames: String) {
javaTask.addCompilerArgument("-processor") { prevValue -> javaTask.addCompilerArgument("-processor") { prevValue ->
if (prevValue != null) "$prevValue,$wrapperFqNames" else wrapperFqNames if (prevValue != null) "$prevValue,$wrapperFqNames" else wrapperFqNames
@@ -134,7 +134,7 @@ class Kotlin2JvmSourceSetProcessor(
val javaTask = project.tasks.findByName(sourceSet.compileJavaTaskName) as AbstractCompile? val javaTask = project.tasks.findByName(sourceSet.compileJavaTaskName) as AbstractCompile?
if (javaTask != null) { if (javaTask != null) {
setUpKotlinToJavaDependency(project, kotlinTask, javaTask, logger) setUpKotlinToJavaDependency(kotlinTask, javaTask, logger)
} }
val kotlinAnnotationProcessingDep = cachedKotlinAnnotationProcessingDep ?: run { 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.kotlinDestinationDir = File(project.buildDir, "tmp/kotlin-classes/$variantDataName")
kotlinTask.destinationDir = javaTask.destinationDir kotlinTask.destinationDir = javaTask.destinationDir
kotlinTask.description = "Compiles the ${variantDataName} kotlin." kotlinTask.description = "Compiles the ${variantDataName} kotlin."
kotlinTask.classpath = javaTask.classpath
kotlinTask.setDependsOn(javaTask.dependsOn) kotlinTask.setDependsOn(javaTask.dependsOn)
fun SourceDirectorySet.addSourceDirectories(additionalSourceFiles: Collection<File>) { fun SourceDirectorySet.addSourceDirectories(additionalSourceFiles: Collection<File>) {
@@ -408,13 +407,17 @@ open class KotlinAndroidPlugin @Inject constructor(val scriptHandler: ScriptHand
subpluginEnvironment.addSubpluginArguments(project, kotlinTask) 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 androidRT = project.files(AndroidGradleWrapper.getRuntimeJars(androidPlugin, androidExt))
val fullClasspath = (javaTask.classpath + androidRT) - project.files(kotlinTask.kotlinDestinationDir) (javaTask.classpath + androidRT) - project.files(kotlinTask.kotlinDestinationDir)
(it as AbstractCompile).classpath = fullClasspath }
kotlinTask.classpath = javaTask.classpath
kotlinTask.updateClasspathBeforeTask { fullClasspath.value }
kotlinTask.doFirst {
for (task in project.getTasksByName(kotlinTaskName + KOTLIN_AFTER_JAVA_TASK_SUFFIX, false)) { 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. // 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 // 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 // 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.dependsOn(kotlinTask.name)
javaTask.doFirst { /*
/* * It's important to modify javaTask.classpath only in doFirst,
* It's important to modify javaTask.classpath only in doFirst, * because Android plugin uses ConventionMapping to modify it too (see JavaCompileConfigAction.execute),
* because Android plugin uses ConventionMapping to modify it too (see JavaCompileConfigAction.execute), * and setting classpath explicitly prevents usage of Android mappings.
* 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)
* 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,
* 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
* so it's only safe to modify javaTask.classpath right before its usage */
*/ javaTask.appendClasspathDynamically(kotlinTask.kotlinDestinationDir!!)
javaTask.classpath += project.files(kotlinTask.property("kotlinDestinationDir"))
}
javaTask.doLast {
javaTask.classpath -= project.files(kotlinTask.property("kotlinDestinationDir"))
}
} }
private fun loadSubplugins(project: Project): SubpluginEnvironment { private fun loadSubplugins(project: Project): SubpluginEnvironment {
@@ -1,10 +1,56 @@
package org.jetbrains.kotlin.gradle.plugin package org.jetbrains.kotlin.gradle.plugin
import org.gradle.api.file.FileCollection
import org.gradle.api.internal.AbstractTask import org.gradle.api.internal.AbstractTask
import org.gradle.api.tasks.compile.AbstractCompile import org.gradle.api.tasks.compile.AbstractCompile
import java.io.File import java.io.File
import kotlin.reflect.KProperty 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.anyClassesCompiled: Boolean? by TaskPropertyDelegate("anyClassesCompiled")
internal var AbstractTask.kotlinDestinationDir: File? by TaskPropertyDelegate("kotlinDestinationDir") 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 @Test
fun testModuleNameAndroid() { fun testModuleNameAndroid() {
val project = Project("AndroidProject", gradleVersion) val project = Project("AndroidProject", gradleVersion)
@@ -3,6 +3,7 @@ package org.jetbrains.kotlin.gradle
import org.gradle.api.logging.LogLevel import org.gradle.api.logging.LogLevel
import org.jetbrains.kotlin.gradle.plugin.CleanUpBuildListener import org.jetbrains.kotlin.gradle.plugin.CleanUpBuildListener
import org.jetbrains.kotlin.gradle.tasks.USING_EXPERIMENTAL_INCREMENTAL_MESSAGE import org.jetbrains.kotlin.gradle.tasks.USING_EXPERIMENTAL_INCREMENTAL_MESSAGE
import org.jetbrains.kotlin.gradle.util.getFileByName
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -221,23 +222,49 @@ class KotlinGradleIT: BaseGradleIT() {
} }
} }
@Test
fun testKaptSimpleIncrementalBuild() {
doTestKaptIncrementalBuild("kaptSimple", arrayOf(":compileKotlin", ":compileJava"))
}
@Test @Test
fun testKaptStubsIncrementalBuild() { 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") { project.build("build") {
assertSuccessful() 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") { project.build("build") {
assertSuccessful() assertSuccessful()
assertContains(":compileKotlin") assertContains(*compileTasks)
assertContains(":compileJava") assertNotContains(*compileTasksUpToDate)
assertNotContains(":compileJava UP-TO-DATE") }
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 { dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile "org.jetbrains.kotlin:kotlin-stdlib:1.1-SNAPSHOT" 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());
}
}