fix: Invalidate update cache across commands when dependencies change

This commit is contained in:
calm329 2026-01-20 14:18:05 -08:00
parent fcb4e0e43c
commit 16cfc66152
7 changed files with 76 additions and 6 deletions

View File

@ -3827,7 +3827,7 @@ object Classpaths {
substituteScalaFiles(scalaOrganization.value, _)(providedScalaJars),
skip = (update / skip).value,
force = shouldForce,
depsUpdated = transitiveUpdate.value.exists(!_.stats.cached),
transitiveUpdates = transitiveUpdate.value,
uwConfig = (update / unresolvedWarningConfiguration).value,
evictionLevel = evictionErrorLevel.value,
versionSchemeOverrides = libraryDependencySchemes.value,

View File

@ -26,7 +26,8 @@ import scala.compat.Platform.EOL
private[sbt] object LibraryManagement {
implicit val linter: sbt.dsl.LinterLevel.Ignore.type = sbt.dsl.LinterLevel.Ignore
private type UpdateInputs = (Long, ModuleSettings, UpdateConfiguration)
// The fourth element is transitive dependency stamps for cross-command cache invalidation
private type UpdateInputs = (Long, ModuleSettings, UpdateConfiguration, Vector[Long])
def cachedUpdate(
lm: DependencyResolution,
@ -37,7 +38,7 @@ private[sbt] object LibraryManagement {
transform: UpdateReport => UpdateReport,
skip: Boolean,
force: Boolean,
depsUpdated: Boolean,
transitiveUpdates: Seq[UpdateReport],
uwConfig: UnresolvedWarningConfiguration,
evictionLevel: Level.Value,
versionSchemeOverrides: Seq[ModuleID],
@ -102,8 +103,9 @@ private[sbt] object LibraryManagement {
/* Check if a update report is still up to date or we must resolve again. */
def upToDate(inChanged: Boolean, out: UpdateReport): Boolean = {
// Transitive dependency stamps are now part of UpdateInputs, so inChanged
// will be true if any transitive stamp changed (cross-command invalidation).
!force &&
!depsUpdated &&
!inChanged &&
out.allFiles.forall(f => fileUptodate(f, out.stamps, log)) &&
fileUptodate(out.cachedDescriptor, out.stamps, log)
@ -166,7 +168,18 @@ private[sbt] object LibraryManagement {
val handler = if (skip && !force) skipResolve(outStore)(_) else doResolve(outStore)
// Remove clock for caching purpose
val withoutClock = updateConfig.withLogicalClock(LogicalClock.unknown)
handler((extraInputHash, settings, withoutClock))
// Collect transitive stamps for cross-command cache invalidation.
// Hash the resolved module IDs (org, name, revision) from each transitive update.
// This changes when any transitive dependency's resolved versions change,
// enabling correct cache invalidation across commands.
val transitiveStamps = transitiveUpdates.map { ur =>
ur.configurations
.flatMap(_.modules.map(mr => (mr.module.organization, mr.module.name, mr.module.revision)))
.toSet
.hashCode
.toLong
}.toVector
handler((extraInputHash, settings, withoutClock, transitiveStamps))
}
private[this] def fileUptodate(file: File, stamps: Map[File, Long], log: Logger): Boolean = {
@ -299,7 +312,7 @@ private[sbt] object LibraryManagement {
identity,
skip = (update / skip).value,
force = shouldForce,
depsUpdated = transitiveUpdate.value.exists(!_.stats.cached),
transitiveUpdates = transitiveUpdate.value,
uwConfig = (update / unresolvedWarningConfiguration).value,
evictionLevel = Level.Debug,
versionSchemeOverrides = Nil,

View File

@ -0,0 +1,17 @@
// Test for https://github.com/sbt/sbt/issues/8357
// Verifies that transitiveUpdate correctly invalidates across command invocations
// when a dependency's dependencies change.
ThisBuild / scalaVersion := "2.12.21"
// Use a setting to control library version - this can be changed via reload
lazy val catsVersion = settingKey[String]("Cats version")
lazy val a = project.in(file("a"))
.settings(
catsVersion := "2.8.0",
libraryDependencies += "org.typelevel" %% "cats-core" % catsVersion.value,
)
lazy val itTests = project.in(file("itTests"))
.dependsOn(a % "test->test")

View File

@ -0,0 +1,16 @@
// Test for https://github.com/sbt/sbt/issues/8357
// This is the changed build.sbt with different library version
ThisBuild / scalaVersion := "2.12.21"
lazy val catsVersion = settingKey[String]("Cats version")
lazy val a = project.in(file("a"))
.settings(
// Changed from 2.8.0 to 2.9.0
catsVersion := "2.9.0",
libraryDependencies += "org.typelevel" %% "cats-core" % catsVersion.value,
)
lazy val itTests = project.in(file("itTests"))
.dependsOn(a % "test->test")

View File

@ -0,0 +1,20 @@
# Test for https://github.com/sbt/sbt/issues/8357
# Verifies that transitiveUpdate correctly invalidates across command invocations
# Step 1: Run itTests/update to establish baseline with cats 2.8.0
> itTests/update
# Step 2: Change a's dependency version (cats 2.8.0 -> 2.9.0)
$ copy-file changes/build.sbt build.sbt
# Step 3: Reload to pick up the new settings
> reload
# Step 4: Run a/update in a SEPARATE command
> a/update
# Step 5: Now run itTests/update - should re-resolve with new dependencies
> itTests/update
# Compile to verify all dependencies are available
> itTests/compile