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:
+10
@@ -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())
|
||||
}
|
||||
|
||||
+26
-8
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -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()
|
||||
|
||||
+2
-15
@@ -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)
|
||||
|
||||
+2
@@ -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()
|
||||
|
||||
|
||||
+50
-4
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user