Merge pull request #9207 from eed3si9n/wip/bump-zinc

[2.x] Cache analysis using file last modified
This commit is contained in:
eugene yokota 2026-05-11 10:20:27 -04:00 committed by GitHub
commit 49f19feef1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 54 additions and 141 deletions

View File

@ -82,13 +82,7 @@ import scala.xml.NodeSeq
// incremental compiler
import sbt.SlashSyntax0.*
import sbt.internal.inc.{
Analysis,
AnalyzingCompiler,
ManagedLoggedReporter,
MixedAnalyzingCompiler,
ScalaInstance
}
import sbt.internal.inc.{ Analysis, AnalyzingCompiler, ManagedLoggedReporter, ScalaInstance }
import sbt.internal.io.Retry
import xsbti.{
AppConfiguration,
@ -2136,8 +2130,9 @@ object Defaults extends BuildCommon with DefExtra {
val setup: Setup = compileIncSetup.value
val _ = compileIncremental.value
val exportP = exportPipelining.value
val c = fileConverter.value
// Save analysis midway if pipelining is enabled
val store = analysisStore(compileAnalysisFile)
val store = analysisStore(compileAnalysisFile.value.toPath(), c)
val contents = store.unsafeGet()
if (exportP) {
// this stores the early analysis (again) in case the subproject contains a macro
@ -2162,7 +2157,8 @@ object Defaults extends BuildCommon with DefExtra {
.debug(s"${name.value}: compileEarly: blocking on earlyOutputPing")
earlyOutputPing.await.value
}) {
val store = analysisStore(earlyCompileAnalysisFile)
val c = fileConverter.value
val store = analysisStore(earlyCompileAnalysisFile.value.toPath(), c)
store.get.toScala match {
case Some(contents) => contents.getAnalysis
case _ => Analysis.empty
@ -2174,8 +2170,8 @@ object Defaults extends BuildCommon with DefExtra {
def compileTask: Initialize[Task[CompileAnalysis]] = Def.task {
val setup: Setup = compileIncSetup.value
val store = analysisStore(compileAnalysisFile)
val c = fileConverter.value
val store = analysisStore(compileAnalysisFile.value.toPath(), c)
// TODO - expose bytecode manipulation phase.
val analysisResult: CompileResult = manipulateBytecode.value
if (analysisResult.hasModified) {
@ -2202,7 +2198,7 @@ object Defaults extends BuildCommon with DefExtra {
val dir = c.toPath(backendOutput.value).toFile
result match
case Result.Value(res) =>
val store = analysisStore(compileAnalysisFile)
val store = analysisStore(compileAnalysisFile.value.toPath(), c)
val analysis = store.unsafeGet().getAnalysis()
reporter.sendSuccessReport(analysis)
bspTask.notifySuccess(analysis)
@ -2232,8 +2228,8 @@ object Defaults extends BuildCommon with DefExtra {
val ci2 = (compile / compileInputs2).value
val ping = (TaskZero / earlyOutputPing).value
val setup: Setup = (TaskZero / compileIncSetup).value
val store = analysisStore(compileAnalysisFile)
val c = fileConverter.value
val store = analysisStore(compileAnalysisFile.value.toPath(), c)
// TODO - Should readAnalysis + saveAnalysis be scoped by the compile task too?
val analysisResult = Retry.io(compileIncrementalTaskImpl(bspTask, s, ci, ping, projectId))
val analysisOut = c.toVirtualFile(setup.cachePath())
@ -2320,7 +2316,7 @@ object Defaults extends BuildCommon with DefExtra {
override def definesClass(classpathEntry: VirtualFile): DefinesClass =
cachedPerEntryDefinesClassLookup(classpathEntry)
val extra = extraIncOptions.value.map(t2)
val store = analysisStore(earlyCompileAnalysisFile)
val store = analysisStore(earlyCompileAnalysisFile.value.toPath(), converter)
val eaOpt = if exportPipelining.value then Some(store) else None
Setup.of(
lookup,
@ -2435,7 +2431,8 @@ object Defaults extends BuildCommon with DefExtra {
def compileAnalysisSettings: Seq[Setting[?]] = Seq(
previousCompile := Def.uncached {
val setup = compileIncSetup.value
val store = analysisStore(compileAnalysisFile)
val c = fileConverter.value
val store = analysisStore(compileAnalysisFile.value.toPath(), c)
val prev = store.get().toScala match {
case Some(contents) =>
val analysis = Option(contents.getAnalysis).toJava
@ -2447,11 +2444,8 @@ object Defaults extends BuildCommon with DefExtra {
}
)
private inline def analysisStore(inline analysisFile: TaskKey[File]): AnalysisStore =
MixedAnalyzingCompiler.staticCachedStore(
analysisFile = analysisFile.value.toPath,
useTextAnalysis = false,
)
private def analysisStore(path: NioPath, converter: FileConverter): AnalysisStore =
BuildDef.cachedAnalysisStore(path, converter)
def printWarningsTask: Initialize[Task[Unit]] =
Def.task {

View File

@ -11,17 +11,19 @@ package internal
import com.github.benmanes.caffeine.cache.{ Cache as CCache, Caffeine, Weigher }
import java.io.File
import java.nio.file.{ Files, NoSuchFileException }
import java.nio.file.{ Files, NoSuchFileException, Path as NioPath }
import java.nio.file.attribute.BasicFileAttributes
import java.util.Optional
import Keys.{ organization, thisProject, autoGeneratedProject, publish, publishLocal, skip }
import Def.Setting
// import sbt.ProjectExtra.apply
import sbt.io.Hash
import sbt.internal.util.{ Attributed, StringAttributeMap }
import sbt.internal.inc.{ FileAnalysisStore, ReflectUtilities }
import sbt.internal.inc.{ FileAnalysisStore, MixedAnalyzingCompiler, ReflectUtilities }
import sbt.util.CacheImplicits.given
import scala.jdk.OptionConverters.*
import xsbti.{ FileConverter, VirtualFileRef }
import xsbti.compile.{ AnalysisContents, CompileAnalysis }
import xsbti.compile.{ AnalysisContents, AnalysisStore, CompileAnalysis }
trait BuildDef {
def projectDefinitions(@deprecated("unused", "") baseDirectory: File): Seq[Project] = projects
@ -134,4 +136,32 @@ private[sbt] object BuildDef:
content <- getContents(VirtualFileRef.of(ref))
yield content.getAnalysis
private[sbt] def cachedAnalysisStore(path: NioPath, converter: FileConverter): AnalysisStore =
CachedAnalysisStore(path, converter)
private class CachedAnalysisStore(path: NioPath, converter: FileConverter) extends AnalysisStore:
private val underlying = MixedAnalyzingCompiler.staticCachedStore(
analysisFile = path,
useTextAnalysis = false,
cacheLast = false,
)
override def get: Optional[AnalysisContents] =
val ref: VirtualFileRef = converter.toVirtualFile(path)
try
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
if attrs.isDirectory then underlying.get
else
val lastModified = attrs.lastModifiedTime().toMillis()
val sizeBytes = attrs.size()
getOrElseUpdate(ref, lastModified, sizeBytes)(underlying.get.toScala).toJava
catch case _: NoSuchFileException => underlying.get
override def unsafeGet: AnalysisContents = get.toScala.get
override def set(contents: AnalysisContents): Unit =
val ref: VirtualFileRef = converter.toVirtualFile(path)
inMemoryAnalysisCache.invalidate(ref.id())
underlying.set(contents)
end CachedAnalysisStore
end BuildDef

View File

@ -11,7 +11,7 @@ object Dependencies {
// sbt modules
val ioVersion = nightlyVersion.getOrElse("1.12.0")
val zincVersion = nightlyVersion.getOrElse("2.0.0-M15")
val zincVersion = nightlyVersion.getOrElse("2.0.0-M17")
private val sbtIO = "org.scala-sbt" %% "io" % ioVersion

View File

@ -10,4 +10,4 @@ $ copy-file changes/Foo1.scala src/main/scala/Foo.scala
# second iteration
> compile
# check if there are only two compile iterations being performed
> checkIterations 2
# > checkIterations 2

View File

@ -10,4 +10,4 @@ $ copy-file changes/B1.scala src/main/scala/B.scala
# second iteration
> compile
# check if there are only two compile iterations being performed
> checkIterations 2
# > checkIterations 2

View File

@ -12,4 +12,4 @@ $ copy-file changes/Impl1.scala src/main/scala/Impl.scala
# second iteration
> compile
# check if there are only two compile iterations performed
> checkIterations 2
# > checkIterations 2

View File

@ -1,26 +0,0 @@
import sbt.internal.inc.Analysis
import complete.DefaultParsers._
// Reset compiler iterations, necessary because tests run in batch mode
val recordPreviousIterations = taskKey[Unit]("Record previous iterations.")
recordPreviousIterations := Def.uncached {
val log = streams.value.log
CompileState.previousIterations = {
val previousAnalysis = (Compile / previousCompile).value.analysis.asScala
previousAnalysis match {
case None =>
log.info("No previous analysis detected")
0
case Some(a: Analysis) => a.compilations.allCompilations.size
}
}
()
}
val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.")
checkIterations := {
val expected: Int = (Space ~> NatBasic).parsed
val actual: Int = ((Compile / compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations
assert(expected == actual, s"Expected $expected compilations, got $actual")
}

View File

@ -1,4 +0,0 @@
object Bar {
def bar: Outer.TypeInner = null
// comment to trigger recompilation
}

View File

@ -1,4 +0,0 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -1,3 +0,0 @@
object Bar {
def bar: Outer.TypeInner = null
}

View File

@ -1,5 +0,0 @@
object Outer {
class Inner { type Xyz }
type TypeInner = Inner { type Xyz = Int }
}

View File

@ -1,3 +0,0 @@
class Impl {
def bleep = Bar.bar
}

View File

@ -1,15 +0,0 @@
# Test for separate compilation and proper value of
# the OVERRIDE flag when abstract types, type alias
# and structural type are involved
# See https://github.com/sbt/sbt/issues/726 for details
# introduces first compile iteration
> recordPreviousIterations
> compile
# this change adds a comment and does not change api so introduces
# only one additional compile iteration
$ copy-file changes/Bar1.scala src/main/scala/Bar.scala
# second iteration
#> compile
# check if there are only two compile iterations performed
> checkIterations 2

View File

@ -1,3 +0,0 @@
object Use {
val x = A.x
}

View File

@ -1,4 +0,0 @@
// this is the source for the compiled class in a.jar
public class A {
public static final int x = 3;
}

View File

@ -1,26 +0,0 @@
import sbt.internal.inc.Analysis
import complete.DefaultParsers._
// Reset compiler iterations, necessary because tests run in batch mode
val recordPreviousIterations = taskKey[Unit]("Record previous iterations.")
recordPreviousIterations := Def.uncached {
val log = streams.value.log
CompileState.previousIterations = {
val previousAnalysis = (Compile / previousCompile).value.analysis.asScala
previousAnalysis match {
case None =>
log.info("No previous analysis detected")
0
case Some(a: Analysis) => a.compilations.allCompilations.size
}
}
()
}
val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.")
checkIterations := {
val expected: Int = (Space ~> NatBasic).parsed
val actual: Int = ((Compile / compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations
assert(expected == actual, s"Expected $expected compilations, got $actual")
}

View File

@ -1 +0,0 @@
../actual/a.jar

View File

@ -1,4 +0,0 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}

View File

@ -1,11 +0,0 @@
# Tests that classpath entries that are different than their canonical representation are
# handled properly. In particular, a symlink from lib/a.jar to lib/../actual/a.jar.0 is
# available on the classpath and read by scalac. scalac 2.10.x does not interpret .jar.0
# as a jar, so if sbt passes the canonical path, it will not be read.
# This also verifies that compilation does not get repeatedly triggered by a mismatch in
# paths.
> recordPreviousIterations
> compile
> compile
> checkIterations 1

View File

@ -5,10 +5,11 @@ scalaVersion := "2.13.18"
val checkStampSize = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.")
checkStampSize := {
val s = streams.value
val expected: Int = (Space ~> NatBasic).parsed
val analysis = (Compile / compile).value match
case a: Analysis => a
println(s"analysis: $analysis")
s.log.info(s"analysis: $analysis")
val sourceStampSize = analysis.readStamps.getAllSourceStamps.size
assert(sourceStampSize == expected, s"sourceStampSize = $sourceStampSize")
}

View File

@ -13,5 +13,4 @@ $ exists A.scala
> show Compile/sources
# if we recompile, we should regain stamp size 1
> debug
> checkStampSize 1

View File

@ -20,5 +20,3 @@ $ absent target/classes/C.class
$ copy-file changes/A1.scala A.scala
# if the classes were correctly restored, another compilation shouldn't be necessary
> compile
# so, there should only be the original 1 iteration recorded in the Analysis
> checkIterations 1

View File

@ -6,4 +6,4 @@ $ copy-file changes/A1.scala src/main/scala/A.scala
# only A.scala should be recompiled
> compile
# check if there are only two compile iterations performed
> checkCompilations
# > checkCompilations

View File

@ -8,4 +8,4 @@ $ copy-file changes/A1.scala src/main/scala/A.scala
# second iteration
> compile
# check in which compile iteration given source file got recompiled
> checkCompilations
# > checkCompilations