[2.x] fix: Fixes ThisBuild-scoped bare settings (#9674)

**Problem**
ThisBuild scoped baresettings are treated as a common setting,
which results in duplicate appends etc.

**Solution**
Only treat This-project and ThisProject scoped setting as a common setting.
This commit is contained in:
eugene yokota
2026-08-24 21:32:34 -04:00
committed by GitHub
parent 9eb615556f
commit 108623f848
3 changed files with 66 additions and 4 deletions
+18 -4
View File
@@ -15,7 +15,7 @@ import sbt.Keys.*
import sbt.Project.inScope
import sbt.ProjectExtra.{ prefixConfigs, setProject, showLoadingKey, structure }
import sbt.Scope.GlobalScope
import sbt.ScopeAxis.{ Select, Zero }
import sbt.ScopeAxis.{ Select, This, Zero }
import sbt.SlashSyntax0.*
import sbt.internal.BuildStreams.*
import sbt.internal.inc.classpath.ClasspathUtil
@@ -1069,6 +1069,11 @@ private[sbt] object Load {
log = log,
)
// Excludes settings already scoped explicitly (ThisBuild, Global); those apply once (#9668).
def thisProjectScoped(settings: Seq[Setting[?]]): Seq[Setting[?]] =
settings.filter: s =>
s.key.scope.project == This || s.key.scope.project == Select(ThisProject)
// load all relevant configuration files (.sbt, as .scala already exists at this point)
def discover(base: File): DiscoveredProjects = {
val auto =
@@ -1132,7 +1137,9 @@ private[sbt] object Load {
val newAcc = acc :+ finalRoot
val newGenerated = generated ++ generatedConfigClassFiles
// only root-level settings are build-wide; a submodule's own settings must not leak to siblings (#9517).
val cs = if isRootPath(p.base, buildBase) then finalRoot.commonSettings else commonSettings
val cs =
if isRootPath(p.base, buildBase) then thisProjectScoped(finalRoot.commonSettings)
else commonSettings
loadTransitive1(newProjects, newAcc, newGenerated, cs)
}
@@ -1168,7 +1175,9 @@ private[sbt] object Load {
acc = acc,
generated = Nil,
commonSettings0 = commonSettings
++ expandCommonSettingsPerBase1(buildBase.getCanonicalFile()),
++ thisProjectScoped(
expandCommonSettingsPerBase1(buildBase.getCanonicalFile())
),
)
val existingIds = otherProjects.projects.map(_.id)
val refs = existingIds.map(id => ProjectRef(buildUri, id))
@@ -1181,7 +1190,12 @@ private[sbt] object Load {
val newAcc = finalRoot +: (acc ++ otherProjects.projects)
val newGenerated =
generated ++ otherProjects.generatedConfigClassFiles ++ generatedConfigClassFiles
loadTransitive1(newProjects, newAcc, newGenerated, finalRoot.commonSettings)
loadTransitive1(
newProjects,
newAcc,
newGenerated,
thisProjectScoped(finalRoot.commonSettings)
)
case Nil =>
val projectIds = acc.map(_.id).mkString("(", ", ", ")")
log.debug(s"[Loading] Done in $buildBase, returning: $projectIds")
@@ -5,6 +5,16 @@ scalaVersion := scala212
val o = "com.example"
organization := o
lazy val aa = settingKey[Seq[String]]("")
Global / aa := Seq("initial-value")
// explicitly ThisBuild-scoped settings must apply exactly once, not once per project (#9668)
ThisBuild / aa += "added-value"
lazy val bb = settingKey[Seq[String]]("")
Global / bb := Seq("initial-value")
// ThisProject means "current project", so it must still apply once per project, unlike ThisBuild
ThisProject / bb += "added-value"
lazy val foo = project
lazy val bar = project
.settings(
@@ -24,5 +34,17 @@ check := {
assert((bar / organization).value == "com.example.bar", s"unexpected bar / organization = {(bar / organization).value}")
// Test that baz/build.sbt bare settings get loaded
assert((baz / organization).value == "com.example.baz", s"unexpected baz/organization")
// Test that ThisBuild / aa += is applied exactly once, not once per project (#9668)
val expectedAa = Seq("initial-value", "added-value")
assert((foo / aa).value == expectedAa, s"(foo / aa).value: ${(foo / aa).value}")
assert((bar / aa).value == expectedAa, s"(bar / aa).value: ${(bar / aa).value}")
assert((baz / aa).value == expectedAa, s"(baz / aa).value: ${(baz / aa).value}")
// Test that ThisProject / bb += still applies once per project (#9668)
val expectedBb = Seq("initial-value", "added-value")
assert((foo / bb).value == expectedBb, s"(foo / bb).value: ${(foo / bb).value}")
assert((bar / bb).value == expectedBb, s"(bar / bb).value: ${(bar / bb).value}")
assert((baz / bb).value == expectedBb, s"(baz / bb).value: ${(baz / bb).value}")
}
check / aggregate := false
@@ -6,6 +6,16 @@ scalaVersion := scala212
val o = "com.example"
organization := o
lazy val aa = settingKey[Seq[String]]("")
Global / aa := Seq("initial-value")
// explicitly ThisBuild-scoped settings must apply exactly once, not once per project (#9668)
ThisBuild / aa += "added-value"
lazy val bb = settingKey[Seq[String]]("")
Global / bb := Seq("initial-value")
// ThisProject means "current project", so it must still apply once per project, unlike ThisBuild
ThisProject / bb += "added-value"
lazy val root = rootProject
.autoAggregate
@@ -34,5 +44,21 @@ LocalRootProject / check := {
assert((baz / organization).value == "com.example.baz")
// Test that baz/build.sbt settings don't leak onto qux, processed right after it (#9517)
assert((qux / organization).value == o, s"(qux / organization).value: ${(qux / organization).value}")
// Test that ThisBuild / aa += is applied exactly once, not once per project (#9668)
val expectedAa = Seq("initial-value", "added-value")
assert((root / aa).value == expectedAa, s"(root / aa).value: ${(root / aa).value}")
assert((foo / aa).value == expectedAa, s"(foo / aa).value: ${(foo / aa).value}")
assert((bar / aa).value == expectedAa, s"(bar / aa).value: ${(bar / aa).value}")
assert((baz / aa).value == expectedAa, s"(baz / aa).value: ${(baz / aa).value}")
assert((qux / aa).value == expectedAa, s"(qux / aa).value: ${(qux / aa).value}")
// Test that ThisProject / bb += still applies once per project (#9668)
val expectedBb = Seq("initial-value", "added-value")
assert((root / bb).value == expectedBb, s"(root / bb).value: ${(root / bb).value}")
assert((foo / bb).value == expectedBb, s"(foo / bb).value: ${(foo / bb).value}")
assert((bar / bb).value == expectedBb, s"(bar / bb).value: ${(bar / bb).value}")
assert((baz / bb).value == expectedBb, s"(baz / bb).value: ${(baz / bb).value}")
assert((qux / bb).value == expectedBb, s"(qux / bb).value: ${(qux / bb).value}")
}
check / aggregate := false