Clean up fall-back logic in IncrementalCompilerRunner

Make it clear that there 3 distinct cases:
   1. Incremental compilation completed with an ExitCode.
   2. Incremental compilation was not possible for some valid reason
      (e.g., for a clean build), and we will perform non-incremental
      compilation.
   3. Incremental compilation failed with an exception.
      In this case, we will:
        - Print a warning with a stack trace
        - Ask the user to file a bug
        - Collect rebuild reason enum for analytics
           + TODO: Collect the stack trace too
        - Fall back to non-incremental compilation

Test: Existing BaseIncrementalCompilationMultiProjectIT.testFailureHandling_UserError,
      Updated BaseIncrementalCompilationMultiProjectIT.testFailureHandling_ToolError

^KT-53015: In progress
This commit is contained in:
Hung Nguyen
2022-06-20 13:41:40 +01:00
committed by Andrey Uskov
parent 57bbc335f4
commit def886cd31
24 changed files with 409 additions and 341 deletions
@@ -18,9 +18,11 @@ enum class BuildAttributeKind : Serializable {
enum class BuildAttribute(val kind: BuildAttributeKind, val readableString: String) : Serializable { enum class BuildAttribute(val kind: BuildAttributeKind, val readableString: String) : Serializable {
NO_BUILD_HISTORY(BuildAttributeKind.REBUILD_REASON, "Build history file not found"), NO_BUILD_HISTORY(BuildAttributeKind.REBUILD_REASON, "Build history file not found"),
NO_ABI_SNAPSHOT(BuildAttributeKind.REBUILD_REASON, "ABI snapshot not found"), NO_ABI_SNAPSHOT(BuildAttributeKind.REBUILD_REASON, "ABI snapshot not found"),
INTERNAL_ERROR(BuildAttributeKind.REBUILD_REASON, "Internal error during preparation of IC round"),
CLASSPATH_SNAPSHOT_NOT_FOUND(BuildAttributeKind.REBUILD_REASON, "Classpath snapshot not found"), CLASSPATH_SNAPSHOT_NOT_FOUND(BuildAttributeKind.REBUILD_REASON, "Classpath snapshot not found"),
INCREMENTAL_COMPILATION_FAILED(BuildAttributeKind.REBUILD_REASON, "Incremental compilation failed"), IC_FAILED_TO_GET_CHANGED_FILES(BuildAttributeKind.REBUILD_REASON, "Failed to get changed files"),
IC_FAILED_TO_COMPUTE_FILES_TO_RECOMPILE(BuildAttributeKind.REBUILD_REASON, "Failed to compute files to recompile"),
IC_FAILED_TO_COMPILE_INCREMENTALLY(BuildAttributeKind.REBUILD_REASON, "Failed to compile incrementally"),
IC_FAILED_TO_CLOSE_CACHES(BuildAttributeKind.REBUILD_REASON, "Failed to close caches"),
UNKNOWN_CHANGES_IN_GRADLE_INPUTS(BuildAttributeKind.REBUILD_REASON, "Unknown Gradle changes"), UNKNOWN_CHANGES_IN_GRADLE_INPUTS(BuildAttributeKind.REBUILD_REASON, "Unknown Gradle changes"),
JAVA_CHANGE_UNTRACKED_FILE_IS_REMOVED(BuildAttributeKind.REBUILD_REASON, "Untracked Java file is removed"), JAVA_CHANGE_UNTRACKED_FILE_IS_REMOVED(BuildAttributeKind.REBUILD_REASON, "Untracked Java file is removed"),
JAVA_CHANGE_UNEXPECTED_PSI(BuildAttributeKind.REBUILD_REASON, "Java PSI file is expected"), JAVA_CHANGE_UNEXPECTED_PSI(BuildAttributeKind.REBUILD_REASON, "Java PSI file is expected"),
@@ -29,15 +29,15 @@ fun File.isClassFile(): Boolean =
extension.equals("class", ignoreCase = true) extension.equals("class", ignoreCase = true)
/** /**
* Deletes the contents of this directory (not the directory itself) if it exists, or creates the directory if it does not yet exist. * Deletes the contents of this directory (not the directory itself).
* *
* If this is a regular file, this method will throw an exception. * If the directory does not exist or if this is a regular file, this method will throw an exception.
*/ */
fun File.cleanDirectoryContents() { fun File.deleteDirectoryContents() {
when { when {
isDirectory -> listFiles()!!.forEach { it.deleteRecursivelyOrThrow() } isDirectory -> listFiles()!!.forEach { it.deleteRecursivelyOrThrow() }
isFile -> error("File.cleanDirectoryContents does not accept a regular file: $path") isFile -> error("Expected a directory but found a regular file: $path")
else -> mkdirsOrThrow() else -> error("Directory does not exist: $path")
} }
} }
@@ -49,11 +49,12 @@ fun File.deleteRecursivelyOrThrow() {
} }
/** /**
* Creates this directory (if it does not yet exist), throwing an exception if the directiory creation failed or if a regular file already * Creates this directory (if it does not yet exist).
* exists at this path. *
* If a regular file already exists at this path, this method will throw an exception.
*/ */
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
fun File.mkdirsOrThrow() { fun File.createDirectory() {
when { when {
isDirectory -> Unit isDirectory -> Unit
isFile -> error("A regular file already exists at this path: $path") isFile -> error("A regular file already exists at this path: $path")
@@ -16,7 +16,10 @@
package org.jetbrains.kotlin.daemon.common package org.jetbrains.kotlin.daemon.common
import org.jetbrains.kotlin.cli.common.repl.* import org.jetbrains.kotlin.cli.common.repl.ReplCheckResult
import org.jetbrains.kotlin.cli.common.repl.ReplCodeLine
import org.jetbrains.kotlin.cli.common.repl.ReplCompileResult
import org.jetbrains.kotlin.cli.common.repl.ReplEvalResult
import java.io.File import java.io.File
import java.io.Serializable import java.io.Serializable
import java.rmi.Remote import java.rmi.Remote
@@ -46,20 +49,26 @@ interface CompileService : Remote {
override fun equals(other: Any?): Boolean = other is Good<*> && this.result == other.result override fun equals(other: Any?): Boolean = other is Good<*> && this.result == other.result
override fun hashCode(): Int = this::class.java.hashCode() + (result?.hashCode() ?: 1) override fun hashCode(): Int = this::class.java.hashCode() + (result?.hashCode() ?: 1)
} }
class Ok : CallResult<Nothing>() { class Ok : CallResult<Nothing>() {
override fun get(): Nothing = throw IllegalStateException("Get is inapplicable to Ok call result") override fun get(): Nothing = throw IllegalStateException("Get is inapplicable to Ok call result")
override fun equals(other: Any?): Boolean = other is Ok override fun equals(other: Any?): Boolean = other is Ok
override fun hashCode(): Int = this::class.java.hashCode() + 1 // avoiding clash with the hash of class itself override fun hashCode(): Int = this::class.java.hashCode() + 1 // avoiding clash with the hash of class itself
} }
class Dying : CallResult<Nothing>() { class Dying : CallResult<Nothing>() {
override fun get(): Nothing = throw IllegalStateException("Service is dying") override fun get(): Nothing = throw IllegalStateException("Service is dying")
override fun equals(other: Any?): Boolean = other is Dying override fun equals(other: Any?): Boolean = other is Dying
override fun hashCode(): Int = this::class.java.hashCode() + 1 // see comment to Ok.hashCode override fun hashCode(): Int = this::class.java.hashCode() + 1 // see comment to Ok.hashCode
} }
class Error(val message: String) : CallResult<Nothing>() {
override fun get(): Nothing = throw Exception(message) class Error(val message: String?, val cause: Throwable?) : CallResult<Nothing>() {
override fun equals(other: Any?): Boolean = other is Error && this.message == other.message constructor(cause: Throwable) : this(message = null, cause = cause)
override fun hashCode(): Int = this::class.java.hashCode() + message.hashCode() constructor(message: String) : this(message = message, cause = null)
override fun get(): Nothing = throw Exception(message, cause)
override fun equals(other: Any?): Boolean = other is Error && this.message == other.message && this.cause == other.cause
override fun hashCode(): Int = this::class.java.hashCode() + (cause?.hashCode() ?: 1) + (message?.hashCode() ?: 2) // see comment to Ok.hashCode
} }
val isGood: Boolean get() = this is Good<*> val isGood: Boolean get() = this is Good<*>
@@ -781,7 +781,7 @@ class CompilerDaemonTest : KotlinIntegrationTestBase() {
} catch (e: Exception) { } catch (e: Exception) {
TestCase.assertEquals( TestCase.assertEquals(
"Unable to use scripting/REPL in the daemon: no scripting plugin loaded", "Unable to use scripting/REPL in the daemon: no scripting plugin loaded",
e.message e.cause?.message
) )
isErrorThrown = true isErrorThrown = true
} finally { } finally {
@@ -47,8 +47,8 @@ import org.jetbrains.kotlin.daemon.report.getBuildReporter
import org.jetbrains.kotlin.incremental.* import org.jetbrains.kotlin.incremental.*
import org.jetbrains.kotlin.incremental.components.EnumWhenTracker import org.jetbrains.kotlin.incremental.components.EnumWhenTracker
import org.jetbrains.kotlin.incremental.components.ExpectActualTracker import org.jetbrains.kotlin.incremental.components.ExpectActualTracker
import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.incremental.components.InlineConstTracker import org.jetbrains.kotlin.incremental.components.InlineConstTracker
import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.incremental.js.IncrementalDataProvider import org.jetbrains.kotlin.incremental.js.IncrementalDataProvider
import org.jetbrains.kotlin.incremental.js.IncrementalResultsConsumer import org.jetbrains.kotlin.incremental.js.IncrementalResultsConsumer
import org.jetbrains.kotlin.incremental.multiproject.ModulesApiHistoryAndroid import org.jetbrains.kotlin.incremental.multiproject.ModulesApiHistoryAndroid
@@ -507,7 +507,7 @@ abstract class CompileServiceImplBase(
body() body()
} catch (e: Throwable) { } catch (e: Throwable) {
log.log(Level.SEVERE, "Exception", e) log.log(Level.SEVERE, "Exception", e)
CompileService.CallResult.Error(e.message ?: "unknown") CompileService.CallResult.Error(e)
} }
} }
} }
@@ -614,7 +614,7 @@ abstract class CompileServiceImplBase(
workingDir, workingDir,
reporter, reporter,
buildHistoryFile = incrementalCompilationOptions.multiModuleICSettings.buildHistoryFile, buildHistoryFile = incrementalCompilationOptions.multiModuleICSettings.buildHistoryFile,
outputFiles = incrementalCompilationOptions.outputFiles, outputDirs = incrementalCompilationOptions.outputFiles,
usePreciseJavaTracking = incrementalCompilationOptions.usePreciseJavaTracking, usePreciseJavaTracking = incrementalCompilationOptions.usePreciseJavaTracking,
modulesApiHistory = modulesApiHistory, modulesApiHistory = modulesApiHistory,
kotlinSourceFilesExtensions = allKotlinExtensions, kotlinSourceFilesExtensions = allKotlinExtensions,
@@ -23,7 +23,6 @@ import org.jetbrains.kotlin.cli.common.repl.ReplCompileResult
import org.jetbrains.kotlin.cli.js.K2JSCompiler import org.jetbrains.kotlin.cli.js.K2JSCompiler
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.jarfs.FastJarFileSystem
import org.jetbrains.kotlin.cli.metadata.K2MetadataCompiler import org.jetbrains.kotlin.cli.metadata.K2MetadataCompiler
import org.jetbrains.kotlin.config.KotlinCompilerVersion import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.config.Services import org.jetbrains.kotlin.config.Services
@@ -749,7 +748,7 @@ class CompileServiceServerSideImpl(
body() body()
} catch (e: Throwable) { } catch (e: Throwable) {
log.log(Level.SEVERE, "Exception", e) log.log(Level.SEVERE, "Exception", e)
CompileService.CallResult.Error(e.message ?: "unknown") CompileService.CallResult.Error(e)
} }
} }
} }
@@ -5,8 +5,6 @@
package org.jetbrains.kotlin.incremental package org.jetbrains.kotlin.incremental
import org.jetbrains.kotlin.build.report.BuildReporter
import org.jetbrains.kotlin.build.report.info
import org.jetbrains.kotlin.metadata.deserialization.NameResolverImpl import org.jetbrains.kotlin.metadata.deserialization.NameResolverImpl
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmNameResolver import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmNameResolver
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
@@ -152,14 +150,9 @@ class AbiSnapshotImpl(override val protos: MutableMap<FqName, ProtoData>) : AbiS
} }
} }
fun read(file: File, reporter: BuildReporter): AbiSnapshot? { fun read(file: File): AbiSnapshot {
if (!file.exists()) { return ObjectInputStream(FileInputStream(file)).use {
reporter.info { "jar snapshot $file is found for jar" } it.readAbiSnapshot()
return null
}
ObjectInputStream(FileInputStream(file)).use {
return it.readAbiSnapshot()
} }
} }
} }
@@ -20,10 +20,8 @@ import java.io.File
import java.io.Serializable import java.io.Serializable
sealed class ChangedFiles : Serializable { sealed class ChangedFiles : Serializable {
class Known(val modified: List<File>, val removed: List<File>) : ChangedFiles() class Known(val modified: List<File>, val removed: List<File>, val forDependencies: Boolean = false) : ChangedFiles()
class Unknown : ChangedFiles() class Unknown : ChangedFiles()
class Dependencies(val modified: List<File>, val removed: List<File>) : ChangedFiles()
companion object { companion object {
const val serialVersionUID: Long = 0 const val serialVersionUID: Long = 0
} }
@@ -16,30 +16,29 @@
package org.jetbrains.kotlin.incremental package org.jetbrains.kotlin.incremental
import com.google.common.io.Closer
import org.jetbrains.kotlin.build.report.ICReporter import org.jetbrains.kotlin.build.report.ICReporter
import org.jetbrains.kotlin.build.report.info
import org.jetbrains.kotlin.incremental.storage.BasicMapsOwner import org.jetbrains.kotlin.incremental.storage.BasicMapsOwner
import org.jetbrains.kotlin.incremental.storage.IncrementalFileToPathConverter import org.jetbrains.kotlin.incremental.storage.IncrementalFileToPathConverter
import org.jetbrains.kotlin.serialization.SerializerExtensionProtocol import org.jetbrains.kotlin.serialization.SerializerExtensionProtocol
import java.io.Closeable
import java.io.File import java.io.File
abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache<*>>( abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache<*>>(
cachesRootDir: File, cachesRootDir: File,
rootProjectDir: File?, rootProjectDir: File?,
protected val reporter: ICReporter, protected val reporter: ICReporter,
storeFullFqNamesInLookupCache: Boolean = false, storeFullFqNamesInLookupCache: Boolean = false,
trackChangesInLookupCache: Boolean = false trackChangesInLookupCache: Boolean = false
) { ) : Closeable {
val pathConverter = IncrementalFileToPathConverter(rootProjectDir) val pathConverter = IncrementalFileToPathConverter(rootProjectDir)
private val caches = arrayListOf<BasicMapsOwner>() private val caches = arrayListOf<BasicMapsOwner>()
var isClosed = false private var isClosed = false
var isSuccessfulyClosed = false
@Synchronized @Synchronized
protected fun <T : BasicMapsOwner> T.registerCache() { protected fun <T : BasicMapsOwner> T.registerCache() {
assert(!isClosed) { "Attempted to add new cache into closed storage." } check(!isClosed) { "This cache storage has already been closed" }
caches.add(this) caches.add(this)
} }
@@ -51,33 +50,29 @@ abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache
LookupStorage(lookupCacheDir, pathConverter, storeFullFqNamesInLookupCache, trackChangesInLookupCache).apply { registerCache() } LookupStorage(lookupCacheDir, pathConverter, storeFullFqNamesInLookupCache, trackChangesInLookupCache).apply { registerCache() }
abstract val platformCache: PlatformCache abstract val platformCache: PlatformCache
@Suppress("UnstableApiUsage")
@Synchronized @Synchronized
fun close(flush: Boolean = false): Boolean { override fun close() {
if (isClosed) { check(!isClosed) { "This cache storage has already been closed" }
return isSuccessfulyClosed
}
isSuccessfulyClosed = true
for (cache in caches) {
if (flush) {
try {
cache.flush(false)
} catch (e: Throwable) {
isSuccessfulyClosed = false
reporter.info { "Exception when flushing cache ${cache.javaClass}: $e" }
}
}
try { val closer = Closer.create()
cache.close() caches.forEach {
} catch (e: Throwable) { closer.register(CacheCloser(it))
isSuccessfulyClosed = false
reporter.info { "Exception when closing cache ${cache.javaClass}: $e" }
}
} }
closer.close()
isClosed = true isClosed = true
return isSuccessfulyClosed
} }
private class CacheCloser(private val cache: BasicMapsOwner) : Closeable {
override fun close() {
// It's important to flush the cache when closing (see KT-53168)
cache.flush(memoryCachesOnly = false)
cache.close()
}
}
} }
class IncrementalJvmCachesManager( class IncrementalJvmCachesManager(
@@ -22,6 +22,7 @@ import org.jetbrains.kotlin.build.report.BuildReporter
import org.jetbrains.kotlin.build.report.debug import org.jetbrains.kotlin.build.report.debug
import org.jetbrains.kotlin.build.report.info import org.jetbrains.kotlin.build.report.info
import org.jetbrains.kotlin.build.report.metrics.BuildAttribute import org.jetbrains.kotlin.build.report.metrics.BuildAttribute
import org.jetbrains.kotlin.build.report.metrics.BuildAttribute.*
import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric
import org.jetbrains.kotlin.build.report.metrics.BuildTime import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.measure import org.jetbrains.kotlin.build.report.metrics.measure
@@ -40,7 +41,7 @@ import org.jetbrains.kotlin.incremental.parsing.classesFqNames
import org.jetbrains.kotlin.incremental.util.BufferingMessageCollector import org.jetbrains.kotlin.incremental.util.BufferingMessageCollector
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.progress.CompilationCanceledStatus import org.jetbrains.kotlin.progress.CompilationCanceledStatus
import org.jetbrains.kotlin.util.suffixIfNot import org.jetbrains.kotlin.util.removeSuffixIfPresent
import java.io.File import java.io.File
abstract class IncrementalCompilerRunner< abstract class IncrementalCompilerRunner<
@@ -51,14 +52,24 @@ abstract class IncrementalCompilerRunner<
cacheDirName: String, cacheDirName: String,
protected val reporter: BuildReporter, protected val reporter: BuildReporter,
protected val buildHistoryFile: File, protected val buildHistoryFile: File,
// there might be some additional output directories (e.g. for generated java in kapt)
// to remove them correctly on rebuild, we pass them as additional argument /**
private val additionalOutputFiles: Collection<File> = emptyList(), * Output directories of the compilation. These include:
* 1. The classes output directory
* 2. [workingDir]
* 3. Any additional output directories (e.g., classpath snapshot directory or Kapt generated-stubs directory)
*
* We will clean these directories when compiling non-incrementally.
*
* If this property is not set, the directories to clean will include the first 2 directories above.
*/
private val outputDirs: Collection<File>?,
protected val withAbiSnapshot: Boolean = false protected val withAbiSnapshot: Boolean = false
) { ) {
protected val cacheDirectory = File(workingDir, cacheDirName) protected val cacheDirectory = File(workingDir, cacheDirName)
protected val dirtySourcesSinceLastTimeFile = File(workingDir, DIRTY_SOURCES_FILE_NAME) private val dirtySourcesSinceLastTimeFile = File(workingDir, DIRTY_SOURCES_FILE_NAME)
protected val lastBuildInfoFile = File(workingDir, LAST_BUILD_INFO_FILE_NAME) protected val lastBuildInfoFile = File(workingDir, LAST_BUILD_INFO_FILE_NAME)
private val abiSnapshotFile = File(workingDir, ABI_SNAPSHOT_FILE_NAME) private val abiSnapshotFile = File(workingDir, ABI_SNAPSHOT_FILE_NAME)
protected open val kotlinSourceFilesExtensions: List<String> = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS protected open val kotlinSourceFilesExtensions: List<String> = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS
@@ -75,184 +86,204 @@ abstract class IncrementalCompilerRunner<
providedChangedFiles: ChangedFiles?, providedChangedFiles: ChangedFiles?,
projectDir: File? = null projectDir: File? = null
): ExitCode = reporter.measure(BuildTime.INCREMENTAL_COMPILATION_DAEMON) { ): ExitCode = reporter.measure(BuildTime.INCREMENTAL_COMPILATION_DAEMON) {
try { return when (val result = tryCompileIncrementally(allSourceFiles, providedChangedFiles, args, projectDir, messageCollector)) {
compileImpl(allSourceFiles, args, messageCollector, providedChangedFiles, projectDir) is ICResult.Completed -> {
} finally { reporter.debug { "Incremental compilation completed" }
reporter.measure(BuildTime.CALCULATE_OUTPUT_SIZE) { result.exitCode
reporter.addMetric( }
BuildPerformanceMetric.SNAPSHOT_SIZE, is ICResult.RequiresRebuild -> {
buildHistoryFile.length() + lastBuildInfoFile.length() + abiSnapshotFile.length() reporter.info { "Non-incremental compilation will be performed: ${result.reason}" }
reporter.addAttribute(result.reason)
compileNonIncrementally(
result.reason, allSourceFiles, args, projectDir, trackChangedFiles = providedChangedFiles == null, messageCollector
) )
if (cacheDirectory.exists() && cacheDirectory.isDirectory()) { }
cacheDirectory.walkTopDown().filter { it.isFile }.map { it.length() }.sum().let { is ICResult.Failed -> {
reporter.addMetric(BuildPerformanceMetric.CACHE_DIRECTORY_SIZE, it) reporter.warn {
} // The indentation after the first line is intentional (so that this message is distinct from next message)
"""
|Incremental compilation was attempted but failed:
| ${result.reason.readableString}: ${result.cause.stackTraceToString().removeSuffixIfPresent("\n")}
| Falling back to non-incremental compilation (reason = ${result.reason})
| To help us fix this issue, please file a bug at https://youtrack.jetbrains.com/issues/KT with the above stack trace.
| (Be sure to search for the above exception in existing issues first to avoid filing duplicated bugs.)
""".trimMargin()
} }
// TODO: Collect the stack trace too
reporter.addAttribute(result.reason)
compileNonIncrementally(
result.reason, allSourceFiles, args, projectDir, trackChangedFiles = providedChangedFiles == null, messageCollector
)
} }
} }
} }
fun rebuild( /** The result when attempting to compile incrementally ([tryCompileIncrementally]). */
reason: BuildAttribute, private sealed interface ICResult {
allSourceFiles: List<File>,
args: Args,
messageCollector: MessageCollector,
providedChangedFiles: ChangedFiles?,
projectDir: File? = null,
classpathAbiSnapshot: Map<String, AbiSnapshot>
): ExitCode {
reporter.info { "Non-incremental compilation will be performed: $reason" }
reporter.measure(BuildTime.CLEAR_OUTPUT_ON_REBUILD) {
cleanOutputsAndLocalStateOnRebuild(args)
}
val caches = createCacheManager(args, projectDir)
try {
if (providedChangedFiles == null) {
caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
}
val allKotlinFiles = allSourceFiles.filter { it.isKotlinFile(kotlinSourceFilesExtensions) }
return compileIncrementally(
args, caches, allKotlinFiles, CompilationMode.Rebuild(reason), messageCollector, withAbiSnapshot,
classpathAbiSnapshot = classpathAbiSnapshot
).also {
if (it == ExitCode.OK) {
performWorkAfterSuccessfulCompilation(caches, wasIncremental = false)
}
}
} finally {
caches.close(true)
}
}
private fun compileImpl( /** Incremental compilation completed with an [ExitCode]. */
allSourceFiles: List<File>, class Completed(val exitCode: ExitCode) : ICResult
args: Args,
messageCollector: MessageCollector,
providedChangedFiles: ChangedFiles?,
projectDir: File? = null
): ExitCode {
var caches = createCacheManager(args, projectDir)
var rebuildReason = BuildAttribute.INTERNAL_ERROR
val classpathAbiSnapshot = /** Incremental compilation was not possible for some valid reason (e.g., for a clean build). */
if (withAbiSnapshot) { class RequiresRebuild(val reason: BuildAttribute) : ICResult
reporter.info { "Incremental compilation with ABI snapshot enabled" }
reporter.measure(BuildTime.SET_UP_ABI_SNAPSHOTS) {
setupJarDependencies(args, withAbiSnapshot, reporter)
}
} else {
emptyMap()
}
try { /** Incremental compilation failed with an exception. */
val changedFiles = when (providedChangedFiles) { class Failed(val reason: BuildAttribute, val cause: Throwable) : ICResult
is ChangedFiles.Dependencies -> {
val changedSources = caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
ChangedFiles.Known(
providedChangedFiles.modified + changedSources.modified,
providedChangedFiles.removed + changedSources.removed
)
}
null -> caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
else -> providedChangedFiles
}
var compilationMode = sourcesToCompile(caches, changedFiles, args, messageCollector, classpathAbiSnapshot)
val abiSnapshot = if (compilationMode is CompilationMode.Incremental && withAbiSnapshot) {
AbiSnapshotImpl.read(abiSnapshotFile, reporter)
} else {
if (withAbiSnapshot) {
compilationMode = CompilationMode.Rebuild(BuildAttribute.NO_ABI_SNAPSHOT)
}
null
}
when (compilationMode) {
is CompilationMode.Incremental -> {
try {
reporter.debug { "Performing incremental compilation" }
val exitCode = if (withAbiSnapshot) {
compileIncrementally(
args, caches, allSourceFiles, compilationMode, messageCollector,
withAbiSnapshot, abiSnapshot!!, classpathAbiSnapshot
)
} else {
compileIncrementally(args, caches, allSourceFiles, compilationMode, messageCollector, withAbiSnapshot)
}
if (exitCode == ExitCode.OK) {
performWorkAfterSuccessfulCompilation(caches, wasIncremental = true)
}
return exitCode
} catch (e: Throwable) {
reporter.warn {
"Incremental compilation failed: ${e.stackTraceToString().suffixIfNot("\n")}" +
"Falling back to non-incremental compilation"
}
rebuildReason = BuildAttribute.INCREMENTAL_COMPILATION_FAILED
}
}
is CompilationMode.Rebuild -> rebuildReason = compilationMode.reason
}
} catch (e: Exception) {
reporter.warn {
"Incremental compilation analysis failed: ${e.stackTraceToString().suffixIfNot("\n")}" +
"Falling back to non-incremental compilation"
}
} finally {
if (!caches.close(flush = true)) {
reporter.info { "Unable to close IC caches. Cleaning internal state" }
cleanOutputsAndLocalStateOnRebuild(args)
}
}
return rebuild(rebuildReason, allSourceFiles, args, messageCollector, providedChangedFiles, projectDir, classpathAbiSnapshot)
} }
/** /**
* Deletes output files and contents of output directories on rebuild, including `@LocalState` files/directories. * Attempts to compile incrementally and returns either [ICResult.Completed], [ICResult.RequiresRebuild], or [ICResult.Failed].
*
* Note that parts of this function may still throw exceptions that are not caught and wrapped by [ICResult.Failed] because they are not
* meant to be caught.
*/
private fun tryCompileIncrementally(
allSourceFiles: List<File>,
providedChangedFiles: ChangedFiles?,
args: Args,
projectDir: File?,
messageCollector: MessageCollector
): ICResult {
if (providedChangedFiles is ChangedFiles.Unknown) {
return ICResult.RequiresRebuild(UNKNOWN_CHANGES_IN_GRADLE_INPUTS)
}
providedChangedFiles as ChangedFiles.Known?
val caches = createCacheManager(args, projectDir)
val exitCode: ExitCode
try {
// Step 1: Get changed files
val changedFiles: ChangedFiles.Known = try {
getChangedFiles(providedChangedFiles, allSourceFiles, caches)
} catch (e: Throwable) {
return ICResult.Failed(IC_FAILED_TO_GET_CHANGED_FILES, e)
}
val classpathAbiSnapshot = if (withAbiSnapshot) getClasspathAbiSnapshot(args) else null
// Step 2: Compute files to recompile
val compilationMode = try {
reporter.measure(BuildTime.IC_CALCULATE_INITIAL_DIRTY_SET) {
calculateSourcesToCompile(caches, changedFiles, args, messageCollector, classpathAbiSnapshot ?: emptyMap())
}
} catch (e: Throwable) {
return ICResult.Failed(IC_FAILED_TO_COMPUTE_FILES_TO_RECOMPILE, e)
}
if (compilationMode is CompilationMode.Rebuild) {
return ICResult.RequiresRebuild(compilationMode.reason)
}
val abiSnapshotData = if (withAbiSnapshot) {
if (!abiSnapshotFile.exists()) {
reporter.debug { "Jar snapshot file does not exist: ${abiSnapshotFile.path}" }
return ICResult.RequiresRebuild(NO_ABI_SNAPSHOT)
}
reporter.info { "Incremental compilation with ABI snapshot enabled" }
AbiSnapshotData(
snapshot = AbiSnapshotImpl.read(abiSnapshotFile),
classpathAbiSnapshot = classpathAbiSnapshot!!
)
} else null
// Step 3: Compile incrementally
exitCode = try {
compileImpl(compilationMode as CompilationMode.Incremental, allSourceFiles, args, caches, abiSnapshotData, messageCollector)
} catch (e: Throwable) {
return ICResult.Failed(IC_FAILED_TO_COMPILE_INCREMENTALLY, e)
}
} catch (e: Throwable) {
// Because `caches` is a Closeable resource, it is good practice to close them in the event of an exception (in addition to
// closing them after a normal use).
try {
caches.close()
} catch (e2: Throwable) {
e.addSuppressed(e2)
}
throw e
}
try {
caches.close()
} catch (e: Throwable) {
return ICResult.Failed(IC_FAILED_TO_CLOSE_CACHES, e)
}
return ICResult.Completed(exitCode)
}
private fun compileNonIncrementally(
rebuildReason: BuildAttribute,
allSourceFiles: List<File>,
args: Args,
projectDir: File?,
trackChangedFiles: Boolean, // Whether we need to track changes to the source files or the build system already handles it
messageCollector: MessageCollector,
): ExitCode {
reporter.measure(BuildTime.CLEAR_OUTPUT_ON_REBUILD) {
val mainOutputDirs = setOf(destinationDir(args), workingDir)
val outputDirsToClean = outputDirs?.also {
check(it.containsAll(mainOutputDirs)) { "outputDirs is missing classesDir and workingDir: $it" }
} ?: mainOutputDirs
reporter.debug { "Cleaning output directories" }
cleanOrCreateDirectories(outputDirsToClean)
}
return createCacheManager(args, projectDir).use { caches ->
if (trackChangedFiles) {
caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
}
val abiSnapshotData = if (withAbiSnapshot) {
AbiSnapshotData(snapshot = AbiSnapshotImpl(mutableMapOf()), classpathAbiSnapshot = getClasspathAbiSnapshot(args))
} else null
compileImpl(CompilationMode.Rebuild(rebuildReason), allSourceFiles, args, caches, abiSnapshotData, messageCollector)
}
}
private class AbiSnapshotData(val snapshot: AbiSnapshot, val classpathAbiSnapshot: Map<String, AbiSnapshot>)
private fun getClasspathAbiSnapshot(args: Args): Map<String, AbiSnapshot> {
return reporter.measure(BuildTime.SET_UP_ABI_SNAPSHOTS) {
setupJarDependencies(args, reporter)
}
}
/**
* Deletes the contents of the given directories (not the directories themselves).
* *
* If the directories do not yet exist, they will be created. * If the directories do not yet exist, they will be created.
*/ */
private fun cleanOutputsAndLocalStateOnRebuild(args: Args) { private fun cleanOrCreateDirectories(outputDirs: Collection<File>) {
// Use Set as additionalOutputFiles may already contain destinationDir and workingDir outputDirs.toSet().forEach {
val outputFiles = setOf(destinationDir(args), workingDir) + additionalOutputFiles
reporter.debug { "Cleaning outputs on rebuild" }
outputFiles.forEach {
when { when {
it.isDirectory -> { it.isDirectory -> it.deleteDirectoryContents()
reporter.debug { " Deleting contents of directory '${it.path}'" } it.isFile -> "Expected a directory but found a regular file: ${it.path}"
it.cleanDirectoryContents() else -> it.createDirectory()
}
it.isFile -> {
reporter.debug { " Deleting file '${it.path}'" }
it.deleteRecursivelyOrThrow()
}
} }
} }
} }
private fun sourcesToCompile( private fun getChangedFiles(
caches: CacheManager, providedChangedFiles: ChangedFiles.Known?,
changedFiles: ChangedFiles, allSourceFiles: List<File>,
args: Args, caches: CacheManager
messageCollector: MessageCollector, ): ChangedFiles.Known {
dependenciesAbiSnapshots: Map<String, AbiSnapshot> return when {
): CompilationMode = providedChangedFiles == null -> caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
when (changedFiles) { providedChangedFiles.forDependencies -> {
is ChangedFiles.Known -> calculateSourcesToCompile(caches, changedFiles, args, messageCollector, dependenciesAbiSnapshots) val moreChangedFiles = caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
is ChangedFiles.Unknown -> CompilationMode.Rebuild(BuildAttribute.UNKNOWN_CHANGES_IN_GRADLE_INPUTS) ChangedFiles.Known(
is ChangedFiles.Dependencies -> error("Unexpected ChangedFiles type (ChangedFiles.Dependencies)") modified = providedChangedFiles.modified + moreChangedFiles.modified,
removed = providedChangedFiles.removed + moreChangedFiles.removed
)
}
else -> providedChangedFiles
} }
}
private fun calculateSourcesToCompile( protected abstract fun calculateSourcesToCompile(
caches: CacheManager, changedFiles: ChangedFiles.Known, args: Args, messageCollector: MessageCollector,
abiSnapshots: Map<String, AbiSnapshot>
): CompilationMode =
reporter.measure(BuildTime.IC_CALCULATE_INITIAL_DIRTY_SET) {
calculateSourcesToCompileImpl(caches, changedFiles, args, messageCollector, abiSnapshots)
}
protected abstract fun calculateSourcesToCompileImpl(
caches: CacheManager, caches: CacheManager,
changedFiles: ChangedFiles.Known, changedFiles: ChangedFiles.Known,
args: Args, args: Args,
@@ -260,7 +291,7 @@ abstract class IncrementalCompilerRunner<
classpathAbiSnapshots: Map<String, AbiSnapshot> classpathAbiSnapshots: Map<String, AbiSnapshot>
): CompilationMode ): CompilationMode
protected open fun setupJarDependencies(args: Args, withSnapshot: Boolean, reporter: BuildReporter): Map<String, AbiSnapshot> = mapOf() protected open fun setupJarDependencies(args: Args, reporter: BuildReporter): Map<String, AbiSnapshot> = emptyMap()
protected fun initDirtyFiles(dirtyFiles: DirtyFilesContainer, changedFiles: ChangedFiles.Known) { protected fun initDirtyFiles(dirtyFiles: DirtyFilesContainer, changedFiles: ChangedFiles.Known) {
dirtyFiles.add(changedFiles.modified, "was modified since last time") dirtyFiles.add(changedFiles.modified, "was modified since last time")
@@ -284,7 +315,6 @@ abstract class IncrementalCompilerRunner<
changesCollector: ChangesCollector changesCollector: ChangesCollector
) )
protected open fun preBuildHook(args: Args, compilationMode: CompilationMode) {}
protected open fun additionalDirtyFiles(caches: CacheManager, generatedFiles: List<GeneratedFile>, services: Services): Iterable<File> = protected open fun additionalDirtyFiles(caches: CacheManager, generatedFiles: List<GeneratedFile>, services: Services): Iterable<File> =
emptyList() emptyList()
@@ -315,33 +345,53 @@ abstract class IncrementalCompilerRunner<
isIncremental: Boolean isIncremental: Boolean
): Pair<ExitCode, Collection<File>> ): Pair<ExitCode, Collection<File>>
protected open fun compileIncrementally( private fun compileImpl(
compilationMode: CompilationMode,
allSourceFiles: List<File>,
args: Args, args: Args,
caches: CacheManager, caches: CacheManager,
allKotlinSources: List<File>, abiSnapshotData: AbiSnapshotData?, // Not null iff withAbiSnapshot = true
compilationMode: CompilationMode, messageCollector: MessageCollector
originalMessageCollector: MessageCollector,
withSnapshot: Boolean,
abiSnapshot: AbiSnapshot = AbiSnapshotImpl(mutableMapOf()),
classpathAbiSnapshot: Map<String, AbiSnapshot> = HashMap()
): ExitCode { ): ExitCode {
if (compilationMode is CompilationMode.Rebuild) { performWorkBeforeCompilation(compilationMode, args)
reporter.info { "Non-incremental compilation will be performed: ${compilationMode.reason}" }
val allKotlinFiles = allSourceFiles.filter { it.isKotlinFile(kotlinSourceFilesExtensions) }
val exitCode = doCompile(compilationMode, allKotlinFiles, args, caches, abiSnapshotData, messageCollector)
performWorkAfterCompilation(compilationMode, exitCode, caches)
return exitCode
}
protected open fun performWorkBeforeCompilation(compilationMode: CompilationMode, args: Args) {}
protected open fun performWorkAfterCompilation(compilationMode: CompilationMode, exitCode: ExitCode, caches: CacheManager) {
collectMetrics()
}
private fun collectMetrics() {
reporter.measure(BuildTime.CALCULATE_OUTPUT_SIZE) {
reporter.addMetric(
BuildPerformanceMetric.SNAPSHOT_SIZE,
buildHistoryFile.length() + lastBuildInfoFile.length() + abiSnapshotFile.length()
)
reporter.addMetric(BuildPerformanceMetric.CACHE_DIRECTORY_SIZE, cacheDirectory.walk().sumOf { it.length() })
} }
}
preBuildHook(args, compilationMode) private fun doCompile(
compilationMode: CompilationMode,
allKotlinSources: List<File>,
args: Args,
caches: CacheManager,
abiSnapshotData: AbiSnapshotData?, // Not null iff withAbiSnapshot = true
originalMessageCollector: MessageCollector
): ExitCode {
val dirtySources = when (compilationMode) { val dirtySources = when (compilationMode) {
is CompilationMode.Incremental -> { is CompilationMode.Incremental -> compilationMode.dirtyFiles.toMutableLinkedSet()
compilationMode.dirtyFiles.toMutableLinkedSet() is CompilationMode.Rebuild -> LinkedHashSet(allKotlinSources)
}
is CompilationMode.Rebuild -> {
reporter.addAttribute(compilationMode.reason)
LinkedHashSet(allKotlinSources)
}
} }
val currentBuildInfo = BuildInfo(startTS = System.currentTimeMillis(), classpathAbiSnapshot) val currentBuildInfo = BuildInfo(startTS = System.currentTimeMillis(), abiSnapshotData?.classpathAbiSnapshot ?: emptyMap())
val buildDirtyLookupSymbols = HashSet<LookupSymbol>() val buildDirtyLookupSymbols = HashSet<LookupSymbol>()
val buildDirtyFqNames = HashSet<FqName>() val buildDirtyFqNames = HashSet<FqName>()
val allDirtySources = HashSet<File>() val allDirtySources = HashSet<File>()
@@ -412,8 +462,8 @@ abstract class IncrementalCompilerRunner<
updateCaches(services, caches, generatedFiles, changesCollector) updateCaches(services, caches, generatedFiles, changesCollector)
} }
if (compilationMode is CompilationMode.Rebuild) { if (compilationMode is CompilationMode.Rebuild) {
if (withSnapshot) { if (withAbiSnapshot) {
abiSnapshot.protos.putAll(changesCollector.protoDataChanges()) abiSnapshotData!!.snapshot.protos.putAll(changesCollector.protoDataChanges())
} }
break break
} }
@@ -445,10 +495,10 @@ abstract class IncrementalCompilerRunner<
buildDirtyFqNames.addAll(dirtyClassFqNames) buildDirtyFqNames.addAll(dirtyClassFqNames)
//update //update
if (withSnapshot) { if (withAbiSnapshot) {
//TODO(valtman) check method/ kts class remove //TODO(valtman) check method/ kts class remove
changesCollector.protoDataRemoved().forEach { abiSnapshot.protos.remove(it) } changesCollector.protoDataRemoved().forEach { abiSnapshotData!!.snapshot.protos.remove(it) }
abiSnapshot.protos.putAll(changesCollector.protoDataChanges()) abiSnapshotData!!.snapshot.protos.putAll(changesCollector.protoDataChanges())
} }
} }
@@ -457,9 +507,9 @@ abstract class IncrementalCompilerRunner<
BuildInfo.write(currentBuildInfo, lastBuildInfoFile) BuildInfo.write(currentBuildInfo, lastBuildInfoFile)
//write abi snapshot //write abi snapshot
if (withSnapshot) { if (withAbiSnapshot) {
//TODO(valtman) check method/class remove //TODO(valtman) check method/class remove
AbiSnapshotImpl.write(abiSnapshot, abiSnapshotFile) AbiSnapshotImpl.write(abiSnapshotData!!.snapshot, abiSnapshotFile)
} }
} }
} }
@@ -516,15 +566,6 @@ abstract class IncrementalCompilerRunner<
BuildDiffsStorage.writeToFile(buildHistoryFile, BuildDiffsStorage(prevDiffs + newDiff), reporter) BuildDiffsStorage.writeToFile(buildHistoryFile, BuildDiffsStorage(prevDiffs + newDiff), reporter)
} }
/**
* Performs some work after a compilation if the compilation completed successfully (no exceptions were thrown AND exit code == 0).
*
* This method MUST NOT be called when the compilation failed because the results produced by the work here would be invalid.
*
* @wasIncremental whether the compilation was incremental or non-incremental
*/
protected open fun performWorkAfterSuccessfulCompilation(caches: CacheManager, wasIncremental: Boolean) {}
companion object { companion object {
const val DIRTY_SOURCES_FILE_NAME = "dirty-sources.txt" const val DIRTY_SOURCES_FILE_NAME = "dirty-sources.txt"
const val LAST_BUILD_INFO_FILE_NAME = "last-build.bin" const val LAST_BUILD_INFO_FILE_NAME = "last-build.bin"
@@ -72,7 +72,7 @@ class IncrementalFirJvmCompilerRunner(
workingDir: File, workingDir: File,
reporter: BuildReporter, reporter: BuildReporter,
buildHistoryFile: File, buildHistoryFile: File,
outputFiles: Collection<File>, outputDirs: Collection<File>?,
modulesApiHistory: ModulesApiHistory, modulesApiHistory: ModulesApiHistory,
kotlinSourceFilesExtensions: List<String> = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS, kotlinSourceFilesExtensions: List<String> = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS,
classpathChanges: ClasspathChanges classpathChanges: ClasspathChanges
@@ -81,7 +81,7 @@ class IncrementalFirJvmCompilerRunner(
reporter, reporter,
false, false,
buildHistoryFile, buildHistoryFile,
outputFiles, outputDirs,
modulesApiHistory, modulesApiHistory,
kotlinSourceFilesExtensions, kotlinSourceFilesExtensions,
classpathChanges classpathChanges
@@ -93,6 +93,7 @@ class IncrementalJsCompilerRunner(
"caches-js", "caches-js",
reporter, reporter,
buildHistoryFile = buildHistoryFile, buildHistoryFile = buildHistoryFile,
outputDirs = null,
withAbiSnapshot = withAbiSnapshot withAbiSnapshot = withAbiSnapshot
) { ) {
@@ -115,7 +116,7 @@ class IncrementalJsCompilerRunner(
outputFile.parentFile outputFile.parentFile
} }
override fun calculateSourcesToCompileImpl( override fun calculateSourcesToCompile(
caches: IncrementalJsCachesManager, caches: IncrementalJsCachesManager,
changedFiles: ChangedFiles.Known, changedFiles: ChangedFiles.Known,
args: K2JSCompilerArguments, args: K2JSCompilerArguments,
@@ -88,7 +88,13 @@ fun makeIncrementally(
val compiler = val compiler =
if (args.useK2 && args.useFirIC && args.useFirLT /* TODO: move LT check into runner */ ) if (args.useK2 && args.useFirIC && args.useFirLT /* TODO: move LT check into runner */ )
IncrementalFirJvmCompilerRunner( IncrementalFirJvmCompilerRunner(
cachesDir, buildReporter, buildHistoryFile, emptyList(), EmptyModulesApiHistory, kotlinExtensions, ClasspathSnapshotDisabled cachesDir,
buildReporter,
buildHistoryFile,
outputDirs = null,
EmptyModulesApiHistory,
kotlinExtensions,
ClasspathSnapshotDisabled
) )
else else
IncrementalJvmCompilerRunner( IncrementalJvmCompilerRunner(
@@ -96,8 +102,8 @@ fun makeIncrementally(
buildReporter, buildReporter,
// Use precise setting in case of non-Gradle build // Use precise setting in case of non-Gradle build
usePreciseJavaTracking = !args.useK2, // TODO: add fir-based java classes tracker when available and set this to true usePreciseJavaTracking = !args.useK2, // TODO: add fir-based java classes tracker when available and set this to true
outputFiles = emptyList(),
buildHistoryFile = buildHistoryFile, buildHistoryFile = buildHistoryFile,
outputDirs = null,
modulesApiHistory = EmptyModulesApiHistory, modulesApiHistory = EmptyModulesApiHistory,
kotlinSourceFilesExtensions = kotlinExtensions, kotlinSourceFilesExtensions = kotlinExtensions,
classpathChanges = ClasspathSnapshotDisabled classpathChanges = ClasspathSnapshotDisabled
@@ -127,7 +133,7 @@ open class IncrementalJvmCompilerRunner(
reporter: BuildReporter, reporter: BuildReporter,
private val usePreciseJavaTracking: Boolean, private val usePreciseJavaTracking: Boolean,
buildHistoryFile: File, buildHistoryFile: File,
outputFiles: Collection<File>, outputDirs: Collection<File>?,
private val modulesApiHistory: ModulesApiHistory, private val modulesApiHistory: ModulesApiHistory,
override val kotlinSourceFilesExtensions: List<String> = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS, override val kotlinSourceFilesExtensions: List<String> = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS,
private val classpathChanges: ClasspathChanges, private val classpathChanges: ClasspathChanges,
@@ -136,8 +142,8 @@ open class IncrementalJvmCompilerRunner(
workingDir, workingDir,
"caches-jvm", "caches-jvm",
reporter, reporter,
additionalOutputFiles = outputFiles,
buildHistoryFile = buildHistoryFile, buildHistoryFile = buildHistoryFile,
outputDirs = outputDirs,
withAbiSnapshot = withAbiSnapshot withAbiSnapshot = withAbiSnapshot
) { ) {
override fun createCacheManager(args: K2JVMCompilerArguments, projectDir: File?): IncrementalJvmCachesManager = override fun createCacheManager(args: K2JVMCompilerArguments, projectDir: File?): IncrementalJvmCachesManager =
@@ -181,7 +187,7 @@ open class IncrementalJvmCompilerRunner(
else else
null null
override fun calculateSourcesToCompileImpl( override fun calculateSourcesToCompile(
caches: IncrementalJvmCachesManager, caches: IncrementalJvmCachesManager,
changedFiles: ChangedFiles.Known, changedFiles: ChangedFiles.Known,
args: K2JVMCompilerArguments, args: K2JVMCompilerArguments,
@@ -199,9 +205,8 @@ open class IncrementalJvmCompilerRunner(
//TODO can't use the same way as for build-history files because abi-snapshot for all dependencies should be stored into last-build //TODO can't use the same way as for build-history files because abi-snapshot for all dependencies should be stored into last-build
// and not only changed one // and not only changed one
// (but possibly we dont need to read it all and may be it is possible to update only those who was changed) // (but possibly we dont need to read it all and may be it is possible to update only those who was changed)
override fun setupJarDependencies(args: K2JVMCompilerArguments, withSnapshot: Boolean, reporter: BuildReporter): Map<String, AbiSnapshot> { override fun setupJarDependencies(args: K2JVMCompilerArguments, reporter: BuildReporter): Map<String, AbiSnapshot> {
//fill abiSnapshots //fill abiSnapshots
if (!withSnapshot) return emptyMap()
val abiSnapshots = HashMap<String, AbiSnapshot>() val abiSnapshots = HashMap<String, AbiSnapshot>()
args.classpathAsList args.classpathAsList
.filter { it.extension.equals("jar", ignoreCase = true) } .filter { it.extension.equals("jar", ignoreCase = true) }
@@ -209,7 +214,12 @@ open class IncrementalJvmCompilerRunner(
modulesApiHistory.abiSnapshot(it).let { result -> modulesApiHistory.abiSnapshot(it).let { result ->
if (result is Either.Success<Set<File>>) { if (result is Either.Success<Set<File>>) {
result.value.forEach { file -> result.value.forEach { file ->
AbiSnapshotImpl.read(file, reporter)?.also { abiSnapshot -> abiSnapshots[it.absolutePath] = abiSnapshot } if (file.exists()) {
abiSnapshots[it.absolutePath] = AbiSnapshotImpl.read(file)
} else {
// FIXME: We should throw an exception here
reporter.warn { "Snapshot file does not exist: ${file.path}. Continue anyway." }
}
} }
} }
} }
@@ -228,7 +238,7 @@ open class IncrementalJvmCompilerRunner(
caches: IncrementalJvmCachesManager, caches: IncrementalJvmCachesManager,
changedFiles: ChangedFiles.Known, changedFiles: ChangedFiles.Known,
args: K2JVMCompilerArguments, args: K2JVMCompilerArguments,
abiSnapshots: Map<String, AbiSnapshot> = HashMap(), abiSnapshots: Map<String, AbiSnapshot>,
withAbiSnapshot: Boolean withAbiSnapshot: Boolean
): CompilationMode { ): CompilationMode {
val dirtyFiles = DirtyFilesContainer(caches, reporter, kotlinSourceFilesExtensions) val dirtyFiles = DirtyFilesContainer(caches, reporter, kotlinSourceFilesExtensions)
@@ -370,7 +380,9 @@ open class IncrementalJvmCompilerRunner(
return result return result
} }
override fun preBuildHook(args: K2JVMCompilerArguments, compilationMode: CompilationMode) { override fun performWorkBeforeCompilation(compilationMode: CompilationMode, args: K2JVMCompilerArguments) {
super.performWorkBeforeCompilation(compilationMode, args)
if (compilationMode is CompilationMode.Incremental) { if (compilationMode is CompilationMode.Incremental) {
val destinationDir = args.destinationAsFile val destinationDir = args.destinationAsFile
destinationDir.mkdirs() destinationDir.mkdirs()
@@ -478,12 +490,15 @@ open class IncrementalJvmCompilerRunner(
return exitCode to sourcesToCompile return exitCode to sourcesToCompile
} }
override fun performWorkAfterSuccessfulCompilation(caches: IncrementalJvmCachesManager, wasIncremental: Boolean) { override fun performWorkAfterCompilation(compilationMode: CompilationMode, exitCode: ExitCode, caches: IncrementalJvmCachesManager) {
if (classpathChanges is ClasspathChanges.ClasspathSnapshotEnabled) { super.performWorkAfterCompilation(compilationMode, exitCode, caches)
// No need to shrink and save classpath snapshot if exitCode != ExitCode.OK as the task will fail anyway
if (classpathChanges is ClasspathChanges.ClasspathSnapshotEnabled && exitCode == ExitCode.OK) {
reporter.measure(BuildTime.SHRINK_AND_SAVE_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION) { reporter.measure(BuildTime.SHRINK_AND_SAVE_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION) {
shrinkAndSaveClasspathSnapshot( shrinkAndSaveClasspathSnapshot(
wasIncremental, classpathChanges, caches.lookupCache, currentClasspathSnapshot, compilationWasIncremental = compilationMode is CompilationMode.Incremental, classpathChanges, caches.lookupCache,
shrunkCurrentClasspathAgainstPreviousLookups, ClasspathSnapshotBuildReporter(reporter) currentClasspathSnapshot, shrunkCurrentClasspathAgainstPreviousLookups, ClasspathSnapshotBuildReporter(reporter)
) )
} }
} }
@@ -213,7 +213,8 @@ abstract class AbstractIncrementalMultiModuleCompilerRunnerTest<Args : CommonCom
val moduleModifiedDependencies = modifiedLibraries.filter { it.first in moduleDependencies }.map { it.second } val moduleModifiedDependencies = modifiedLibraries.filter { it.first in moduleDependencies }.map { it.second }
val moduleDeletedDependencies = deletedLibraries.filter { it.first in moduleDependencies }.map { it.second } val moduleDeletedDependencies = deletedLibraries.filter { it.first in moduleDependencies }.map { it.second }
val changedDepsFiles = if (isInitial) null else ChangedFiles.Dependencies(moduleModifiedDependencies, moduleDeletedDependencies) val changedDepsFiles =
if (isInitial) null else ChangedFiles.Known(moduleModifiedDependencies, moduleDeletedDependencies, forDependencies = true)
val moduleOutDir = File(outDir, module) val moduleOutDir = File(outDir, module)
val moduleCacheDir = File(cacheDir, module) val moduleCacheDir = File(cacheDir, module)
@@ -2,6 +2,7 @@ package org.jetbrains.kotlin.gradle
import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.BuildResult
import org.gradle.util.GradleVersion import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.build.report.metrics.BuildAttribute
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.util.checkedReplace import org.jetbrains.kotlin.gradle.util.checkedReplace
@@ -735,7 +736,7 @@ abstract class BaseIncrementalCompilationMultiProjectIT : IncrementalCompilation
// In the next build, compilation should be incremental and fail, then fall back to non-incremental compilation and succeed // In the next build, compilation should be incremental and fail, then fall back to non-incremental compilation and succeed
build(":lib:compileKotlin") { build(":lib:compileKotlin") {
assertIncrementalCompilationFellBackToNonIncremental() assertIncrementalCompilationFellBackToNonIncremental(BuildAttribute.IC_FAILED_TO_COMPILE_INCREMENTALLY)
// Also check that the output is not deleted (regression test for KT-49780) // Also check that the output is not deleted (regression test for KT-49780)
assertFileExists(lookupFile) assertFileExists(lookupFile)
} }
@@ -96,8 +96,10 @@ fun BuildResult.assertNonIncrementalCompilation(reason: BuildAttribute? = null)
} else { } else {
assertOutputContains(NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED) assertOutputContains(NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED)
} }
// Also check that incremental compilation was not attempted, failed, and fell back to non-incremental compilation
assertOutputDoesNotContain(PERFORMING_INCREMENTAL_COMPILATION) // Also check that the other cases didn't happen
assertOutputDoesNotContain(INCREMENTAL_COMPILATION_COMPLETED)
assertOutputDoesNotContain(FALLING_BACK_TO_NON_INCREMENTAL_COMPILATION)
} }
/** /**
@@ -108,9 +110,11 @@ fun BuildResult.assertNonIncrementalCompilation(reason: BuildAttribute? = null)
* Note: Log level of output must be set to [LogLevel.DEBUG]. * Note: Log level of output must be set to [LogLevel.DEBUG].
*/ */
fun BuildResult.assertIncrementalCompilation(expectedCompiledKotlinFiles: Iterable<Path>? = null) { fun BuildResult.assertIncrementalCompilation(expectedCompiledKotlinFiles: Iterable<Path>? = null) {
assertOutputContains(PERFORMING_INCREMENTAL_COMPILATION) assertOutputContains(INCREMENTAL_COMPILATION_COMPLETED)
// Also check that incremental compilation did not fail and fall back to non-incremental compilation
// Also check that the other cases didn't happen
assertOutputDoesNotContain(NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED) assertOutputDoesNotContain(NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED)
assertOutputDoesNotContain(FALLING_BACK_TO_NON_INCREMENTAL_COMPILATION)
expectedCompiledKotlinFiles?.let { expectedCompiledKotlinFiles?.let {
assertSameFiles(expected = it, actual = extractCompiledKotlinFiles(output), "Compiled Kotlin files differ:\n") assertSameFiles(expected = it, actual = extractCompiledKotlinFiles(output), "Compiled Kotlin files differ:\n")
@@ -122,9 +126,19 @@ fun BuildResult.assertIncrementalCompilation(expectedCompiledKotlinFiles: Iterab
* *
* Note: Log level of output must be set to [LogLevel.DEBUG]. * Note: Log level of output must be set to [LogLevel.DEBUG].
*/ */
fun BuildResult.assertIncrementalCompilationFellBackToNonIncremental() { fun BuildResult.assertIncrementalCompilationFellBackToNonIncremental(reason: BuildAttribute? = null) {
assertOutputContains("$NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED: ${BuildAttribute.INCREMENTAL_COMPILATION_FAILED.name}") if (reason != null) {
assertOutputContains("$FALLING_BACK_TO_NON_INCREMENTAL_COMPILATION (reason = ${reason.name})")
} else {
assertOutputContains(FALLING_BACK_TO_NON_INCREMENTAL_COMPILATION)
}
// Also check that the other cases didn't happen
assertOutputDoesNotContain(INCREMENTAL_COMPILATION_COMPLETED)
assertOutputDoesNotContain(NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED)
} }
private const val PERFORMING_INCREMENTAL_COMPILATION = "Performing incremental compilation" // Each of the following messages should uniquely correspond to a case in `IncrementalCompilerRunner.ICResult`
private const val INCREMENTAL_COMPILATION_COMPLETED = "Incremental compilation completed"
const val NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED = "Non-incremental compilation will be performed" const val NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED = "Non-incremental compilation will be performed"
private const val FALLING_BACK_TO_NON_INCREMENTAL_COMPILATION = "Falling back to non-incremental compilation"
@@ -5,7 +5,6 @@
package org.jetbrains.kotlin.compilerRunner package org.jetbrains.kotlin.compilerRunner
import org.gradle.api.GradleException
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.Logging import org.gradle.api.logging.Logging
@@ -18,9 +17,7 @@ import org.gradle.workers.WorkerExecutor
import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter
import org.jetbrains.kotlin.build.report.metrics.BuildTime import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.measure import org.jetbrains.kotlin.build.report.metrics.measure
import org.jetbrains.kotlin.gradle.tasks.GradleCompileTaskProvider import org.jetbrains.kotlin.gradle.tasks.*
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
import org.jetbrains.kotlin.gradle.tasks.TaskOutputsBackup
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@@ -43,7 +40,7 @@ internal class GradleCompilerRunnerWithWorkers(
workQueue.submit(GradleKotlinCompilerWorkAction::class.java) { params -> workQueue.submit(GradleKotlinCompilerWorkAction::class.java) { params ->
params.compilerWorkArguments.set(workArgs) params.compilerWorkArguments.set(workArgs)
if (taskOutputsBackup != null) { if (taskOutputsBackup != null) {
params.taskOutputs.set(taskOutputsBackup.outputs) params.taskOutputsToRestore.set(taskOutputsBackup.outputsToRestore)
params.buildDir.set(taskOutputsBackup.buildDirectory) params.buildDir.set(taskOutputsBackup.buildDirectory)
params.snapshotsDir.set(taskOutputsBackup.snapshotsDir) params.snapshotsDir.set(taskOutputsBackup.snapshotsDir)
params.metricsReporter.set(buildMetrics) params.metricsReporter.set(buildMetrics)
@@ -64,8 +61,7 @@ internal class GradleCompilerRunnerWithWorkers(
fileSystemOperations, fileSystemOperations,
parameters.buildDir, parameters.buildDir,
parameters.snapshotsDir, parameters.snapshotsDir,
parameters.taskOutputs.get(), parameters.taskOutputsToRestore.get(),
outputsToExclude = emptyList(),
logger logger
) )
} else { } else {
@@ -76,11 +72,14 @@ internal class GradleCompilerRunnerWithWorkers(
GradleKotlinCompilerWork( GradleKotlinCompilerWork(
parameters.compilerWorkArguments.get() parameters.compilerWorkArguments.get()
).run() ).run()
} catch (e: GradleException) { } catch (e: FailedCompilationException) {
// Currently, metrics are not reported as in the worker we are getting new instance of [BuildMetricsReporter] // Restore outputs only in cases where we expect that the user will make some changes to their project:
// [BuildDataRecorder] knows nothing about this new instance. Possibly could be fixed in the future by migrating // - For a compilation error, the user will need to fix their source code
// [BuildMetricsReporter] to be shared Gradle service. // - For an OOM error, the user will need to increase their memory settings
if (taskOutputsBackup != null) { // 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 (taskOutputsBackup != null && (e is CompilationErrorException || e is OOMErrorException)) {
parameters.metricsReporter.get().measure(BuildTime.RESTORE_OUTPUT_FROM_BACKUP) { parameters.metricsReporter.get().measure(BuildTime.RESTORE_OUTPUT_FROM_BACKUP) {
logger.info("Restoring task outputs to pre-compilation state") logger.info("Restoring task outputs to pre-compilation state")
taskOutputsBackup.restoreOutputs() taskOutputsBackup.restoreOutputs()
@@ -96,7 +95,7 @@ internal class GradleCompilerRunnerWithWorkers(
internal interface GradleKotlinCompilerWorkParameters : WorkParameters { internal interface GradleKotlinCompilerWorkParameters : WorkParameters {
val compilerWorkArguments: Property<GradleKotlinCompilerWorkArguments> val compilerWorkArguments: Property<GradleKotlinCompilerWorkArguments>
val taskOutputs: ListProperty<File> val taskOutputsToRestore: ListProperty<File>
val snapshotsDir: DirectoryProperty val snapshotsDir: DirectoryProperty
val buildDir: DirectoryProperty val buildDir: DirectoryProperty
val metricsReporter: Property<BuildMetricsReporter> val metricsReporter: Property<BuildMetricsReporter>
@@ -5,7 +5,6 @@
package org.jetbrains.kotlin.compilerRunner package org.jetbrains.kotlin.compilerRunner
import org.gradle.api.GradleException
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.invocation.Gradle import org.gradle.api.invocation.Gradle
import org.gradle.api.logging.Logger import org.gradle.api.logging.Logger
@@ -33,11 +32,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaTarget
import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService
import org.jetbrains.kotlin.gradle.plugin.variantImplementationFactory import org.jetbrains.kotlin.gradle.plugin.variantImplementationFactory
import org.jetbrains.kotlin.gradle.tasks.* import org.jetbrains.kotlin.gradle.tasks.*
import org.jetbrains.kotlin.gradle.utils.IsolatedKotlinClasspathClassCastException import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.gradle.utils.archivePathCompatible
import org.jetbrains.kotlin.gradle.utils.findByType
import org.jetbrains.kotlin.gradle.utils.newTmpFile
import org.jetbrains.kotlin.gradle.utils.relativeOrCanonical
import org.jetbrains.kotlin.incremental.IncrementalModuleEntry import org.jetbrains.kotlin.incremental.IncrementalModuleEntry
import org.jetbrains.kotlin.incremental.IncrementalModuleInfo import org.jetbrains.kotlin.incremental.IncrementalModuleInfo
import org.jetbrains.kotlin.statistics.metrics.BooleanMetrics import org.jetbrains.kotlin.statistics.metrics.BooleanMetrics
@@ -223,8 +218,9 @@ internal open class GradleCompilerRunner(
try { try {
val kotlinCompilerRunnable = GradleKotlinCompilerWork(workArgs) val kotlinCompilerRunnable = GradleKotlinCompilerWork(workArgs)
kotlinCompilerRunnable.run() kotlinCompilerRunnable.run()
} catch (e: GradleException) { } catch (e: FailedCompilationException) {
if (taskOutputsBackup != null) { // Restore outputs only for CompilationErrorException or OOMErrorException (see GradleKotlinCompilerWorkAction.execute)
if (taskOutputsBackup != null && (e is CompilationErrorException || e is OOMErrorException)) {
buildMetrics.measure(BuildTime.RESTORE_OUTPUT_FROM_BACKUP) { buildMetrics.measure(BuildTime.RESTORE_OUTPUT_FROM_BACKUP) {
taskOutputsBackup.restoreOutputs() taskOutputsBackup.restoreOutputs()
} }
@@ -17,7 +17,7 @@ import org.jetbrains.kotlin.gradle.plugin.internal.state.TaskLoggers
import org.jetbrains.kotlin.gradle.report.* import org.jetbrains.kotlin.gradle.report.*
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
import org.jetbrains.kotlin.gradle.tasks.cleanOutputsAndLocalState import org.jetbrains.kotlin.gradle.tasks.cleanOutputsAndLocalState
import org.jetbrains.kotlin.gradle.tasks.throwGradleExceptionIfError import org.jetbrains.kotlin.gradle.tasks.throwExceptionIfCompilationFailed
import org.jetbrains.kotlin.gradle.utils.stackTraceAsString import org.jetbrains.kotlin.gradle.utils.stackTraceAsString
import org.jetbrains.kotlin.incremental.ChangedFiles import org.jetbrains.kotlin.incremental.ChangedFiles
import org.jetbrains.kotlin.incremental.ClasspathChanges import org.jetbrains.kotlin.incremental.ClasspathChanges
@@ -125,7 +125,7 @@ internal class GradleKotlinCompilerWork @Inject constructor(
incrementalCompilationEnvironment.multiModuleICSettings.buildHistoryFile.delete() incrementalCompilationEnvironment.multiModuleICSettings.buildHistoryFile.delete()
} }
throwGradleExceptionIfError(exitCode, executionStrategy) throwExceptionIfCompilationFailed(exitCode, executionStrategy)
} finally { } finally {
val taskInfo = TaskExecutionInfo( val taskInfo = TaskExecutionInfo(
changedFiles = incrementalCompilationEnvironment?.changedFiles, changedFiles = incrementalCompilationEnvironment?.changedFiles,
@@ -424,9 +424,7 @@ abstract class BuildReportsService : BuildService<BuildReportsService.Parameters
taskExecutionResult?.buildMetrics?.buildPerformanceMetrics?.asMap()?.filterValues { value -> value != 0L } ?: emptyMap() taskExecutionResult?.buildMetrics?.buildPerformanceMetrics?.asMap()?.filterValues { value -> value != 0L } ?: emptyMap()
val changes = when (val changedFiles = taskExecutionResult?.taskInfo?.changedFiles) { val changes = when (val changedFiles = taskExecutionResult?.taskInfo?.changedFiles) {
is ChangedFiles.Known -> changedFiles.modified.map { it.absolutePath } + changedFiles.removed.map { it.absolutePath } is ChangedFiles.Known -> changedFiles.modified.map { it.absolutePath } + changedFiles.removed.map { it.absolutePath }
is ChangedFiles.Dependencies -> changedFiles.modified.map { it.absolutePath } + changedFiles.removed.map { it.absolutePath }
else -> emptyList<String>() else -> emptyList<String>()
} }
return CompileStatisticsData( return CompileStatisticsData(
durationMs = durationMs, durationMs = durationMs,
@@ -141,7 +141,7 @@ abstract class KotlinJsDce @Inject constructor(
buildDir.get().asFile, buildDir.get().asFile,
jvmArgs jvmArgs
) )
throwGradleExceptionIfError(exitCode, KotlinCompilerExecutionStrategy.OUT_OF_PROCESS) throwExceptionIfCompilationFailed(exitCode, KotlinCompilerExecutionStrategy.OUT_OF_PROCESS)
} }
private fun isDceCandidate(file: File): Boolean { private fun isDceCandidate(file: File): Boolean {
@@ -363,7 +363,7 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments> @Inject constr
private val systemPropertiesService = CompilerSystemPropertiesService.registerIfAbsent(project.gradle) private val systemPropertiesService = CompilerSystemPropertiesService.registerIfAbsent(project.gradle)
/** Task outputs that we don't want to include in [TaskOutputsBackup] (see [TaskOutputsBackup]'s kdoc for more info). */ /** Task outputs that we don't want to include in [TaskOutputsBackup] (see [TaskOutputsBackup.outputsToRestore] for more info). */
@get:Internal @get:Internal
protected open val taskOutputsBackupExcludes: List<File> = emptyList() protected open val taskOutputsBackupExcludes: List<File> = emptyList()
@@ -391,8 +391,7 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments> @Inject constr
fileSystemOperations, fileSystemOperations,
layout.buildDirectory, layout.buildDirectory,
layout.buildDirectory.dir("snapshot/kotlin/$name"), layout.buildDirectory.dir("snapshot/kotlin/$name"),
allOutputFiles(), outputsToRestore = allOutputFiles() - taskOutputsBackupExcludes,
taskOutputsBackupExcludes,
logger logger
).also { ).also {
it.createSnapshot() it.createSnapshot()
@@ -596,7 +595,6 @@ abstract class KotlinCompile @Inject constructor(
classpathSnapshotProperties.classpathSnapshot classpathSnapshotProperties.classpathSnapshot
) )
// Exclude classpathSnapshotDir from TaskOutputsBackup (see TaskOutputsBackup's kdoc for more info). */
override val taskOutputsBackupExcludes: List<File> override val taskOutputsBackupExcludes: List<File>
get() = classpathSnapshotProperties.classpathSnapshotDir.orNull?.asFile?.let { listOf(it) } ?: emptyList() get() = classpathSnapshotProperties.classpathSnapshotDir.orNull?.asFile?.let { listOf(it) } ?: emptyList()
@@ -5,7 +5,9 @@
package org.jetbrains.kotlin.gradle.tasks package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.file.* import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.logging.Logger import org.gradle.api.logging.Logger
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import java.io.File import java.io.File
@@ -14,35 +16,31 @@ import java.nio.file.FileSystems
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import java.util.zip.* import java.util.zip.Deflater
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
internal class TaskOutputsBackup( internal class TaskOutputsBackup(
val fileSystemOperations: FileSystemOperations, private val fileSystemOperations: FileSystemOperations,
val buildDirectory: DirectoryProperty, val buildDirectory: DirectoryProperty,
val snapshotsDir: Provider<Directory>, val snapshotsDir: Provider<Directory>,
allOutputs: List<File>,
/** /**
* Task outputs that we don't want to back up for performance reasons (e.g., if (1) they are too big, and (2) they are usually updated * Task outputs to back up and restore.
* only at the end of the task execution--in a failed task run, they are usually unchanged and therefore don't need to be restored).
* *
* NOTE: In `IncrementalCompilerRunner`, if incremental compilation fails, it will try again by cleaning all the outputs and perform * Note that this could be a subset of all the outputs of a task because there could be task outputs that we don't want to back up and
* non-incremental compilation. It is important that `IncrementalCompilerRunner` do not clean [outputsToExclude] immediately but only * restore (e.g., if (1) they are too big and (2) they are updated only at the end of the task execution so in a failed task run, they
* right before [outputsToExclude] are updated (which is usually at the end of the task execution). This is so that if the fallback * are usually unchanged and therefore don't need to be restored).
* compilation fails, [outputsToExclude] will remain unchanged and the other outputs will be restored, and the next task run can be
* incremental.
*/ */
outputsToExclude: List<File> = emptyList(), val outputsToRestore: List<File>,
val logger: Logger val logger: Logger
) { ) {
/** The outputs to back up and restore. Note that this may be a subset of all the outputs of a task (see `outputsToExclude`). */
val outputs: List<File> = allOutputs - outputsToExclude.toSet()
fun createSnapshot() { fun createSnapshot() {
// Kotlin JS compilation task declares one file from 'destinationDirectory' output as task `@OutputFile' // Kotlin JS compilation task declares one file from 'destinationDirectory' output as task `@OutputFile'
// property. To avoid snapshot sync collisions, each snapshot output directory has also 'index' as prefix. // property. To avoid snapshot sync collisions, each snapshot output directory has also 'index' as prefix.
outputs.toSortedSet().forEachIndexed { index, outputPath -> outputsToRestore.toSortedSet().forEachIndexed { index, outputPath ->
if (outputPath.isDirectory && !outputPath.isEmptyDirectory) { if (outputPath.isDirectory && !outputPath.isEmptyDirectory) {
compressDirectoryToZip( compressDirectoryToZip(
File(snapshotsDir.get().asFile, index.asSnapshotArchiveName), File(snapshotsDir.get().asFile, index.asSnapshotArchiveName),
@@ -59,10 +57,10 @@ internal class TaskOutputsBackup(
fun restoreOutputs() { fun restoreOutputs() {
fileSystemOperations.delete { fileSystemOperations.delete {
it.delete(outputs) it.delete(outputsToRestore)
} }
outputs.toSortedSet().forEachIndexed { index, outputPath -> outputsToRestore.toSortedSet().forEachIndexed { index, outputPath ->
val snapshotDir = snapshotsDir.get().file(index.asSnapshotDirectoryName).asFile val snapshotDir = snapshotsDir.get().file(index.asSnapshotDirectoryName).asFile
if (snapshotDir.isDirectory) { if (snapshotDir.isDirectory) {
fileSystemOperations.copy { spec -> fileSystemOperations.copy { spec ->
@@ -1,6 +1,5 @@
package org.jetbrains.kotlin.gradle.tasks package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.GradleException
import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter
import org.jetbrains.kotlin.build.report.metrics.BuildTime import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.measure import org.jetbrains.kotlin.build.report.metrics.measure
@@ -10,18 +9,19 @@ import org.jetbrains.kotlin.gradle.internal.tasks.TaskWithLocalState
import org.jetbrains.kotlin.gradle.internal.tasks.allOutputFiles import org.jetbrains.kotlin.gradle.internal.tasks.allOutputFiles
import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger
import org.jetbrains.kotlin.gradle.logging.kotlinDebug import org.jetbrains.kotlin.gradle.logging.kotlinDebug
import org.jetbrains.kotlin.incremental.cleanDirectoryContents import org.jetbrains.kotlin.incremental.deleteDirectoryContents
import org.jetbrains.kotlin.incremental.deleteRecursivelyOrThrow import org.jetbrains.kotlin.incremental.deleteRecursivelyOrThrow
import java.io.File import java.io.File
fun throwGradleExceptionIfError( /** Throws [FailedCompilationException] if compilation completed with [exitCode] != [ExitCode.OK]. */
fun throwExceptionIfCompilationFailed(
exitCode: ExitCode, exitCode: ExitCode,
executionStrategy: KotlinCompilerExecutionStrategy executionStrategy: KotlinCompilerExecutionStrategy
) { ) {
when (exitCode) { when (exitCode) {
ExitCode.COMPILATION_ERROR -> throw GradleException("Compilation error. See log for more details") ExitCode.COMPILATION_ERROR -> throw CompilationErrorException("Compilation error. See log for more details")
ExitCode.INTERNAL_ERROR -> throw GradleException("Internal compiler error. See log for more details") ExitCode.INTERNAL_ERROR -> throw FailedCompilationException("Internal compiler error. See log for more details")
ExitCode.SCRIPT_EXECUTION_ERROR -> throw GradleException("Script execution error. See log for more details") ExitCode.SCRIPT_EXECUTION_ERROR -> throw FailedCompilationException("Script execution error. See log for more details")
ExitCode.OOM_ERROR -> { ExitCode.OOM_ERROR -> {
var exceptionMessage = "Not enough memory to run compilation." var exceptionMessage = "Not enough memory to run compilation."
when (executionStrategy) { when (executionStrategy) {
@@ -31,13 +31,22 @@ fun throwGradleExceptionIfError(
exceptionMessage += " Try to increase it via 'gradle.properties':\norg.gradle.jvmargs=-Xmx<size>" exceptionMessage += " Try to increase it via 'gradle.properties':\norg.gradle.jvmargs=-Xmx<size>"
KotlinCompilerExecutionStrategy.OUT_OF_PROCESS -> Unit KotlinCompilerExecutionStrategy.OUT_OF_PROCESS -> Unit
} }
throw GradleException(exceptionMessage) throw OOMErrorException(exceptionMessage)
} }
ExitCode.OK -> Unit ExitCode.OK -> Unit
else -> throw IllegalStateException("Unexpected exit code: $exitCode") else -> throw IllegalStateException("Unexpected exit code: $exitCode")
} }
} }
/** Exception thrown when [ExitCode] != [ExitCode.OK]. */
internal open class FailedCompilationException(message: String) : RuntimeException(message)
/** Exception thrown when [ExitCode] == [ExitCode.COMPILATION_ERROR]. */
internal class CompilationErrorException(message: String) : FailedCompilationException(message)
/** Exception thrown when [ExitCode] == [ExitCode.OOM_ERROR]. */
internal class OOMErrorException(message: String) : FailedCompilationException(message)
internal fun TaskWithLocalState.cleanOutputsAndLocalState(reason: String? = null) { internal fun TaskWithLocalState.cleanOutputsAndLocalState(reason: String? = null) {
val log = GradleKotlinLogger(logger) val log = GradleKotlinLogger(logger)
cleanOutputsAndLocalState(allOutputFiles(), log, metrics.get(), reason) cleanOutputsAndLocalState(allOutputFiles(), log, metrics.get(), reason)
@@ -59,7 +68,7 @@ internal fun cleanOutputsAndLocalState(
when { when {
file.isDirectory -> { file.isDirectory -> {
log.debug("Deleting contents of output directory: $file") log.debug("Deleting contents of output directory: $file")
file.cleanDirectoryContents() file.deleteDirectoryContents()
} }
file.isFile -> { file.isFile -> {
log.debug("Deleting output file: $file") log.debug("Deleting output file: $file")