[Gradle, BT] Add support for outputs backup within build tools API compilation

#KT-61896 Fixed
This commit is contained in:
Alexander.Likhachev
2023-09-13 06:06:19 +02:00
committed by Space Team
parent b460c06907
commit 3adb202eb6
5 changed files with 109 additions and 15 deletions
@@ -13,6 +13,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
import org.jetbrains.kotlin.gradle.tasks.USING_JVM_INCREMENTAL_COMPILATION_MESSAGE import org.jetbrains.kotlin.gradle.tasks.USING_JVM_INCREMENTAL_COMPILATION_MESSAGE
import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.testbase.*
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import kotlin.io.path.deleteExisting
import kotlin.io.path.writeText
@DisplayName("JVM compilation via the Build Tools API") @DisplayName("JVM compilation via the Build Tools API")
@JvmGradlePluginTests @JvmGradlePluginTests
@@ -55,6 +57,35 @@ class BuildToolsApiJvmCompilationIT : KGPBaseTest() {
} }
} }
@GradleTest
@DisplayName("Gradle side outputs backup works")
fun outputsBackupWorks(gradleVersion: GradleVersion) {
project(
"simpleProject", gradleVersion, buildOptions = defaultBuildOptions.copy(
usePreciseOutputsBackup = false,
keepIncrementalCompilationCachesInMemory = false,
)
) {
build("compileKotlin") {
assertTasksExecuted(":compileKotlin")
}
val newBrokenSrc = kotlinSourcesDir().resolve("broken.kt")
newBrokenSrc.writeText(
//language=kt
"""
broken code
""".trimIndent()
)
buildAndFail("compileKotlin") {
assertTasksFailed(":compileKotlin")
}
newBrokenSrc.deleteExisting()
build("compileKotlin") {
assertTasksUpToDate(":compileKotlin")
}
}
}
@GradleTest @GradleTest
@DisplayName("Simple project non-incremental in-process compilation") @DisplayName("Simple project non-incremental in-process compilation")
fun compileJvmInProcessNonIncremental(gradleVersion: GradleVersion) = testSimpleProject( fun compileJvmInProcessNonIncremental(gradleVersion: GradleVersion) = testSimpleProject(
@@ -15,6 +15,7 @@ import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkQueue import org.gradle.workers.WorkQueue
import org.gradle.workers.WorkerExecutor import org.gradle.workers.WorkerExecutor
import org.jetbrains.kotlin.build.report.metrics.* import org.jetbrains.kotlin.build.report.metrics.*
import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger
import org.jetbrains.kotlin.gradle.tasks.* import org.jetbrains.kotlin.gradle.tasks.*
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@@ -27,11 +28,11 @@ internal class GradleCompilerRunnerWithWorkers(
jdkToolsJar: File?, jdkToolsJar: File?,
compilerExecutionSettings: CompilerExecutionSettings, compilerExecutionSettings: CompilerExecutionSettings,
buildMetrics: BuildMetricsReporter<GradleBuildTime, GradleBuildPerformanceMetric>, buildMetrics: BuildMetricsReporter<GradleBuildTime, GradleBuildPerformanceMetric>,
private val workerExecutor: WorkerExecutor private val workerExecutor: WorkerExecutor,
) : GradleCompilerRunner(taskProvider, jdkToolsJar, compilerExecutionSettings, buildMetrics) { ) : GradleCompilerRunner(taskProvider, jdkToolsJar, compilerExecutionSettings, buildMetrics) {
override fun runCompilerAsync( override fun runCompilerAsync(
workArgs: GradleKotlinCompilerWorkArguments, workArgs: GradleKotlinCompilerWorkArguments,
taskOutputsBackup: TaskOutputsBackup? taskOutputsBackup: TaskOutputsBackup?,
): WorkQueue { ): WorkQueue {
buildMetrics.addTimeMetric(GradleBuildPerformanceMetric.CALL_WORKER) buildMetrics.addTimeMetric(GradleBuildPerformanceMetric.CALL_WORKER)
@@ -49,10 +50,10 @@ internal class GradleCompilerRunnerWithWorkers(
} }
internal abstract class GradleKotlinCompilerWorkAction @Inject constructor( internal abstract class GradleKotlinCompilerWorkAction @Inject constructor(
private val fileSystemOperations: FileSystemOperations private val fileSystemOperations: FileSystemOperations,
) : WorkAction<GradleKotlinCompilerWorkParameters> { ) : WorkAction<GradleKotlinCompilerWorkParameters> {
private val logger = Logging.getLogger("kotlin-compile-worker") private val logger = GradleKotlinLogger(Logging.getLogger("kotlin-compile-worker"))
override fun execute() { override fun execute() {
val taskOutputsBackup = if (parameters.snapshotsDir.isPresent) { val taskOutputsBackup = if (parameters.snapshotsDir.isPresent) {
@@ -61,7 +62,7 @@ internal class GradleCompilerRunnerWithWorkers(
parameters.buildDir, parameters.buildDir,
parameters.snapshotsDir, parameters.snapshotsDir,
parameters.taskOutputsToRestore.get(), parameters.taskOutputsToRestore.get(),
logger logger,
) )
} else { } else {
null null
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.compilerRunner.btapi package org.jetbrains.kotlin.compilerRunner.btapi
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.provider.ListProperty import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.workers.WorkAction import org.gradle.workers.WorkAction
@@ -25,13 +26,21 @@ import org.jetbrains.kotlin.gradle.logging.SL4JKotlinLogger
import org.jetbrains.kotlin.gradle.plugin.BuildFinishedListenerService import org.jetbrains.kotlin.gradle.plugin.BuildFinishedListenerService
import org.jetbrains.kotlin.gradle.plugin.internal.BuildIdService import org.jetbrains.kotlin.gradle.plugin.internal.BuildIdService
import org.jetbrains.kotlin.gradle.plugin.internal.state.TaskLoggers import org.jetbrains.kotlin.gradle.plugin.internal.state.TaskLoggers
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy import org.jetbrains.kotlin.gradle.tasks.*
import org.jetbrains.kotlin.gradle.tasks.throwExceptionIfCompilationFailed import org.jetbrains.kotlin.gradle.tasks.CompilationErrorException
import org.jetbrains.kotlin.gradle.tasks.FailedCompilationException
import org.jetbrains.kotlin.gradle.tasks.OOMErrorException
import org.jetbrains.kotlin.gradle.tasks.TaskOutputsBackup
import org.jetbrains.kotlin.incremental.ClasspathChanges import org.jetbrains.kotlin.incremental.ClasspathChanges
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.rmi.RemoteException
import javax.inject.Inject
internal abstract class BuildToolsApiCompilationWork : WorkAction<BuildToolsApiCompilationWork.BuildToolsApiCompilationParameters> { internal abstract class BuildToolsApiCompilationWork @Inject constructor(
private val fileSystemOperations: FileSystemOperations,
) :
WorkAction<BuildToolsApiCompilationWork.BuildToolsApiCompilationParameters> {
internal interface BuildToolsApiCompilationParameters : WorkParameters { internal interface BuildToolsApiCompilationParameters : WorkParameters {
val buildIdService: Property<BuildIdService> val buildIdService: Property<BuildIdService>
val buildFinishedListenerService: Property<BuildFinishedListenerService> val buildFinishedListenerService: Property<BuildFinishedListenerService>
@@ -63,7 +72,7 @@ internal abstract class BuildToolsApiCompilationWork : WorkAction<BuildToolsApiC
} }
} }
override fun execute() { private fun performCompilation(): CompilationResult {
val executionStrategy = workArguments.compilerExecutionSettings.strategy val executionStrategy = workArguments.compilerExecutionSettings.strategy
try { try {
val classLoader = parameters.classLoadersCachingService.get() val classLoader = parameters.classLoadersCachingService.get()
@@ -75,7 +84,9 @@ internal abstract class BuildToolsApiCompilationWork : WorkAction<BuildToolsApiC
} }
val executionConfig = compilationService.makeCompilerExecutionStrategyConfiguration().apply { val executionConfig = compilationService.makeCompilerExecutionStrategyConfiguration().apply {
when (executionStrategy) { when (executionStrategy) {
KotlinCompilerExecutionStrategy.DAEMON -> useDaemonStrategy(workArguments.compilerExecutionSettings.daemonJvmArgs ?: emptyList()) KotlinCompilerExecutionStrategy.DAEMON -> useDaemonStrategy(
workArguments.compilerExecutionSettings.daemonJvmArgs ?: emptyList()
)
KotlinCompilerExecutionStrategy.IN_PROCESS -> useInProcessStrategy() KotlinCompilerExecutionStrategy.IN_PROCESS -> useInProcessStrategy()
else -> error("The \"$executionStrategy\" execution strategy is not supported by the Build Tools API") else -> error("The \"$executionStrategy\" execution strategy is not supported by the Build Tools API")
} }
@@ -110,19 +121,70 @@ internal abstract class BuildToolsApiCompilationWork : WorkAction<BuildToolsApiC
classpathSnapshotsConfig, classpathSnapshotsConfig,
) )
} }
val result = compilationService.compileJvm( return compilationService.compileJvm(
buildId, buildId,
executionConfig, executionConfig,
jvmCompilationConfig, jvmCompilationConfig,
emptyList(), emptyList(),
workArguments.compilerArgs.toList(), workArguments.compilerArgs.toList(),
) )
throwExceptionIfCompilationFailed(result.asExitCode, executionStrategy) } catch (e: Throwable) {
if (e is OutOfMemoryError || e.hasOOMCause()) {
val helpMessage = when (executionStrategy) {
KotlinCompilerExecutionStrategy.DAEMON -> kotlinDaemonOOMHelperMessage
KotlinCompilerExecutionStrategy.IN_PROCESS -> kotlinInProcessOOMHelperMessage
else -> error("The \"$executionStrategy\" execution strategy is not supported by the Build Tools API")
}
throw OOMErrorException(helpMessage)
} else if (e is RemoteException) {
throw DaemonCrashedException(e)
} else {
throw e
}
} finally { } finally {
log.info(executionStrategy.asFinishLogMessage) log.info(executionStrategy.asFinishLogMessage)
} }
} }
// the files are backed up in the task action before any changes to the outputs
private fun initializeBackup(): TaskOutputsBackup? = if (parameters.snapshotsDir.isPresent) {
TaskOutputsBackup(
fileSystemOperations,
parameters.buildDir,
parameters.snapshotsDir,
parameters.taskOutputsToRestore.get(),
log,
)
} else {
null
}
override fun execute() {
val backup = initializeBackup()
val executionStrategy = workArguments.compilerExecutionSettings.strategy
try {
val result = performCompilation()
if (result == CompilationResult.COMPILATION_OOM_ERROR || result == CompilationResult.COMPILATION_ERROR) {
backup?.restoreOutputs()
}
throwExceptionIfCompilationFailed(result.asExitCode, executionStrategy)
} catch (e: FailedCompilationException) {
// Restore outputs only in cases where we expect that the user will make some changes to their project:
// - For a compilation error, the user will need to fix their source code
// - For an OOM error, the user will need to increase their memory settings
// In the other cases where there is nothing the user can fix in their project, we should not restore the outputs.
// Otherwise, the next build(s) will likely fail in exactly the same way as this build because their inputs and outputs are
// the same.
if (backup != null && (e is CompilationErrorException || e is OOMErrorException)) {
log.info("Restoring task outputs to pre-compilation state")
backup.restoreOutputs()
}
throw e
} finally {
backup?.deleteSnapshot()
}
}
// temporary adapter property // temporary adapter property
private val CompilationResult.asExitCode private val CompilationResult.asExitCode
get() = when (this) { get() = when (this) {
@@ -269,7 +269,7 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments> @Inject constr
projectLayout.buildDirectory, projectLayout.buildDirectory,
projectLayout.buildDirectory.dir("snapshot/kotlin/$name"), projectLayout.buildDirectory.dir("snapshot/kotlin/$name"),
outputsToRestore = allOutputFiles() - taskOutputsBackupExcludes.get(), outputsToRestore = allOutputFiles() - taskOutputsBackupExcludes.get(),
logger GradleKotlinLogger(logger),
).also { ).also {
it.createSnapshot() it.createSnapshot()
} }
@@ -8,8 +8,8 @@ package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.file.Directory import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations import org.gradle.api.file.FileSystemOperations
import org.gradle.api.logging.Logger
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.jetbrains.kotlin.buildtools.api.KotlinLogger
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.nio.file.FileSystems import java.nio.file.FileSystems
@@ -34,7 +34,7 @@ internal class TaskOutputsBackup(
*/ */
val outputsToRestore: List<File>, val outputsToRestore: List<File>,
val logger: Logger val logger: KotlinLogger,
) { ) {
fun createSnapshot() { fun createSnapshot() {