From f6319f19a393d4448a69f7ef8da88f80b0d0e358 Mon Sep 17 00:00:00 2001 From: bitloi <89318445+bitloi@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:25:55 -0500 Subject: [PATCH] [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 --- .../main/scala/sbt/internal/Aggregation.scala | 13 ++++++-- .../scala/sbt/internal/AggregationSpec.scala | 26 ++++++++++++++++ .../actions/thisbuild-no-aggregate/build.sbt | 31 +++++++++++++++++++ .../actions/thisbuild-no-aggregate/test | 19 ++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 sbt-app/src/sbt-test/actions/thisbuild-no-aggregate/build.sbt create mode 100644 sbt-app/src/sbt-test/actions/thisbuild-no-aggregate/test diff --git a/main/src/main/scala/sbt/internal/Aggregation.scala b/main/src/main/scala/sbt/internal/Aggregation.scala index 874e1d2b7..b5c3f811b 100644 --- a/main/src/main/scala/sbt/internal/Aggregation.scala +++ b/main/src/main/scala/sbt/internal/Aggregation.scala @@ -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) } diff --git a/main/src/test/scala/sbt/internal/AggregationSpec.scala b/main/src/test/scala/sbt/internal/AggregationSpec.scala index d2f440b9e..bb4fbed5d 100644 --- a/main/src/test/scala/sbt/internal/AggregationSpec.scala +++ b/main/src/test/scala/sbt/internal/AggregationSpec.scala @@ -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")) diff --git a/sbt-app/src/sbt-test/actions/thisbuild-no-aggregate/build.sbt b/sbt-app/src/sbt-test/actions/thisbuild-no-aggregate/build.sbt new file mode 100644 index 000000000..17abf53aa --- /dev/null +++ b/sbt-app/src/sbt-test/actions/thisbuild-no-aggregate/build.sbt @@ -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) +} diff --git a/sbt-app/src/sbt-test/actions/thisbuild-no-aggregate/test b/sbt-app/src/sbt-test/actions/thisbuild-no-aggregate/test new file mode 100644 index 000000000..7a32a7989 --- /dev/null +++ b/sbt-app/src/sbt-test/actions/thisbuild-no-aggregate/test @@ -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