Restore outputs when Kotlin task fails

#KT-12295 Fixed

Gradle forces non-incremetnal builds,
when a task failed, but its outputs were changed.
This is an issue for Kotlin incremental compilation,
because it also forces all dependent modules to be rebuilt.
This change fixes the issue by reading all output and
restoring it on a compile error/exception.
This change does not affect non-incremental builds.
This commit is contained in:
Alexey Tsvetkov
2019-01-17 00:12:15 +03:00
parent 467090bd9e
commit 7dcdba7e1a
6 changed files with 116 additions and 27 deletions
@@ -451,6 +451,16 @@ abstract class BaseGradleIT {
}
}
fun CompiledProject.assertTasksFailed(vararg tasks: String) {
assertTasksFailed(tasks.toList())
}
fun CompiledProject.assertTasksFailed(tasks: Iterable<String>) {
for (task in tasks) {
assertContains("$task FAILED")
}
}
fun CompiledProject.assertTasksUpToDate(vararg tasks: String) {
assertTasksUpToDate(tasks.toList())
}
@@ -27,12 +27,18 @@ class IncrementalCompilationJsMultiProjectIT : BaseIncrementalCompilationMultiPr
override val additionalLibDependencies: String =
"implementation \"org.jetbrains.kotlin:kotlin-test-js:${'$'}kotlin_version\""
override val compileKotlinTaskName: String
get() = "compileKotlin2Js"
}
class IncrementalCompilationJvmMultiProjectIT : BaseIncrementalCompilationMultiProjectIT() {
override val additionalLibDependencies: String =
"implementation \"org.jetbrains.kotlin:kotlin-stdlib:${'$'}kotlin_version\""
override val compileKotlinTaskName: String
get() = "compileKotlin"
override fun defaultProject(): Project =
Project("incrementalMultiproject")
@@ -203,6 +209,7 @@ open class A {
}
protected abstract val additionalLibDependencies: String
protected abstract val compileKotlinTaskName: String
@Test
fun testAddDependencyToLib() {
@@ -251,20 +258,31 @@ open class A {
}
val bKt = project.projectDir.getFileByName("B.kt")
val bKtContent = bKt.readText()
bKt.delete()
project.build("build") {
assertFailed()
fun runFailingBuild() {
project.build("build") {
assertFailed()
assertContains("B.kt has been removed")
assertTasksFailed(":lib:$compileKotlinTaskName")
val affectedFiles = project.projectDir.getFilesByNames("barUseAB.kt", "barUseB.kt")
assertCompiledKotlinSources(project.relativize(affectedFiles))
}
}
project.projectDir.getFileByName("barUseB.kt").delete()
project.projectDir.getFileByName("barUseAB.kt").delete()
runFailingBuild()
runFailingBuild()
bKt.writeText(bKtContent.replace("fun b", "open fun b"))
project.build("build") {
assertFailed()
val affectedSources = project.projectDir.allKotlinFiles()
val relativePaths = project.relativize(affectedSources)
assertCompiledKotlinSources(relativePaths)
assertSuccessful()
val affectedFiles = project.projectDir.getFilesByNames(
"B.kt", "barUseAB.kt", "barUseB.kt",
"BB.kt", "fooUseB.kt"
)
assertCompiledKotlinSources(project.relativize(affectedFiles))
}
}
}
@@ -71,6 +71,32 @@ class KaptIncrementalIT : BaseGradleIT() {
}
}
@Test
fun testCompileError() {
val project = getProject()
project.build("build") {
assertSuccessful()
}
val bKt = project.projectDir.getFileByName("B.kt")
val errorKt = bKt.resolveSibling("error.kt")
errorKt.writeText("<COMPILE_ERROR_MARKER>")
project.build("build") {
assertFailed()
assertTasksFailed(":kaptGenerateStubsKotlin")
}
errorKt.delete()
bKt.modify { "$it\n" }
project.build("build") {
assertSuccessful()
assertCompiledKotlinSources(project.relativize(bKt))
assertTasksExecuted(":kaptGenerateStubsKotlin", ":compileKotlin")
}
}
@Test
fun testChangeFunctionBodyWithoutChangingSignature() {
val project = getProject()
@@ -91,30 +91,17 @@ internal class GradleKotlinCompilerWork @Inject constructor(
get() = incrementalCompilationEnvironment != null
override fun run() {
if (!isIncremental) {
clearLocalStateDirectories(log, localStateDirectories, "IC is disabled")
}
val messageCollector = GradlePrintingMessageCollector(log)
val exitCode = try {
compileWithDaemonOrFallbackImpl(messageCollector)
} catch (e: Throwable) {
clearLocalStateDirectories(log, localStateDirectories, "exception when running compiler")
throw e
} finally {
if (buildFile != null && System.getProperty(DELETE_MODULE_FILE_PROPERTY) != "false") {
buildFile.delete()
}
}
if (incrementalCompilationEnvironment != null) {
if (incrementalCompilationEnvironment.disableMultiModuleIC) {
incrementalCompilationEnvironment.multiModuleICSettings.buildHistoryFile.delete()
}
if (exitCode != ExitCode.OK) {
clearLocalStateDirectories(log, localStateDirectories, "exit code: $exitCode")
}
if (incrementalCompilationEnvironment?.disableMultiModuleIC == true) {
incrementalCompilationEnvironment.multiModuleICSettings.buildHistoryFile.delete()
}
throwGradleExceptionIfError(exitCode)
@@ -20,6 +20,7 @@ import org.jetbrains.kotlin.compilerRunner.OutputItemsCollectorImpl
import org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME
import org.jetbrains.kotlin.gradle.tasks.CompilerPluginOptions
import org.jetbrains.kotlin.gradle.logging.GradlePrintingMessageCollector
import org.jetbrains.kotlin.gradle.tasks.clearLocalStateDirectories
import org.jetbrains.kotlin.gradle.tasks.localStateDirectories
import org.jetbrains.kotlin.gradle.utils.toSortedPathsArray
@@ -55,6 +56,7 @@ open class KaptWithKotlincTask : KaptTask(), CompilerArgumentAwareWithInput<K2JV
fun compile() {
logger.debug("Running kapt annotation processing using the Kotlin compiler")
checkAnnotationProcessorClasspath()
clearLocalStateDirectories()
val args = prepareCompilerArguments()
@@ -30,14 +30,12 @@ import org.jetbrains.kotlin.gradle.logging.GradlePrintingMessageCollector
import org.jetbrains.kotlin.gradle.logging.kotlinDebug
import org.jetbrains.kotlin.gradle.logging.kotlinWarn
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.utils.ParsedGradleVersion
import org.jetbrains.kotlin.gradle.utils.isParentOf
import org.jetbrains.kotlin.gradle.utils.pathsAsStringRelativeTo
import org.jetbrains.kotlin.gradle.utils.toSortedPathsArray
import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.incremental.ChangedFiles
import org.jetbrains.kotlin.incremental.classpathAsList
import org.jetbrains.kotlin.incremental.destinationAsFile
import org.jetbrains.kotlin.utils.LibraryUtils
import org.jetbrains.kotlin.utils.keysToMap
import java.io.File
import java.util.*
import javax.inject.Inject
@@ -257,6 +255,54 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments>() : AbstractKo
@TaskAction
fun execute(inputs: IncrementalTaskInputs) {
// If task throws exception, but its outputs are changed during execution,
// then Gradle forces next build to be non-incremental (see Gradle's DefaultTaskArtifactStateRepository#persistNewOutputs)
// To prevent this, we backup outputs before incremental build and restore when exception is thrown
val prevOutputs: Map<File, ByteArray>? = if (incremental && inputs.isIncremental) {
val outputFiles = HashSet<File>()
outputsCompatible.files.forEach {
if (it.isDirectory) {
it.walk().filterTo(outputFiles, File::isFile)
} else if (it.isFile) {
outputFiles.add(it)
}
}
outputFiles.keysToMap { it.readBytes() }
} else null
if (!incremental) {
clearLocalStateDirectories("IC is disabled")
}
try {
executeImpl(inputs)
} catch (t: Throwable) {
outputsCompatible.files.forEach {
if (it.isDirectory) {
it.deleteRecursively()
} else if (it.isFile) {
it.delete()
}
}
if (prevOutputs != null) {
val dirs = HashSet<File>()
for ((file, bytes) in prevOutputs) {
val dir = file.parentFile
if (dirs.add(dir)) {
dir.mkdirs()
}
file.writeBytes(bytes)
}
}
throw t
}
}
private fun executeImpl(inputs: IncrementalTaskInputs) {
// Check that the JDK tools are available in Gradle (fail-fast, instead of a fail during the compiler run):
findToolsJar()