[IC] Relocatable IC caches for projects with custom buildDir

IC caches often contain file paths. To make them relocatable, we need
to convert these file paths into relative paths, relative to a base
directory.
  - If the file paths are source files, we can use the root project
    directory as base.
  - If the file paths are class files, we should use the classes
    directory as base (before this commit, we used the root project
    directory in both cases, that's why we hit KT-58547).

The key changes in this commit include:
  - RelocatableFileToPathConverter: converts paths to relative paths
  - IncrementalCompilationContext: contains 2 different path converters,
    one for source files and one for class files
  - SourceToOutputFilesMap: maps source files to class files using the
    above path converters
  - IncrementalCompilerRunner: creates the path converters based on file
    locations

Test: RelocatableFileToPathConverterTest unit test
      SourceToOutputFilesMapTest unit test
      BuildCacheRelocationIT.testCustomBuildDirectory integration test
^KT-58547 Fixed
This commit is contained in:
Hung Nguyen
2023-08-10 11:05:55 +01:00
committed by Space Team
parent 432c9fe592
commit 894ba9ab80
30 changed files with 329 additions and 293 deletions
@@ -21,8 +21,11 @@ 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.*
import org.jetbrains.kotlin.build.report.metrics.BuildAttribute
import org.jetbrains.kotlin.build.report.metrics.BuildAttribute.*
import org.jetbrains.kotlin.build.report.metrics.GradleBuildPerformanceMetric
import org.jetbrains.kotlin.build.report.metrics.GradleBuildTime
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
@@ -35,6 +38,8 @@ 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.storage.FileLocations
import org.jetbrains.kotlin.incremental.storage.FileToAbsolutePathConverter
import org.jetbrains.kotlin.incremental.util.BufferingMessageCollector
import org.jetbrains.kotlin.incremental.util.ExceptionLocation
import org.jetbrains.kotlin.incremental.util.reportException
@@ -45,7 +50,6 @@ import org.jetbrains.kotlin.util.removeSuffixIfPresent
import org.jetbrains.kotlin.utils.toMetadataVersion
import java.io.File
import java.nio.file.Files
import java.util.*
abstract class IncrementalCompilerRunner<
Args : CommonCompilerArguments,
@@ -83,11 +87,12 @@ abstract class IncrementalCompilerRunner<
* Creates an instance of [IncrementalCompilationContext] that holds common incremental compilation context mostly required for [CacheManager]
*/
private fun createIncrementalCompilationContext(
projectDir: File?,
transaction: CompilationTransaction
fileLocations: FileLocations?,
transaction: CompilationTransaction,
) = IncrementalCompilationContext(
pathConverterForSourceFiles = fileLocations?.let { it.getRelocatablePathConverterForSourceFiles() } ?: FileToAbsolutePathConverter,
pathConverterForOutputFiles = fileLocations?.let { it.getRelocatablePathConverterForOutputFiles() } ?: FileToAbsolutePathConverter,
transaction = transaction,
rootProjectDir = projectDir,
reporter = reporter,
trackChangesInLookupCache = shouldTrackChangesInLookupCache,
storeFullFqNamesInLookupCache = shouldStoreFullFqNamesInLookupCache,
@@ -105,12 +110,10 @@ abstract class IncrementalCompilerRunner<
allSourceFiles: List<File>,
args: Args,
messageCollector: MessageCollector,
// when `changedFiles` is not null, changes are provided by external system (e.g. Gradle)
// otherwise we track source files changes ourselves.
changedFiles: ChangedFiles?,
projectDir: File? = null
changedFiles: ChangedFiles?, // When not-null, changes are provided by the build system; otherwise, the IC will need to track them
fileLocations: FileLocations? = null, // Must be not-null if the build system needs to support build cache relocatability
): ExitCode = reporter.measure(GradleBuildTime.INCREMENTAL_COMPILATION_DAEMON) {
return when (val result = tryCompileIncrementally(allSourceFiles, changedFiles, args, projectDir, messageCollector)) {
return when (val result = tryCompileIncrementally(allSourceFiles, changedFiles, args, fileLocations, messageCollector)) {
is ICResult.Completed -> {
reporter.debug { "Incremental compilation completed" }
result.exitCode
@@ -120,7 +123,7 @@ abstract class IncrementalCompilerRunner<
reporter.addAttribute(result.reason)
compileNonIncrementally(
result.reason, allSourceFiles, args, projectDir, trackChangedFiles = changedFiles == null, messageCollector
result.reason, allSourceFiles, args, fileLocations, trackChangedFiles = changedFiles == null, messageCollector
)
}
is ICResult.Failed -> {
@@ -139,7 +142,7 @@ abstract class IncrementalCompilerRunner<
reporter.addAttribute(result.reason)
compileNonIncrementally(
result.reason, allSourceFiles, args, projectDir, trackChangedFiles = changedFiles == null, messageCollector
result.reason, allSourceFiles, args, fileLocations, trackChangedFiles = changedFiles == null, messageCollector
)
}
}
@@ -173,8 +176,8 @@ abstract class IncrementalCompilerRunner<
allSourceFiles: List<File>,
changedFiles: ChangedFiles?,
args: Args,
projectDir: File?,
messageCollector: MessageCollector
fileLocations: FileLocations?,
messageCollector: MessageCollector,
): ICResult {
if (changedFiles is ChangedFiles.Unknown) {
return ICResult.RequiresRebuild(UNKNOWN_CHANGES_IN_GRADLE_INPUTS)
@@ -182,7 +185,7 @@ abstract class IncrementalCompilerRunner<
changedFiles as ChangedFiles.Known?
return createTransaction().runWithin(::incrementalCompilationExceptionTransformer) { transaction ->
val icContext = createIncrementalCompilationContext(projectDir, transaction)
val icContext = createIncrementalCompilationContext(fileLocations, transaction)
val caches = createCacheManager(icContext, args).also {
// this way we make the transaction to be responsible for closing the caches manager
transaction.cachesManager = it
@@ -253,7 +256,7 @@ abstract class IncrementalCompilerRunner<
rebuildReason: BuildAttribute,
allSourceFiles: List<File>,
args: Args,
projectDir: File?,
fileLocations: FileLocations?,
trackChangedFiles: Boolean, // Whether we need to track changes to the source files or the build system already handles it
messageCollector: MessageCollector,
): ExitCode {
@@ -266,7 +269,7 @@ abstract class IncrementalCompilerRunner<
reporter.debug { "Cleaning ${outputDirsToClean.size} output directories" }
cleanOrCreateDirectories(outputDirsToClean)
}
val icContext = createIncrementalCompilationContext(projectDir, NonRecoverableCompilationTransaction())
val icContext = createIncrementalCompilationContext(fileLocations, NonRecoverableCompilationTransaction())
return createCacheManager(icContext, args).use { caches ->
if (trackChangedFiles) {
caches.inputsCache.sourceSnapshotMap.compareAndUpdate(allSourceFiles)
@@ -38,7 +38,7 @@ class InputsCache(
fun removeOutputForSourceFiles(sources: Iterable<File>) {
for (sourceFile in sources) {
sourceToOutputMap.remove(sourceFile).forEach {
sourceToOutputMap.getAndRemove(sourceFile)?.forEach {
icContext.reporter.debug { "Deleting $it on clearing cache for $sourceFile" }
icContext.transaction.deleteFile(it.toPath())
}
@@ -46,7 +46,7 @@ class InputsCache(
}
fun getOutputForSourceFiles(sources: Iterable<File>): List<File> = sources.flatMap {
sourceToOutputMap[it]
sourceToOutputMap[it].orEmpty()
}
// generatedFiles can contain multiple entries with the same source file
@@ -8,12 +8,14 @@ package org.jetbrains.kotlin.incremental.classpathDiff
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.build.report.DoNothingICReporter
import org.jetbrains.kotlin.build.report.debug
import org.jetbrains.kotlin.build.report.metrics.*
import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter
import org.jetbrains.kotlin.build.report.metrics.GradleBuildPerformanceMetric
import org.jetbrains.kotlin.build.report.metrics.GradleBuildTime
import org.jetbrains.kotlin.build.report.metrics.measure
import org.jetbrains.kotlin.incremental.*
import org.jetbrains.kotlin.incremental.classpathDiff.BreadthFirstSearch.findReachableNodes
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinker.shrinkClasspath
import org.jetbrains.kotlin.incremental.classpathDiff.ImpactedSymbolsComputer.computeImpactedSymbols
import org.jetbrains.kotlin.incremental.storage.FileToAbsolutePathConverter
import org.jetbrains.kotlin.incremental.storage.ListExternalizer
import org.jetbrains.kotlin.incremental.storage.loadFromFile
import org.jetbrains.kotlin.name.ClassId
@@ -202,7 +204,7 @@ object ClasspathChangesComputer {
): ProgramSymbolSet {
val workingDir =
FileUtil.createTempDirectory(this::class.java.simpleName, "_WorkingDir_${UUID.randomUUID()}", /* deleteOnExit */ true)
val icContext = IncrementalCompilationContext(pathConverter = FileToAbsolutePathConverter)
val icContext = IncrementalCompilationContext()
val incrementalJvmCache = IncrementalJvmCache(workingDir, icContext, null)
// Step 1:
@@ -14,15 +14,15 @@ class SourceToOutputFilesMap(
icContext: IncrementalCompilationContext,
) : BasicStringMap<Collection<String>>(storageFile, PathStringDescriptor, StringCollectionExternalizer, icContext) {
operator fun set(sourceFile: File, outputFiles: Collection<File>) {
storage[pathConverter.toPath(sourceFile)] = outputFiles.map(pathConverter::toPath)
storage[icContext.pathConverterForSourceFiles.toPath(sourceFile)] = outputFiles.map(icContext.pathConverterForOutputFiles::toPath)
}
operator fun get(sourceFile: File): Collection<File> =
storage[pathConverter.toPath(sourceFile)].orEmpty().map(pathConverter::toFile)
operator fun get(sourceFile: File): Collection<File>? =
storage[icContext.pathConverterForSourceFiles.toPath(sourceFile)]?.map(icContext.pathConverterForOutputFiles::toFile)
override fun dumpValue(value: Collection<String>) =
value.dumpCollection()
fun remove(file: File): Collection<File> =
get(file).also { storage.remove(pathConverter.toPath(file)) }
fun getAndRemove(file: File): Collection<File>? =
get(file).also { storage.remove(icContext.pathConverterForSourceFiles.toPath(file)) }
}
@@ -28,7 +28,7 @@ import java.util.*
class BuildDiffsStorageTest {
lateinit var storageFile: File
private val random = Random(System.currentTimeMillis())
private val icContext = IncrementalCompilationContext(null)
private val icContext = IncrementalCompilationContext()
@Before
fun setUp() {
@@ -7,8 +7,7 @@ package org.jetbrains.kotlin.incremental.snapshots
import org.jetbrains.kotlin.TestWithWorkingDir
import org.jetbrains.kotlin.incremental.IncrementalCompilationContext
import org.jetbrains.kotlin.incremental.storage.FileToPathConverter
import org.jetbrains.kotlin.incremental.storage.IncrementalFileToPathConverter
import org.jetbrains.kotlin.incremental.storage.RelocatableFileToPathConverter
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Before
@@ -24,9 +23,9 @@ class FileSnapshotMapTest : TestWithWorkingDir() {
super.setUp()
val caches = File(workingDir, "caches").apply { mkdirs() }
val snapshotMapFile = File(caches, "snapshots.tab")
val pathConverter = IncrementalFileToPathConverter((workingDir.canonicalFile))
val pathConverter = RelocatableFileToPathConverter((workingDir.canonicalFile))
val icContext = IncrementalCompilationContext(
pathConverter = pathConverter
pathConverterForSourceFiles = pathConverter
)
snapshotMap = FileSnapshotMap(snapshotMapFile, icContext)
}
@@ -5,131 +5,105 @@
package org.jetbrains.kotlin.incremental.storage
import org.jetbrains.kotlin.TestWithWorkingDir
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
import org.jetbrains.kotlin.incremental.IncrementalCompilationContext
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import kotlin.properties.Delegates
import kotlin.test.assertFailsWith
class SourceToOutputFilesMapTest : TestWithWorkingDir() {
private var stofMap: SourceToOutputFilesMap by Delegates.notNull()
private var pathConverter: FileToPathConverter by Delegates.notNull()
class SourceToOutputFilesMapTest {
@get:Rule
val tmpDir = TemporaryFolder()
private lateinit var srcDir: File
private lateinit var classesDir: File
private lateinit var stofMap: SourceToOutputFilesMap
private lateinit var fooDotKt: File
private lateinit var fooDotClass: File
@Before
override fun setUp() {
super.setUp()
val caches = File(workingDir, "caches").apply { mkdirs() }
val stofMapFile = File(caches, "stof.tab")
pathConverter = IncrementalFileToPathConverter((workingDir.canonicalFile))
val icContext = IncrementalCompilationContext(
pathConverter = pathConverter
fun setUp() {
val workingDir = tmpDir.root
srcDir = workingDir.resolve("src")
classesDir = workingDir.resolve("classes")
stofMap = SourceToOutputFilesMap(
storageFile = workingDir.resolve("stof.tab"),
icContext = IncrementalCompilationContext(
pathConverterForSourceFiles = RelocatableFileToPathConverter(srcDir),
pathConverterForOutputFiles = RelocatableFileToPathConverter(classesDir),
)
)
stofMap = SourceToOutputFilesMap(stofMapFile, icContext)
fooDotKt = srcDir.resolve("Foo.kt")
fooDotClass = classesDir.resolve("Foo.class")
}
@After
override fun tearDown() {
fun tearDown() {
stofMap.flush(false)
stofMap.closeForTest()
super.tearDown()
}
@Test
fun testEmptyGetReturnsEmpty() {
assertTrue(stofMap.get(File("")).isEmpty())
fun testNoSetGetReturnsNull() {
assertNull(stofMap[fooDotKt])
}
@Test
fun testSetGetOneReturnsOne() {
stofMap.set(
File(""),
listOf(File("one").canonicalFile))
assertEquals(
listOf(File("one").canonicalFile),
stofMap.get(File("")))
fun testSetOneGetReturnsOne() {
stofMap[fooDotKt] = listOf(fooDotClass)
assertEquals(listOf(fooDotClass), stofMap[fooDotKt])
}
@Test
fun testSetDupeReturnsUnique() {
stofMap.set(
File(""),
listOf(File("one").canonicalFile, File("one").canonicalFile, File("one").canonicalFile))
fun testSetDupeGetReturnsUnique() {
stofMap[fooDotKt] = listOf(fooDotClass, fooDotClass)
assertEquals(
listOf(File("one").canonicalFile),
stofMap.get(File("")))
assertEquals(listOf(fooDotClass), stofMap[fooDotKt])
}
@Test
fun testSetOverwriteReturnsNew() {
stofMap.set(
File(""),
listOf(File("old").canonicalFile, File("old").canonicalFile, File("old").canonicalFile))
stofMap.set(
File(""),
listOf(File("one").canonicalFile, File("two").canonicalFile, File("three").canonicalFile))
fun testSetOverwriteGetReturnsNew() {
val fooKtDotClass = classesDir.resolve("FooKt.class")
stofMap[fooDotKt] = listOf(fooDotClass)
stofMap[fooDotKt] = listOf(fooKtDotClass)
assertArrayEquals(
listOf(File("one").canonicalFile, File("two").canonicalFile, File("three").canonicalFile).toSortedPaths(),
stofMap.get(File("")).toSortedPaths())
assertEquals(listOf(fooKtDotClass), stofMap[fooDotKt])
}
@Test
fun testRelativeInReturnsAbsolute() {
stofMap.set(
File(""),
listOf(File("one"), File("two"), File("three")))
assertArrayEquals(
listOf(File("one").canonicalFile, File("two").canonicalFile, File("three").canonicalFile).toSortedPaths(),
stofMap.get(File("")).toSortedPaths()
)
}
@Test
fun testSetRelativeGetAbsolute() {
stofMap.set(
File("blah"),
listOf(File("one"), File("two"), File("three")))
assertArrayEquals(
listOf(File("one").canonicalFile, File("two").canonicalFile, File("three").canonicalFile).toSortedPaths(),
stofMap.get(File("blah").canonicalFile).toSortedPaths()
)
}
@Test
fun testSetRemove() {
stofMap.set(
File("blah"),
listOf(File("one"), File("two"), File("three")))
assertArrayEquals(
listOf(File("one").canonicalFile, File("two").canonicalFile, File("three").canonicalFile).toSortedPaths(),
stofMap.remove(File("blah")).toSortedPaths()
)
assertTrue(stofMap.get(File("blah")).isEmpty())
}
@Test
fun testSetRemoveLoop() {
repeat(5) {
stofMap.set(
File("blah"),
listOf(File("one"), File("two"), File("three"))
)
assertArrayEquals(
listOf(File("one").canonicalFile, File("two").canonicalFile, File("three").canonicalFile).toSortedPaths(),
stofMap.remove(File("blah")).toSortedPaths()
)
assertTrue(stofMap.get(File("blah")).isEmpty())
fun testSetRelativeFails() {
assertFailsWith<IllegalStateException> {
stofMap[fooDotKt] = listOf(File("relativePath"))
}
}
private fun Iterable<File>.toSortedPaths(): Array<String> =
map { it.canonicalPath }.sorted().toTypedArray()
@Test
fun testGetRelativeFails() {
stofMap[fooDotKt] = listOf(fooDotClass)
assertFailsWith<IllegalStateException> {
stofMap[fooDotKt.relativeTo(srcDir)]
}
}
@Test
fun testGetAndRemove() {
stofMap[fooDotKt] = listOf(fooDotClass)
assertEquals(listOf(fooDotClass), stofMap.getAndRemove(fooDotKt))
assertNull(stofMap[fooDotKt])
}
}