[2.x] fix: Fixes ThisBuild-scoped keys using root project's aggregates (#8703)

Build-level references (ThisBuild, BuildRef) should not participate
in aggregation. Only project-level references should aggregate.

Previously, when querying `ThisBuild/version`, the aggregation logic
would resolve ThisBuild to a BuildRef, then convert it to the root
project's ProjectRef, causing it to incorrectly use the root project's
aggregate definitions.

The fix uses pattern matching to distinguish BuildReference from other
reference types, returning None (no aggregation) for build-level scopes.

Fixes sbt/sbt#5349
This commit is contained in:
bitloi 2026-02-07 19:25:55 -05:00 committed by GitHub
parent 499ec520a7
commit f6319f19a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 87 additions and 2 deletions

View File

@ -239,13 +239,22 @@ object Aggregation {
private def maps[T, S](vs: Values[T])(f: T => S): Values[S] =
vs map { case KeyValue(k, v) => KeyValue(k, f(v)) }
/**
* Returns the aggregated projects for the given reference.
* Build-level references (ThisBuild, BuildRef) do not aggregate - only project-level
* references participate in aggregation. This fixes issue #5349 where ThisBuild-scoped
* keys incorrectly used the root project's aggregates.
*/
def projectAggregates[Proj](
proj: Option[Reference],
extra: BuildUtil[Proj],
reverse: Boolean
): Seq[ProjectRef] =
val resRef = proj.map(p => extra.projectRefFor(extra.resolveRef(p)))
resRef.toList.flatMap { ref =>
val projectRef = proj.flatMap {
case _: BuildReference => None
case ref => Some(extra.projectRefFor(extra.resolveRef(ref)))
}
projectRef.toList.flatMap { ref =>
if reverse then extra.aggregates.reverse(ref)
else extra.aggregates.forward(ref)
}

View File

@ -8,9 +8,35 @@
package sbt.internal
import java.net.URI
import sbt.{ BuildRef, Def, Scope }
import sbt.Def.ScopedKey
import sbt.ScopeAxis.{ Select, Zero }
import sbt.internal.TestBuild.{ Build, Env, Proj, Taskk }
import sbt.internal.util.AttributeKey
import sbt.librarymanagement.Configuration
object AggregationSpec extends verify.BasicTestSuite {
val timing = Aggregation.timing(0, _: Long)
test(
"projectAggregates should return empty for BuildRef (ThisBuild-scoped keys do not aggregate, #5349)"
) {
val buildURI = new URI("file", "///path/", null)
val config = Configuration.of("Compile", "compile")
val project = Proj("root", Nil, Seq(config))
val build = Build(buildURI, Vector(project))
val key = AttributeKey[String]("test")
val task = Taskk(key, Nil)
val env = Env(Vector(build), Vector(task))
val scope = Scope(Select(BuildRef(buildURI)), Zero, Select(key), Zero)
val settings = Seq(Def.setting(ScopedKey(scope, key), Def.value("v")))
val structure = TestBuild.structure(env, settings, build.allProjects.head._1)
val result =
Aggregation.projectAggregates(Some(BuildRef(buildURI)), structure.extra, reverse = false)
assert(result.isEmpty, s"BuildRef must not aggregate; got: $result")
}
test("timing should format total time properly") {
assert(timing(101).startsWith("elapsed time: 0 s"))
assert(timing(1000).startsWith("elapsed time: 1 s"))

View File

@ -0,0 +1,31 @@
ThisBuild / scalaVersion := "2.13.16"
lazy val mark = taskKey[Unit]("Creates a marker file to track where this task ran")
lazy val root = (project in file("."))
.aggregate(sub)
.settings(
name := "root",
mark := {
val toMark = baseDirectory.value / "root-ran"
if (toMark.exists) sys.error(s"Already ran ($toMark exists)")
else IO.touch(toMark)
}
)
lazy val sub = (project in file("sub"))
.settings(
name := "sub",
mark := {
val toMark = baseDirectory.value / "sub-ran"
if (toMark.exists) sys.error(s"Already ran ($toMark exists)")
else IO.touch(toMark)
}
)
ThisBuild / mark := {
val base = (ThisBuild / baseDirectory).value
val toMark = base / "build-ran"
if (toMark.exists) sys.error(s"Already ran ($toMark exists)")
else IO.touch(toMark)
}

View File

@ -0,0 +1,19 @@
# Verify ThisBuild-scoped keys do not participate in aggregation (sbt/sbt#5349, PR #8703).
# Each scope level has its own mark task creating distinct files:
# - ThisBuild/mark creates build-ran
# - root/mark creates root-ran
# - sub/mark creates sub-ran
# Before the fix, "show ThisBuild/mark" would aggregate and run root+sub marks.
# After the fix, it runs only the build-level task.
$ mkdir sub
# ThisBuild/mark should only create build-ran, not trigger root or sub
> show ThisBuild/mark
$ exists build-ran
$ absent root-ran
$ absent sub/sub-ran
# Verify project-level marks still work and do aggregate
> show root/mark
$ exists root-ran
$ exists sub/sub-ran