Merge pull request #9291 from eed3si9n/bport2/avoid-update-full

This commit is contained in:
eugene yokota 2026-06-01 10:45:55 -04:00 committed by GitHub
commit aae1a980fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 204 additions and 21 deletions

View File

@ -2737,16 +2737,38 @@ object Classpaths {
dependencyMode.value match
case DependencyMode.Transitive =>
Def.task { dependencyClasspath.value }
case _ =>
case DependencyMode.Direct =>
Def.task {
ClasspathImpl.filterByDependencyMode(
dependencyMode.value,
val internalFiltered = ClasspathImpl.filterInternalByMode(
DependencyMode.Direct,
thisProjectRef.value,
settingsData.value,
buildDependencies.value,
internalDependencyClasspath.value,
)
val externalFiltered = ClasspathImpl.filterByDirectDeps(
allDependencies.value,
externalDependencyClasspath.value,
)
internalFiltered ++ externalFiltered
}
case DependencyMode.PlusOne =>
Def.task {
val internalFiltered = ClasspathImpl.filterInternalByMode(
DependencyMode.PlusOne,
thisProjectRef.value,
settingsData.value,
buildDependencies.value,
internalDependencyClasspath.value,
)
val externalFiltered = ClasspathImpl.filterByPlusOne(
allDependencies.value,
projectID.value,
classpathConfiguration.value,
updateFull.value,
dependencyClasspath.value,
externalDependencyClasspath.value,
)
internalFiltered ++ externalFiltered
}
})
.value,
@ -2807,23 +2829,46 @@ object Classpaths {
// Note: invoking this task from shell would block indefinitely because it will
// wait for the upstream compilation to start.
dependencyPicklePath := Def.uncached {
// This is a conditional task. Do not refactor.
if (incOptions.value.pipelining) {
val cp = concat(
internalDependencyPicklePath,
externalDependencyClasspath,
).value
ClasspathImpl.filterByDependencyMode(
dependencyMode.value,
allDependencies.value,
projectID.value,
classpathConfiguration.value,
updateFull.value,
cp,
)
} else {
filteredDependencyClasspath.value
}
Def.taskDyn {
(incOptions.value.pipelining, dependencyMode.value) match
case (false, _) =>
Def.task { filteredDependencyClasspath.value }
case (true, DependencyMode.Transitive) =>
Def.task { dependencyClasspath.value }
case (true, DependencyMode.Direct) =>
Def.task {
val internalFiltered = ClasspathImpl.filterInternalByMode(
DependencyMode.Direct,
thisProjectRef.value,
settingsData.value,
buildDependencies.value,
internalDependencyClasspath.value,
)
val externalFiltered = ClasspathImpl.filterByDirectDeps(
allDependencies.value,
externalDependencyClasspath.value,
)
internalFiltered ++ externalFiltered
}
case (true, DependencyMode.PlusOne) =>
Def.task {
val internalFiltered = ClasspathImpl.filterInternalByMode(
DependencyMode.PlusOne,
thisProjectRef.value,
settingsData.value,
buildDependencies.value,
internalDependencyClasspath.value,
)
val externalFiltered = ClasspathImpl.filterByPlusOne(
allDependencies.value,
projectID.value,
classpathConfiguration.value,
updateFull.value,
externalDependencyClasspath.value,
)
internalFiltered ++ externalFiltered
}
}.value
},
internalDependencyPicklePath := ClasspathImpl.internalDependencyPicklePathTask.value,
exportedPickles := ClasspathImpl.exportedPicklesTask.value,

View File

@ -559,4 +559,53 @@ private[sbt] object ClasspathImpl {
case DependencyMode.Direct => filterByDirectDeps(directDeps, cp)
case DependencyMode.PlusOne => filterByPlusOne(directDeps, projectId, config, fullReport, cp)
/**
* Apply dependencyMode filtering to the *internal* classpath using the build's project graph.
* `UpdateReport` only contains externally resolved modules, so it cannot answer
* "is this internal project a direct dep of `projectRef`"; the `BuildDependencies` graph can.
*
* Direct -- entries from projects directly listed in `projectRef.dependsOn(...)`.
* PlusOne -- direct + one more hop along the project graph.
* Transitive -- unfiltered.
*/
def filterInternalByMode(
mode: DependencyMode,
projectRef: ProjectRef,
data: Def.Settings,
deps: BuildDependencies,
internalCp: Classpath,
): Classpath =
mode match
case DependencyMode.Transitive => internalCp
case _ =>
val allowed = allowedInternalKeys(mode, projectRef, data, deps)
internalCp.filter: entry =>
entry.get(Keys.moduleIDStr) match
case Some(str) =>
val mid = Classpaths.moduleIdJsonKeyFormat.read(str)
allowed.contains((mid.organization, mid.name))
case None => true
private def allowedInternalKeys(
mode: DependencyMode,
projectRef: ProjectRef,
data: Def.Settings,
deps: BuildDependencies,
): Set[(String, String)] =
def directRefs(p: ProjectRef): Set[ProjectRef] =
deps
.classpath(p)
.collect:
case ClasspathDep.ResolvedClasspathDependency(dep, _) => dep
.toSet
val refs: Set[ProjectRef] = mode match
case DependencyMode.Direct =>
directRefs(projectRef)
case DependencyMode.PlusOne =>
val direct = directRefs(projectRef)
direct ++ direct.flatMap(directRefs)
case DependencyMode.Transitive => Set.empty
refs.flatMap: pr =>
(pr / projectID).get(data).map(mid => (mid.organization, mid.name))
}

View File

@ -0,0 +1,78 @@
// Regression test for sbt/sbt#9009. dependencyMode := Direct / PlusOne
// must apply to *internal* project dependencies as well, walking the
// project graph (UpdateReport only contains external/LM modules).
//
// Fixture: core <- libA <- libB <- libC (each depends on the previous).
//
// Direct on libA -- must include core
// Direct on libB -- must *not* include core (core is transitive)
// PlusOne on libB -- must include core (one hop through libA)
// PlusOne on libC -- must include libA (one hop through libB)
// must *not* include core (two hops away)
ThisBuild / scalaVersion := "3.7.4"
ThisBuild / organization := "org.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
lazy val core = (project in file("core")).settings(name := "core")
lazy val libA = (project in file("libA"))
.settings(name := "libA")
.dependsOn(core)
lazy val libB = (project in file("libB"))
.settings(name := "libB")
.dependsOn(libA)
lazy val libC = (project in file("libC"))
.settings(name := "libC")
.dependsOn(libB)
def filteredIds(cp: Seq[Attributed[xsbti.HashedVirtualFileRef]]): Seq[String] =
cp.map(_.data.id)
// Match against the lowercased jar filename (sbt lowercases project
// names when building artifact filenames, e.g. `liba_3-...jar`).
def assertIn(needle: String, haystack: Seq[String], label: String): Unit =
val n = needle.toLowerCase
assert(
haystack.exists(_.toLowerCase.contains(n)),
s"$label: expected `$needle` to appear in $haystack"
)
def assertNotIn(needle: String, haystack: Seq[String], label: String): Unit =
val n = needle.toLowerCase
assert(
!haystack.exists(_.toLowerCase.contains(n)),
s"$label: expected `$needle` NOT to appear in $haystack"
)
lazy val checkDirectLibA = taskKey[Unit]("Direct mode on libA includes core")
lazy val checkDirectLibB = taskKey[Unit]("Direct mode on libB excludes core (transitive)")
lazy val checkPlusOneLibB = taskKey[Unit]("PlusOne mode on libB includes core (one hop)")
lazy val checkPlusOneLibC =
taskKey[Unit]("PlusOne mode on libC includes libA (one hop) but not core (two hops)")
libA / checkDirectLibA := {
val cp = filteredIds((libA / Compile / filteredDependencyClasspath).value)
assertIn("core", cp, "libA/Direct")
}
libB / checkDirectLibB := {
val cp = filteredIds((libB / Compile / filteredDependencyClasspath).value)
assertIn("libA", cp, "libB/Direct")
assertNotIn("core", cp, "libB/Direct")
}
libB / checkPlusOneLibB := {
val cp = filteredIds((libB / Compile / filteredDependencyClasspath).value)
assertIn("libA", cp, "libB/PlusOne")
assertIn("core", cp, "libB/PlusOne")
}
libC / checkPlusOneLibC := {
val cp = filteredIds((libC / Compile / filteredDependencyClasspath).value)
assertIn("libB", cp, "libC/PlusOne")
assertIn("libA", cp, "libC/PlusOne")
assertNotIn("core", cp, "libC/PlusOne")
}

View File

@ -0,0 +1,11 @@
# Direct mode: only the immediate dependsOn project is on the filtered
# classpath. Transitive internal deps (core via libA) are stripped.
> set ThisBuild / dependencyMode := DependencyMode.Direct
> libA/checkDirectLibA
> libB/checkDirectLibB
# PlusOne mode: direct + one more hop. libB sees core (one hop through
# libA); libC sees libA but not core (two hops).
> set ThisBuild / dependencyMode := DependencyMode.PlusOne
> libB/checkPlusOneLibB
> libC/checkPlusOneLibC