All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.kt Maven / Gradle / Ivy

There is a newer version: 2.0.0
Show newest version
/*
 * Copyright 2010-2017 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.incremental

import org.jetbrains.kotlin.build.DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS
import org.jetbrains.kotlin.build.GeneratedFile
import org.jetbrains.kotlin.build.report.BuildReporter
import org.jetbrains.kotlin.build.report.debug
import org.jetbrains.kotlin.build.report.info
import org.jetbrains.kotlin.build.report.metrics.BuildAttribute
import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric
import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.measure
import org.jetbrains.kotlin.build.report.warn
import org.jetbrains.kotlin.cli.common.*
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.compilerRunner.MessageCollectorToOutputItemsCollectorAdapter
import org.jetbrains.kotlin.compilerRunner.OutputItemsCollectorImpl
import org.jetbrains.kotlin.compilerRunner.SimpleOutputItem
import org.jetbrains.kotlin.compilerRunner.toGeneratedFile
import org.jetbrains.kotlin.config.Services
import org.jetbrains.kotlin.incremental.components.ExpectActualTracker
import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.incremental.parsing.classesFqNames
import org.jetbrains.kotlin.incremental.util.BufferingMessageCollector
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
import org.jetbrains.kotlin.util.suffixIfNot
import java.io.File

abstract class IncrementalCompilerRunner<
        Args : CommonCompilerArguments,
        CacheManager : IncrementalCachesManager<*>
        >(
    private val workingDir: File,
    cacheDirName: String,
    protected val reporter: BuildReporter,
    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 = emptyList(),
    protected val withAbiSnapshot: Boolean = false
) {

    protected val cacheDirectory = File(workingDir, cacheDirName)
    protected val dirtySourcesSinceLastTimeFile = File(workingDir, DIRTY_SOURCES_FILE_NAME)
    protected val lastBuildInfoFile = File(workingDir, LAST_BUILD_INFO_FILE_NAME)
    private val abiSnapshotFile = File(workingDir, ABI_SNAPSHOT_FILE_NAME)
    protected open val kotlinSourceFilesExtensions: List = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS

    protected abstract fun createCacheManager(args: Args, projectDir: File?): CacheManager
    protected abstract fun destinationDir(args: Args): File

    fun compile(
        allSourceFiles: List,
        args: Args,
        messageCollector: MessageCollector,
        // when [providedChangedFiles] is not null, changes are provided by external system (e.g. Gradle)
        // otherwise we track source files changes ourselves.
        providedChangedFiles: ChangedFiles?,
        projectDir: File? = null
    ): ExitCode = reporter.measure(BuildTime.INCREMENTAL_COMPILATION_DAEMON) {
        try {
            compileImpl(allSourceFiles, args, messageCollector, providedChangedFiles, projectDir)
        } finally {
            reporter.measure(BuildTime.CALCULATE_OUTPUT_SIZE) {
                reporter.addMetric(
                    BuildPerformanceMetric.SNAPSHOT_SIZE,
                    buildHistoryFile.length() + lastBuildInfoFile.length() + abiSnapshotFile.length()
                )
                if (cacheDirectory.exists() && cacheDirectory.isDirectory()) {
                    cacheDirectory.walkTopDown().filter { it.isFile }.map { it.length() }.sum().let {
                        reporter.addMetric(BuildPerformanceMetric.CACHE_DIRECTORY_SIZE, it)
                    }
                }
            }
        }
    }

    fun rebuild(
        reason: BuildAttribute,
        allSourceFiles: List,
        args: Args,
        messageCollector: MessageCollector,
        providedChangedFiles: ChangedFiles?,
        projectDir: File? = null,
        classpathAbiSnapshot: Map
    ): 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(
        allSourceFiles: List,
        args: Args,
        messageCollector: MessageCollector,
        providedChangedFiles: ChangedFiles?,
        projectDir: File? = null
    ): ExitCode {
        var caches = createCacheManager(args, projectDir)
        var rebuildReason = BuildAttribute.INTERNAL_ERROR

        val classpathAbiSnapshot =
            if (withAbiSnapshot) {
                reporter.info { "Incremental compilation with ABI snapshot enabled" }
                reporter.measure(BuildTime.SET_UP_ABI_SNAPSHOTS) {
                    setupJarDependencies(args, withAbiSnapshot, reporter)
                }
            } else {
                emptyMap()
            }

        try {
            val changedFiles = when (providedChangedFiles) {
                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.
     *
     * If the directories do not yet exist, they will be created.
     */
    private fun cleanOutputsAndLocalStateOnRebuild(args: Args) {
        // Use Set as additionalOutputFiles may already contain destinationDir and workingDir
        val outputFiles = setOf(destinationDir(args), workingDir) + additionalOutputFiles

        reporter.debug { "Cleaning outputs on rebuild" }
        outputFiles.forEach {
            when {
                it.isDirectory -> {
                    reporter.debug { "  Deleting contents of directory '${it.path}'" }
                    it.cleanDirectoryContents()
                }
                it.isFile -> {
                    reporter.debug { "  Deleting file '${it.path}'" }
                    it.deleteRecursivelyOrThrow()
                }
            }
        }
    }

    private fun sourcesToCompile(
        caches: CacheManager,
        changedFiles: ChangedFiles,
        args: Args,
        messageCollector: MessageCollector,
        dependenciesAbiSnapshots: Map
    ): CompilationMode =
        when (changedFiles) {
            is ChangedFiles.Known -> calculateSourcesToCompile(caches, changedFiles, args, messageCollector, dependenciesAbiSnapshots)
            is ChangedFiles.Unknown -> CompilationMode.Rebuild(BuildAttribute.UNKNOWN_CHANGES_IN_GRADLE_INPUTS)
            is ChangedFiles.Dependencies -> error("Unexpected ChangedFiles type (ChangedFiles.Dependencies)")
        }

    private fun calculateSourcesToCompile(
        caches: CacheManager, changedFiles: ChangedFiles.Known, args: Args, messageCollector: MessageCollector,
        abiSnapshots: Map
    ): CompilationMode =
        reporter.measure(BuildTime.IC_CALCULATE_INITIAL_DIRTY_SET) {
            calculateSourcesToCompileImpl(caches, changedFiles, args, messageCollector, abiSnapshots)
        }

    protected abstract fun calculateSourcesToCompileImpl(
        caches: CacheManager,
        changedFiles: ChangedFiles.Known,
        args: Args,
        messageCollector: MessageCollector,
        classpathAbiSnapshots: Map
    ): CompilationMode

    protected open fun setupJarDependencies(args: Args, withSnapshot: Boolean, reporter: BuildReporter): Map = mapOf()

    protected fun initDirtyFiles(dirtyFiles: DirtyFilesContainer, changedFiles: ChangedFiles.Known) {
        dirtyFiles.add(changedFiles.modified, "was modified since last time")
        dirtyFiles.add(changedFiles.removed, "was removed since last time")

        if (dirtySourcesSinceLastTimeFile.exists()) {
            val files = dirtySourcesSinceLastTimeFile.readLines().map(::File)
            dirtyFiles.add(files, "was not compiled last time")
        }
    }

    protected sealed class CompilationMode {
        class Incremental(val dirtyFiles: DirtyFilesContainer) : CompilationMode()
        class Rebuild(val reason: BuildAttribute) : CompilationMode()
    }

    protected abstract fun updateCaches(
        services: Services,
        caches: CacheManager,
        generatedFiles: List,
        changesCollector: ChangesCollector
    )

    protected open fun preBuildHook(args: Args, compilationMode: CompilationMode) {}
    protected open fun additionalDirtyFiles(caches: CacheManager, generatedFiles: List, services: Services): Iterable =
        emptyList()

    protected open fun additionalDirtyLookupSymbols(): Iterable =
        emptyList()

    protected open fun makeServices(
        args: Args,
        lookupTracker: LookupTracker,
        expectActualTracker: ExpectActualTracker,
        caches: CacheManager,
        dirtySources: Set,
        isIncremental: Boolean
    ): Services.Builder =
        Services.Builder().apply {
            register(LookupTracker::class.java, lookupTracker)
            register(ExpectActualTracker::class.java, expectActualTracker)
            register(CompilationCanceledStatus::class.java, EmptyCompilationCanceledStatus)
        }

    protected abstract fun runCompiler(
        sourcesToCompile: List,
        args: Args,
        caches: CacheManager,
        services: Services,
        messageCollector: MessageCollector,
        allSources: List,
        isIncremental: Boolean
    ): Pair>

    protected open fun compileIncrementally(
        args: Args,
        caches: CacheManager,
        allKotlinSources: List,
        compilationMode: CompilationMode,
        originalMessageCollector: MessageCollector,
        withSnapshot: Boolean,
        abiSnapshot: AbiSnapshot = AbiSnapshotImpl(mutableMapOf()),
        classpathAbiSnapshot: Map = HashMap()
    ): ExitCode {
        if (compilationMode is CompilationMode.Rebuild) {
            reporter.info { "Non-incremental compilation will be performed: ${compilationMode.reason}" }
        }

        preBuildHook(args, compilationMode)

        val dirtySources = when (compilationMode) {
            is CompilationMode.Incremental -> {
                compilationMode.dirtyFiles.toMutableLinkedSet()
            }
            is CompilationMode.Rebuild -> {
                reporter.addAttribute(compilationMode.reason)
                LinkedHashSet(allKotlinSources)
            }
        }

        val currentBuildInfo = BuildInfo(startTS = System.currentTimeMillis(), classpathAbiSnapshot)
        val buildDirtyLookupSymbols = HashSet()
        val buildDirtyFqNames = HashSet()
        val allDirtySources = HashSet()

        var exitCode = ExitCode.OK

        while (dirtySources.any() || runWithNoDirtyKotlinSources(caches)) {
            val complementaryFiles = caches.platformCache.getComplementaryFilesRecursive(dirtySources)
            dirtySources.addAll(complementaryFiles)
            caches.platformCache.markDirty(dirtySources)
            caches.inputsCache.removeOutputForSourceFiles(dirtySources)

            val lookupTracker = LookupTrackerImpl(LookupTracker.DO_NOTHING)
            val expectActualTracker = ExpectActualTrackerImpl()
            //TODO(valtman) sourceToCompile calculate based on abiSnapshot
            val (sourcesToCompile, removedKotlinSources) = dirtySources.partition(File::exists)

            val services = makeServices(
                args, lookupTracker, expectActualTracker, caches,
                dirtySources.toSet(), compilationMode is CompilationMode.Incremental
            ).build()

            args.reportOutputFiles = true
            val outputItemsCollector = OutputItemsCollectorImpl()
            val bufferingMessageCollector = BufferingMessageCollector()
            val messageCollectorAdapter = MessageCollectorToOutputItemsCollectorAdapter(bufferingMessageCollector, outputItemsCollector)

            val compiledSources = reporter.measure(BuildTime.COMPILATION_ROUND) {
                runCompiler(
                    sourcesToCompile, args, caches, services, messageCollectorAdapter,
                    allKotlinSources, compilationMode is CompilationMode.Incremental
                )
            }.let { (ec, compiled) ->
                exitCode = ec
                compiled
            }

            dirtySources.addAll(compiledSources)
            allDirtySources.addAll(dirtySources)
            val text = allDirtySources.joinToString(separator = System.getProperty("line.separator")) { it.canonicalPath }
            dirtySourcesSinceLastTimeFile.writeText(text)


            val generatedFiles = outputItemsCollector.outputs.map(SimpleOutputItem::toGeneratedFile)
            if (compilationMode is CompilationMode.Incremental) {
                // todo: feels dirty, can this be refactored?
                val dirtySourcesSet = dirtySources.toHashSet()
                val additionalDirtyFiles = additionalDirtyFiles(caches, generatedFiles, services).filter { it !in dirtySourcesSet }
                if (additionalDirtyFiles.isNotEmpty()) {
                    dirtySources.addAll(additionalDirtyFiles)
                    generatedFiles.forEach { it.outputFile.delete() }
                    continue
                }
            }

            reporter.reportCompileIteration(compilationMode is CompilationMode.Incremental, compiledSources, exitCode)
            bufferingMessageCollector.flush(originalMessageCollector)

            if (exitCode != ExitCode.OK) break

            dirtySourcesSinceLastTimeFile.delete()

            val changesCollector = ChangesCollector()
            reporter.measure(BuildTime.IC_UPDATE_CACHES) {
                caches.platformCache.updateComplementaryFiles(dirtySources, expectActualTracker)
                caches.inputsCache.registerOutputForSourceFiles(generatedFiles)
                caches.lookupCache.update(lookupTracker, sourcesToCompile, removedKotlinSources)
                updateCaches(services, caches, generatedFiles, changesCollector)
            }
            if (compilationMode is CompilationMode.Rebuild) {
                if (withSnapshot) {
                    abiSnapshot.protos.putAll(changesCollector.protoDataChanges())
                }
                break
            }

            val (dirtyLookupSymbols, dirtyClassFqNames, forceRecompile) = changesCollector.getDirtyData(
                listOf(caches.platformCache),
                reporter
            )
            val compiledInThisIterationSet = sourcesToCompile.toHashSet()

            val forceToRecompileFiles = mapClassesFqNamesToFiles(listOf(caches.platformCache), forceRecompile, reporter)
            with(dirtySources) {
                clear()
                addAll(mapLookupSymbolsToFiles(caches.lookupCache, dirtyLookupSymbols, reporter, excludes = compiledInThisIterationSet))
                addAll(
                    mapClassesFqNamesToFiles(
                        listOf(caches.platformCache),
                        dirtyClassFqNames,
                        reporter,
                        excludes = compiledInThisIterationSet
                    )
                )
                if (!compiledInThisIterationSet.containsAll(forceToRecompileFiles)) {
                    addAll(forceToRecompileFiles)
                }
            }

            buildDirtyLookupSymbols.addAll(dirtyLookupSymbols)
            buildDirtyFqNames.addAll(dirtyClassFqNames)

            //update
            if (withSnapshot) {
                //TODO(valtman) check method/ kts class remove
                changesCollector.protoDataRemoved().forEach { abiSnapshot.protos.remove(it) }
                abiSnapshot.protos.putAll(changesCollector.protoDataChanges())
            }
        }

        if (exitCode == ExitCode.OK) {
            reporter.measure(BuildTime.STORE_BUILD_INFO) {
                BuildInfo.write(currentBuildInfo, lastBuildInfoFile)

                //write abi snapshot
                if (withSnapshot) {
                    //TODO(valtman) check method/class remove
                    AbiSnapshotImpl.write(abiSnapshot, abiSnapshotFile)
                }
            }
        }
        if (exitCode == ExitCode.OK && compilationMode is CompilationMode.Incremental) {
            buildDirtyLookupSymbols.addAll(additionalDirtyLookupSymbols())
        }

        val dirtyData = DirtyData(buildDirtyLookupSymbols, buildDirtyFqNames)
        processChangesAfterBuild(compilationMode, currentBuildInfo, dirtyData)

        return exitCode
    }

    protected fun getRemovedClassesChanges(
        caches: IncrementalCachesManager<*>,
        changedFiles: ChangedFiles.Known
    ): DirtyData {
        val removedClasses = HashSet()
        val dirtyFiles = changedFiles.modified.filterTo(HashSet()) { it.isKotlinFile(kotlinSourceFilesExtensions) }
        val removedFiles = changedFiles.removed.filterTo(HashSet()) { it.isKotlinFile(kotlinSourceFilesExtensions) }

        val existingClasses = classesFqNames(dirtyFiles)
        val previousClasses = caches.platformCache
            .classesFqNamesBySources(dirtyFiles + removedFiles)
            .map { it.asString() }

        for (fqName in previousClasses) {
            if (fqName !in existingClasses) {
                removedClasses.add(fqName)
            }
        }

        val changesCollector = ChangesCollector()
        removedClasses.forEach { changesCollector.collectSignature(FqName(it), areSubclassesAffected = true) }
        return changesCollector.getDirtyData(listOf(caches.platformCache), reporter)
    }

    open fun runWithNoDirtyKotlinSources(caches: CacheManager): Boolean = false

    private fun processChangesAfterBuild(
        compilationMode: CompilationMode,
        currentBuildInfo: BuildInfo,
        dirtyData: DirtyData
    ) = reporter.measure(BuildTime.IC_WRITE_HISTORY_FILE) {
        val prevDiffs = BuildDiffsStorage.readFromFile(buildHistoryFile, reporter)?.buildDiffs ?: emptyList()
        val newDiff = if (compilationMode is CompilationMode.Incremental) {
            BuildDifference(currentBuildInfo.startTS, true, dirtyData)
        } else {
            val emptyDirtyData = DirtyData()
            BuildDifference(currentBuildInfo.startTS, false, emptyDirtyData)
        }

        //TODO(valtman) old history build should be restored in case of build fail
        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 {
        const val DIRTY_SOURCES_FILE_NAME = "dirty-sources.txt"
        const val LAST_BUILD_INFO_FILE_NAME = "last-build.bin"
        const val ABI_SNAPSHOT_FILE_NAME = "abi-snapshot.bin"
    }

    private object EmptyCompilationCanceledStatus : CompilationCanceledStatus {
        override fun checkCanceled() {
        }
    }

    protected fun reportPerformanceData(defaultPerformanceManager: CommonCompilerPerformanceManager) {
        defaultPerformanceManager.getMeasurementResults().forEach {
            when (it) {
                is CompilerInitializationMeasurement -> reporter.addTimeMetricMs(BuildTime.COMPILER_INITIALIZATION, it.milliseconds)
                is CodeAnalysisMeasurement -> reporter.addTimeMetricMs(BuildTime.CODE_ANALYSIS, it.milliseconds)
                is CodeGenerationMeasurement -> reporter.addTimeMetricMs(BuildTime.CODE_GENERATION, it.milliseconds)
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy