Force invalidate dependency changes

After adding the automatic lookup to external hooks for missing binary
jars, the scripted test dependency-management/invalidate-internal
started failing. This was because the previous analysis contained a jar
dependency that still existed on disk but was no longer a part of the
dependency classpath. Fundamentally the problem is that the zinc
compile analysis is not tightly coupled with the sbt build state.

To fix this, we can cache the dependency classpath file stamps in the
same way that we cache the input file stamps in external hooks and
manually diff them at the sbt level. We then force updates regardless of
the difference between the zinc state and the sbt state.
This commit is contained in:
Ethan Atkins 2019-08-15 11:25:52 -07:00
parent 32a6d0d5d7
commit 9c7acdb713
4 changed files with 41 additions and 20 deletions

View File

@ -610,19 +610,30 @@ object Defaults extends BuildCommon {
}, },
externalHooks := { externalHooks := {
import sbt.nio.FileStamp.Formats.seqPathFileStampJsonFormatter import sbt.nio.FileStamp.Formats.seqPathFileStampJsonFormatter
val current = val currentInputs =
(unmanagedSources / inputFileStamps).value ++ (managedSources / outputFileStamps).value (unmanagedSources / inputFileStamps).value ++ (managedSources / outputFileStamps).value
val previous = (externalHooks / inputFileStamps).previous val previousInputs = (externalHooks / inputFileStamps).previous
val changes = previous val inputChanges = previousInputs
.map(sbt.nio.Settings.changedFiles(_, current)) .map(sbt.nio.Settings.changedFiles(_, currentInputs))
.getOrElse(FileChanges.noPrevious(current.map(_._1))) .getOrElse(FileChanges.noPrevious(currentInputs.map(_._1)))
ExternalHooks.default.value(changes, fileTreeView.value) val currentOutputs = (dependencyClasspathFiles / outputFileStamps).value
val previousOutputs = (externalHooks / outputFileStamps).previous
val outputChanges = previousOutputs
.map(sbt.nio.Settings.changedFiles(_, currentOutputs))
.getOrElse(FileChanges.noPrevious(currentOutputs.map(_._1)))
ExternalHooks.default.value(inputChanges, outputChanges, fileTreeView.value)
}, },
externalHooks / inputFileStamps := { externalHooks / inputFileStamps := {
compile.value // ensures the inputFileStamps previous value is only set if compile succeeds. compile.value // ensures the inputFileStamps previous value is only set if compile succeeds.
(unmanagedSources / inputFileStamps).value ++ (managedSources / outputFileStamps).value (unmanagedSources / inputFileStamps).value ++ (managedSources / outputFileStamps).value
}, },
externalHooks / inputFileStamps := (externalHooks / inputFileStamps).triggeredBy(compile).value, externalHooks / inputFileStamps := (externalHooks / inputFileStamps).triggeredBy(compile).value,
externalHooks / outputFileStamps := {
compile.value // ensures the inputFileStamps previous value is only set if compile succeeds.
(dependencyClasspathFiles / outputFileStamps).value
},
externalHooks / outputFileStamps :=
(externalHooks / outputFileStamps).triggeredBy(compile).value,
incOptions := { incOptions.value.withExternalHooks(externalHooks.value) }, incOptions := { incOptions.value.withExternalHooks(externalHooks.value) },
compileIncSetup := compileIncSetupTask.value, compileIncSetup := compileIncSetupTask.value,
console := consoleTask.value, console := consoleTask.value,
@ -2009,8 +2020,10 @@ object Classpaths {
includeFilter in unmanagedJars value, includeFilter in unmanagedJars value,
excludeFilter in unmanagedJars value excludeFilter in unmanagedJars value
) )
).map(exportClasspath) :+ ).map(exportClasspath) ++ Seq(
(sbt.nio.Keys.classpathFiles := data(fullClasspath.value).map(_.toPath)) sbt.nio.Keys.classpathFiles := data(fullClasspath.value).map(_.toPath),
sbt.nio.Keys.dependencyClasspathFiles := data(dependencyClasspath.value).map(_.toPath),
)
private[this] def exportClasspath(s: Setting[Task[Classpath]]): Setting[Task[Classpath]] = private[this] def exportClasspath(s: Setting[Task[Classpath]]): Setting[Task[Classpath]] =
s.mapInitialize(init => Def.task { exportClasspath(streams.value, init.value) }) s.mapInitialize(init => Def.task { exportClasspath(streams.value, init.value) })

View File

@ -26,7 +26,8 @@ import scala.collection.JavaConverters._
private[sbt] object ExternalHooks { private[sbt] object ExternalHooks {
private val javaHome = Option(System.getProperty("java.home")).map(Paths.get(_)) private val javaHome = Option(System.getProperty("java.home")).map(Paths.get(_))
private type Func = (FileChanges, FileTreeView[(Path, FileAttributes)]) => ExternalHooks private type Func =
(FileChanges, FileChanges, FileTreeView[(Path, FileAttributes)]) => ExternalHooks
def default: Def.Initialize[sbt.Task[Func]] = Def.task { def default: Def.Initialize[sbt.Task[Func]] = Def.task {
val unmanagedCache = unmanagedFileStampCache.value val unmanagedCache = unmanagedFileStampCache.value
val managedCache = managedFileStampCache.value val managedCache = managedFileStampCache.value
@ -37,15 +38,16 @@ private[sbt] object ExternalHooks {
} }
val classGlob = classDirectory.value.toGlob / RecursiveGlob / "*.class" val classGlob = classDirectory.value.toGlob / RecursiveGlob / "*.class"
val options = (compileOptions in compile).value val options = (compileOptions in compile).value
(fc: FileChanges, fileTreeView: FileTreeView[(Path, FileAttributes)]) => { ((inputFileChanges, outputFileChanges, fileTreeView) => {
fileTreeView.list(classGlob).foreach { fileTreeView.list(classGlob).foreach {
case (path, _) => managedCache.update(path, FileStamper.LastModified) case (path, _) => managedCache.update(path, FileStamper.LastModified)
} }
apply(fc, options, unmanagedCache, managedCache) apply(inputFileChanges, outputFileChanges, options, unmanagedCache, managedCache)
} }): Func
} }
private def apply( private def apply(
changedFiles: FileChanges, inputFileChanges: FileChanges,
outputFileChanges: FileChanges,
options: CompileOptions, options: CompileOptions,
unmanagedCache: FileStamp.Cache, unmanagedCache: FileStamp.Cache,
managedCache: FileStamp.Cache managedCache: FileStamp.Cache
@ -62,7 +64,7 @@ private[sbt] object ExternalHooks {
} }
private def add(f: File, set: java.util.Set[File]): Unit = { set.add(f); () } private def add(f: File, set: java.util.Set[File]): Unit = { set.add(f); () }
val allChanges = new java.util.HashSet[File] val allChanges = new java.util.HashSet[File]
changedFiles match { inputFileChanges match {
case FileChanges(c, d, m, _) => case FileChanges(c, d, m, _) =>
c.foreach(add(_, getAdded, allChanges)) c.foreach(add(_, getAdded, allChanges))
d.foreach(add(_, getRemoved, allChanges)) d.foreach(add(_, getRemoved, allChanges))
@ -99,7 +101,11 @@ private[sbt] object ExternalHooks {
Optional.empty[Array[FileHash]] Optional.empty[Array[FileHash]]
override def changedBinaries(previousAnalysis: CompileAnalysis): Option[Set[File]] = { override def changedBinaries(previousAnalysis: CompileAnalysis): Option[Set[File]] = {
Some(previousAnalysis.readStamps.getAllBinaryStamps.asScala.flatMap { val base =
(outputFileChanges.modified ++ outputFileChanges.created ++ outputFileChanges.deleted)
.map(_.toFile)
.toSet
Some(base ++ previousAnalysis.readStamps.getAllBinaryStamps.asScala.flatMap {
case (file, stamp) => case (file, stamp) =>
managedCache.getOrElseUpdate(file.toPath, FileStamper.LastModified) match { managedCache.getOrElseUpdate(file.toPath, FileStamper.LastModified) match {
case Some(cachedStamp) if equiv(cachedStamp.stamp, stamp) => None case Some(cachedStamp) if equiv(cachedStamp.stamp, stamp) => None
@ -110,7 +116,7 @@ private[sbt] object ExternalHooks {
case _ => Some(file) case _ => Some(file)
} }
} }
}.toSet) })
} }
override def removedProducts(previousAnalysis: CompileAnalysis): Option[Set[File]] = { override def removedProducts(previousAnalysis: CompileAnalysis): Option[Set[File]] = {

View File

@ -160,6 +160,8 @@ object Keys {
private[sbt] val managedFileStampCache = taskKey[FileStamp.Cache]( private[sbt] val managedFileStampCache = taskKey[FileStamp.Cache](
"Map of managed file stamps that may be cleared between task evaluation runs." "Map of managed file stamps that may be cleared between task evaluation runs."
).withRank(Invisible) ).withRank(Invisible)
private[sbt] val dependencyClasspathFiles =
taskKey[Seq[Path]]("The dependency classpath for a task.").withRank(Invisible)
private[sbt] val classpathFiles = private[sbt] val classpathFiles =
taskKey[Seq[Path]]("The classpath for a task.").withRank(Invisible) taskKey[Seq[Path]]("The classpath for a task.").withRank(Invisible)

View File

@ -242,12 +242,12 @@ private[sbt] object Settings {
} }
prevMap.forEach((p, _) => deletedBuilder += p) prevMap.forEach((p, _) => deletedBuilder += p)
val unmodified = unmodifiedBuilder.result() val unmodified = unmodifiedBuilder.result()
if (unmodified.size == current.size) { val deleted = deletedBuilder.result()
val created = createdBuilder.result()
val modified = modifiedBuilder.result()
if (created.isEmpty && deleted.isEmpty && modified.isEmpty) {
FileChanges.unmodified(unmodifiedBuilder.result) FileChanges.unmodified(unmodifiedBuilder.result)
} else { } else {
val created = createdBuilder.result()
val deleted = deletedBuilder.result()
val modified = modifiedBuilder.result()
FileChanges(created, deleted, modified, unmodified) FileChanges(created, deleted, modified, unmodified)
} }
} }