mirror of https://github.com/sbt/sbt.git
Merge pull request #8593 from calm329/fix/8357-transitive-update-invalidation-1.x
[1.x] fix: Invalidate update cache across commands when dependencies change
This commit is contained in:
commit
08ab372fa6
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -46,16 +46,27 @@ lazy val root = (project in file("."))
|
|||
.withMetadataDirectory(dependencyCacheDirectory.value)
|
||||
|
||||
import sbt.librarymanagement.{ ModuleSettings, UpdateConfiguration, LibraryManagementCodec }
|
||||
type In = (Long, ModuleSettings, UpdateConfiguration)
|
||||
// The fourth element is transitive dependency stamps for cross-command cache invalidation
|
||||
type In = (Long, ModuleSettings, UpdateConfiguration, Vector[Long])
|
||||
|
||||
import LibraryManagementCodec._
|
||||
|
||||
// Compute transitive stamps the same way as LibraryManagement.cachedUpdate
|
||||
val transitiveStamps0 = transitiveUpdate.value.map { ur =>
|
||||
ur.configurations
|
||||
.flatMap(_.modules.map(mr => (mr.module.organization, mr.module.name, mr.module.revision)))
|
||||
.toSet
|
||||
.hashCode
|
||||
.toLong
|
||||
}.toVector
|
||||
|
||||
val f: In => Unit =
|
||||
Tracked.inputChanged(cacheStoreFactory make "inputs") { (inChanged: Boolean, in: In) =>
|
||||
val extraInputHash1 = in._1
|
||||
val moduleSettings1 = in._2
|
||||
val inline1 = moduleSettings1 match { case x: InlineConfiguration => x }
|
||||
val updateConfig1 = in._3
|
||||
val transitiveStamps1 = in._4
|
||||
|
||||
if (inChanged) {
|
||||
sys.error(s"""
|
||||
|
|
@ -82,11 +93,19 @@ $updateConfig1
|
|||
|
||||
updateConfig0
|
||||
$updateConfig0
|
||||
-----
|
||||
transitiveStamps1 == transitiveStamps0: ${transitiveStamps1 == transitiveStamps0}
|
||||
|
||||
transitiveStamps1:
|
||||
$transitiveStamps1
|
||||
|
||||
transitiveStamps0
|
||||
$transitiveStamps0
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
f((extraInputHash0, (inline0: ModuleSettings), updateConfig0))
|
||||
f((extraInputHash0, (inline0: ModuleSettings), updateConfig0, transitiveStamps0))
|
||||
},
|
||||
|
||||
// https://github.com/sbt/sbt/issues/3226
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
package a
|
||||
object A
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
package itTests
|
||||
class Test
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue