[2.x] fix: extraProjects with auto-root aggregate breaks key aggregation (#8690)

When using the aggregated key parser, a key is now valid if it exists in `data` for that scope **or** it's an aggregate key and every key it aggregates to exists in `data`. So `(root, scripted)` is accepted when the root aggregates a project that defines `scripted`, and running `scripted` at root runs it on that project as before.
This commit is contained in:
bitloi 2026-02-04 23:25:47 -05:00 committed by GitHub
parent b460bb871e
commit 2f27b5cecd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 89 additions and 11 deletions

View File

@ -63,7 +63,7 @@ object Act {
keyMap: Map[String, AttributeKey[?]],
data: Def.Settings
): Parser[ScopedKey[Any]] =
scopedKeySelected(index, current, defaultConfigs, keyMap, data, askProject = true)
scopedKeySelected(index, current, defaultConfigs, keyMap, data, askProject = true, None)
.map(_.key.asInstanceOf[ScopedKey[Any]])
// the index should be an aggregated index for proper tab completion
@ -80,6 +80,7 @@ object Act {
structure.index.keyMap,
structure.data,
askProject = true,
structure = Some(structure),
)
)
yield Aggregation.aggregate(
@ -102,6 +103,7 @@ object Act {
structure.index.keyMap,
structure.data,
askProject = optQuery.isEmpty,
structure = Some(structure),
)
yield Aggregation
.aggregate(selected.key, selected.mask, structure.extra)
@ -117,10 +119,11 @@ object Act {
keyMap: Map[String, AttributeKey[?]],
data: Def.Settings,
askProject: Boolean,
structure: Option[BuildStructure],
): Parser[ParsedKey] =
scopedKeyFull(index, current, defaultConfigs, keyMap, askProject = askProject).flatMap {
choices =>
select(choices, data)(using showRelativeKey2(current))
select(choices, data, structure)(using showRelativeKey2(current))
}
def scopedKeyFull(
@ -197,16 +200,39 @@ object Act {
key
)
def select(allKeys: Seq[Parser[ParsedKey]], data: Def.Settings)(using
show: Show[ScopedKey[?]]
): Parser[ParsedKey] =
def select(
allKeys: Seq[Parser[ParsedKey]],
data: Def.Settings
)(using show: Show[ScopedKey[?]]): Parser[ParsedKey] =
select(allKeys, data, None)
def select(
allKeys: Seq[Parser[ParsedKey]],
data: Def.Settings,
structure: Option[BuildStructure]
)(using show: Show[ScopedKey[?]]): Parser[ParsedKey] =
seq(allKeys) flatMap { ss =>
val default: Parser[ParsedKey] = ss.headOption match
case None => noValidKeys
case Some(x) => success(x)
selectFromValid(ss filter isValid(data), default)
val validFilter = structure.fold(isValid(data))(isValidForAggregate(data, _))
selectFromValid(ss filter validFilter, default)
}
private def isValidForAggregate(
data: Def.Settings,
structure: BuildStructure
)(parsed: ParsedKey): Boolean =
if data.contains(parsed.key) then true
else
val aggregated =
Aggregation.aggregate(
parsed.key.asInstanceOf[ScopedKey[Any]],
parsed.mask,
structure.extra
)
aggregated.nonEmpty && aggregated.exists(data.contains)
def selectFromValid(ss: Seq[ParsedKey], default: Parser[ParsedKey])(using
show: Show[ScopedKey[?]]
): Parser[ParsedKey] =
@ -230,7 +256,7 @@ object Act {
if (zeros.nonEmpty) zeros else selects
}
def noValidKeys = failure("No such key.")
def noValidKeys: Parser[ParsedKey] = failure("No such key.")
def showAmbiguous(keys: Seq[ScopedKey[?]])(using show: Show[ScopedKey[?]]): String =
keys.take(3).map(x => show.show(x)).mkString("", ", ", if (keys.size > 3) ", ..." else "")

View File

@ -867,7 +867,7 @@ private[sbt] object Load {
defaultProjects.generatedConfigClassFiles ++ loadedProjectsRaw.generatedConfigClassFiles
)
}
val loadedProjects = processAutoAggregate(loadedProjects0, uri)
val loadedProjects = processAutoAggregate(loadedProjects0, uri, normBase, !hasRoot)
timed("Load.loadUnit: cleanEvalClasses", log) {
cleanEvalClasses(defDir, keepClassFiles)
}
@ -889,9 +889,29 @@ private[sbt] object Load {
new BuildUnit(uri, normBase, loadedDefs, plugs, converter)
}
private def processAutoAggregate(inProjects: Seq[Project], uri: URI): Seq[Project] =
inProjects.map: proj =>
proj
private def processAutoAggregate(
inProjects: Seq[Project],
uri: URI,
buildBase: File,
hasAutoRoot: Boolean
): Seq[Project] =
if !hasAutoRoot then inProjects
else
val allProjectIds = inProjects.map(_.id).toSet
inProjects.map: proj =>
val isAutoRoot = isRootPath(proj.base, buildBase) && proj.aggregate.nonEmpty
if isAutoRoot then
val currentAggregateIds = proj.aggregate
.flatMap:
case ref: ProjectRef => Some(ref.project)
case _ => None
.toSet
val missingIds = allProjectIds - proj.id -- currentAggregateIds
if missingIds.nonEmpty then
val missingRefs = missingIds.toSeq.map(id => ProjectRef(uri, id))
proj.aggregate((proj.aggregate ++ missingRefs)*)
else proj
else proj
private def autoID(
localBase: File,

View File

@ -0,0 +1,16 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
val check = taskKey[Unit]("Repro for #4947: task at root when extraProjects creates auto root")
val a = project
val p = project
.settings(
name := "p",
check := ()
)

View File

@ -0,0 +1,15 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
import sbt._, Keys._
object ExtraPlugin extends AutoPlugin {
override def trigger = allRequirements
override def extraProjects: Seq[Project] =
Seq(Project("z", file("z")).settings(name := "z"))
}

View File

@ -0,0 +1 @@
> check