[2.x] fix: Use correct configuration identifier for display (#8698)

**Problem**
The configuration name translation in logging was incorrect. When a
configuration like MultiJvm (id="MultiJvm", name="multi-jvm") was
displayed, it showed "Multi-jvm" instead of "MultiJvm" because the
display logic was guessing the identifier by capitalizing the ivy
config name.

**Solution**
This fix:
- Adds configNameToIdent reverse mapping in ConfigIndex to look up
  the correct Configuration.id from the ivy config name
- Adds toConfigIdent method in KeyIndex trait for display lookup
- Updates Scope.display to accept a config name lookup function
- Updates showLoadingKey and showContextKey to use the index lookup

Fixes #5211

Generated-by: Claude
This commit is contained in:
bitloi 2026-02-06 12:54:58 -05:00 committed by GitHub
parent 47e7133260
commit 4e0180d759
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 181 additions and 48 deletions

View File

@ -733,6 +733,7 @@ lazy val mainProj = (project in file("main"))
exclude[DirectMissingMethodProblem]("sbt.coursierint.LMCoursier.coursierConfiguration"),
exclude[IncompatibleMethTypeProblem]("sbt.internal.Compiler.scalaInstanceTask"),
exclude[ReversedMissingMethodProblem]("sbt.ScriptedRun.invoke"),
exclude[ReversedMissingMethodProblem]("sbt.internal.KeyIndex.toConfigIdent"),
),
)
.dependsOn(lmCore, lmIvy, lmCoursierShadedPublishing)

View File

@ -102,6 +102,12 @@ object Def extends BuildSyntax with Init with InitializeImplicits:
private[sbt] def showShortKey(
keyNameColor: Option[String],
): Show[ScopedKey[?]] =
showShortKey(keyNameColor, Scope.guessConfigIdent)
private[sbt] def showShortKey(
keyNameColor: Option[String],
configNameToIdent: String => String,
): Show[ScopedKey[?]] = {
def displayShort(
project: Reference
@ -117,7 +123,8 @@ object Def extends BuildSyntax with Init with InitializeImplicits:
Scope.display(
key.scope,
withColor(key.key.label, keyNameColor),
ref => displayShort(ref)
ref => displayShort(ref),
configNameToIdent
)
)
}

View File

@ -169,6 +169,9 @@ object Scope:
def display(config: ConfigKey): String = guessConfigIdent(config.name) + " /"
def display(config: ConfigKey, configNameToIdent: String => String): String =
configNameToIdent(config.name) + " /"
private[sbt] val configIdents: Map[String, String] =
Map(
"scala-tool" -> "ScalaTool",
@ -191,6 +194,14 @@ object Scope:
def display(scope: Scope, sep: String, showProject: Reference => String): String =
displayMasked(scope, sep, showProject, ScopeMask())
def display(
scope: Scope,
sep: String,
showProject: Reference => String,
configNameToIdent: String => String
): String =
displayMasked(scope, sep, showProject, ScopeMask(), showZeroConfig = false, configNameToIdent)
private[sbt] def displayPedantic(scope: Scope, sep: String): String =
displayMasked(scope, sep, showProject, ScopeMask(), true)
@ -237,25 +248,35 @@ object Scope:
showProject: Reference => String,
mask: ScopeMask,
showZeroConfig: Boolean
): String = {
): String =
displayMasked(scope, sep, showProject, mask, showZeroConfig, guessConfigIdent)
def displayMasked(
scope: Scope,
sep: String,
showProject: Reference => String,
mask: ScopeMask,
showZeroConfig: Boolean,
configNameToIdent: String => String
): String =
import scope.{ project, config, task, extra }
extra.toOption.flatMap(_.get(customShowString)).getOrElse {
val zeroConfig = if (showZeroConfig) "Zero /" else ""
val configPrefix = config.foldStrict(display, zeroConfig, "./")
val taskPrefix = task.foldStrict(_.label + " /", "", "./")
val extras = extra.foldStrict(_.entries.map(_.toString).toList, nil, nil)
val postfix = if (extras.isEmpty) "" else extras.mkString("(", ", ", ")")
if (scope == GlobalScope) "Global / " + sep + postfix
else
mask.concatShow(
appendSpace(projectPrefix(project, showProject)),
appendSpace(configPrefix),
appendSpace(taskPrefix),
sep,
postfix
)
}
}
extra.toOption
.flatMap(_.get(customShowString))
.getOrElse:
val zeroConfig = if showZeroConfig then "Zero /" else ""
val configPrefix = config.foldStrict(c => display(c, configNameToIdent), zeroConfig, "./")
val taskPrefix = task.foldStrict(_.label + " /", "", "./")
val extras = extra.foldStrict(_.entries.map(_.toString).toList, nil, nil)
val postfix = if extras.isEmpty then "" else extras.mkString("(", ", ", ")")
if scope == GlobalScope then "Global / " + sep + postfix
else
mask.concatShow(
appendSpace(projectPrefix(project, showProject)),
appendSpace(configPrefix),
appendSpace(taskPrefix),
sep,
postfix
)
private[sbt] def appendSpace(s: String): String =
if (s == "") ""

View File

@ -9,7 +9,7 @@
package sbt
import sbt.BasicCommandStrings.{ StashOnFailure, networkExecPrefix }
import sbt.ProjectExtra.extract
import sbt.ProjectExtra.*
import sbt.internal.{ ConsoleChannel, FastTrackCommands, ShutdownHooks, SysProp, TaskProgress }
import sbt.internal.langserver.ErrorCodes
import sbt.internal.nio.CheckBuildSources.CheckBuildSourcesKey
@ -156,7 +156,13 @@ private[sbt] object MainLoop:
state.get(Keys.superShellSleep.key).getOrElse(SysProp.supershellSleep.millis)
val superShellThreshold =
state.get(Keys.superShellThreshold.key).getOrElse(SysProp.supershellThreshold)
val taskProgress = new TaskProgress(superShellSleep, superShellThreshold, state.log)
val taskProgress =
new TaskProgress(
superShellSleep,
superShellThreshold,
state.log,
Project.configNameToIdent(state)
)
val gcMonitor = if (SysProp.gcMonitor) Some(new sbt.internal.GCMonitor(state.log)) else None
try {
ErrorHandling.wideConvert {

View File

@ -42,6 +42,7 @@ import Def.{ Flattened, Initialize, ScopedKey, Setting }
import sbt.internal.{
Load,
BuildStructure,
KeyIndex,
LoadedBuild,
LoadedBuildUnit,
SettingGraph,
@ -199,31 +200,69 @@ trait ProjectExtra extends Scoped.Syntax:
showContextKey(state, None)
def showContextKey(state: State, keyNameColor: Option[String]): Show[ScopedKey[?]] =
if (isProjectLoaded(state)) showContextKey2(session(state), keyNameColor)
if isProjectLoaded(state) then
val se = session(state)
val st = structure(state)
showContextKey2(se, st.index.keyIndex, keyNameColor)
else Def.showFullKey
// @deprecated("Use showContextKey2 which doesn't take the unused structure param", "1.1.1")
// def showContextKey(
// session: SessionSettings,
// structure: BuildStructure,
// keyNameColor: Option[String] = None
// ): Show[ScopedKey[_]] =
// showContextKey2(session, keyNameColor)
def showContextKey2(
session: SessionSettings,
keyNameColor: Option[String] = None
): Show[ScopedKey[?]] =
Def.showRelativeKey2(session.current, keyNameColor)
def showContextKey2(
session: SessionSettings,
keyIndex: KeyIndex,
keyNameColor: Option[String]
): Show[ScopedKey[?]] =
val current = session.current
val configNameToIdent: String => String = name => keyIndex.toConfigIdent(Some(current))(name)
Show[ScopedKey[?]]: key =>
val color: String => String = Def.withColor(_, keyNameColor)
key.scope.extra.toOption
.flatMap(_.get(Scope.customShowString).map(color))
.getOrElse:
Scope.display(
key.scope,
color(key.key.label),
ref => Def.displayRelative2(current, ref),
configNameToIdent
)
def showLoadingKey(
loaded: LoadedBuild,
keyNameColor: Option[String] = None
): Show[ScopedKey[?]] =
Def.showRelativeKey2(
ProjectRef(loaded.root, loaded.units(loaded.root).rootProjects.head),
keyNameColor
)
val configNameToIdent = buildConfigNameToIdent(loaded)
val current = ProjectRef(loaded.root, loaded.units(loaded.root).rootProjects.head)
Show[ScopedKey[?]]: key =>
val color: String => String = Def.withColor(_, keyNameColor)
key.scope.extra.toOption
.flatMap(_.get(Scope.customShowString).map(color))
.getOrElse:
Scope.display(
key.scope,
color(key.key.label),
ref => Def.displayRelative2(current, ref),
configNameToIdent
)
private[sbt] def configNameToIdent(state: State): String => String =
if isProjectLoaded(state) then buildConfigNameToIdent(structure(state).units)
else Scope.guessConfigIdent
private def buildConfigNameToIdent(loaded: LoadedBuild): String => String =
buildConfigNameToIdent(loaded.units)
private def buildConfigNameToIdent(units: Map[URI, LoadedBuildUnit]): String => String =
val configMap = (for
(_, unit) <- units.iterator
(_, project) <- unit.defined.iterator
config <- project.configurations.iterator
yield config.name -> config.id).toMap
name => configMap.getOrElse(name, Scope.guessConfigIdent(name))
def getOrError[T](state: State, key: AttributeKey[T], msg: String): T =
state.get(key).getOrElse(sys.error(msg))
@ -253,11 +292,11 @@ trait ProjectExtra extends Scoped.Syntax:
val se = Project.session(state)
val st = Project.structure(state)
val currentRef = internal.ProjectNavigation.effectiveCurrentRef(state)
Extracted(st, se, currentRef)(using Project.showContextKey2(se))
Extracted(st, se, currentRef)(using Project.showContextKey2(se, st.index.keyIndex, None))
}
private[sbt] def extract(se: SessionSettings, st: BuildStructure): Extracted =
Extracted(st, se, se.current)(using Project.showContextKey2(se))
Extracted(st, se, se.current)(using Project.showContextKey2(se, st.index.keyIndex, None))
def getProjectForReference(ref: Reference, structure: BuildStructure): Option[ResolvedProject] =
ref match
@ -401,7 +440,7 @@ trait ProjectExtra extends Scoped.Syntax:
case None => ""
val (definingKey, providedBy) = structure.data.definingKey(key) match
case Some(k) => k -> s"Provided by:\n\t${Scope.display(k.scope, key.key.label)}\n"
case Some(k) => k -> s"Provided by:\n\t${display.show(ScopedKey(k.scope, key.key))}\n"
case None => key -> ""
val comp =
Def.compiled(structure.settings, actual)(using

View File

@ -17,10 +17,12 @@ import scala.collection.mutable
import scala.collection.immutable.VectorBuilder
import scala.concurrent.duration.*
private[sbt] abstract class AbstractTaskExecuteProgress extends ExecuteProgress {
private[sbt] abstract class AbstractTaskExecuteProgress(
configNameToIdent: String => String = Scope.guessConfigIdent
) extends ExecuteProgress {
import AbstractTaskExecuteProgress.Timer
private val showScopedKey = Def.showShortKey(None)
private val showScopedKey = Def.showShortKey(None, configNameToIdent)
private val anonOwners = new ConcurrentHashMap[TaskId[?], TaskId[?]]
private val calledBy = new ConcurrentHashMap[TaskId[?], TaskId[?]]
private val timings = new ConcurrentHashMap[TaskId[?], Timer]

View File

@ -48,7 +48,13 @@ object KeyIndex {
val data = ids map { id =>
val configs = configurations.getOrElse(id, Seq())
val configIdentToName = configs.map(config => config.id -> config.name).toMap
Option(id) -> new ConfigIndex(Map.empty, configIdentToName, emptyAKeyIndex)
val configNameToIdent = configs.map(config => config.name -> config.id).toMap
Option(id) -> new ConfigIndex(
Map.empty,
configIdentToName,
configNameToIdent,
emptyAKeyIndex
)
}
Option(uri) -> new ProjectIndex(data.toMap)
}
@ -66,6 +72,11 @@ object KeyIndex {
case Some(idx) => idx.fromConfigIdent(proj)(configIdent)
case _ => Scope.unguessConfigIdent(configIdent)
}
private[sbt] def toConfigIdent(proj: Option[ResolvedReference])(configName: String): String =
indices.find(idx => idx.exists(proj)) match {
case Some(idx) => idx.toConfigIdent(proj)(configName)
case _ => Scope.guessConfigIdent(configName)
}
def tasks(proj: Option[ResolvedReference], conf: Option[String]) = concat(_.tasks(proj, conf))
def tasks(proj: Option[ResolvedReference], conf: Option[String], key: String) =
concat(_.tasks(proj, conf, key))
@ -79,7 +90,8 @@ object KeyIndex {
private[sbt] def getOr[A, B](m: Map[A, B], key: A, or: B): B = m.getOrElse(key, or)
private[sbt] def keySet[A, B](m: Map[Option[A], B]): Set[A] = m.keys.flatten.toSet
private[sbt] val emptyAKeyIndex = new AKeyIndex(Relation.empty)
private[sbt] val emptyConfigIndex = new ConfigIndex(Map.empty, Map.empty, emptyAKeyIndex)
private[sbt] val emptyConfigIndex =
new ConfigIndex(Map.empty, Map.empty, Map.empty, emptyAKeyIndex)
private[sbt] val emptyProjectIndex = new ProjectIndex(Map.empty)
private[sbt] val emptyBuildIndex = new BuildIndex(Map.empty)
@ -115,6 +127,7 @@ trait KeyIndex {
): Set[String]
private[sbt] def configIdents(project: Option[ResolvedReference]): Set[String]
private[sbt] def fromConfigIdent(proj: Option[ResolvedReference])(configIdent: String): String
private[sbt] def toConfigIdent(proj: Option[ResolvedReference])(configName: String): String
}
trait ExtendableKeyIndex extends KeyIndex {
def add(scoped: ScopedKey[?]): ExtendableKeyIndex
@ -135,11 +148,13 @@ private[sbt] case class IdentifiableConfig(name: String, ident: Option[String])
/*
* data contains the mapping between a configuration name and its keys.
* configIdentToName contains the mapping between a configuration ident and its name
* configNameToIdent contains the reverse mapping from name to ident for display purposes
* noConfigKeys contains the keys without a configuration.
*/
private[sbt] final class ConfigIndex(
val data: Map[String, AKeyIndex],
val configIdentToName: Map[String, String],
val configNameToIdent: Map[String, String],
val noConfigKeys: AKeyIndex
) {
def add(
@ -157,18 +172,22 @@ private[sbt] final class ConfigIndex(
config: IdentifiableConfig,
task: Option[AttributeKey[?]],
key: AttributeKey[?]
): ConfigIndex = {
): ConfigIndex =
val keyIndex = data.getOrElse(config.name, emptyAKeyIndex)
val configIdent = config.ident.getOrElse(Scope.guessConfigIdent(config.name))
// Only add to configNameToIdent if not already present to preserve correct mappings from base()
val updatedNameToIdent =
if configNameToIdent.contains(config.name) then configNameToIdent
else configNameToIdent.updated(config.name, configIdent)
new ConfigIndex(
data.updated(config.name, keyIndex.add(task, key)),
configIdentToName.updated(configIdent, config.name),
updatedNameToIdent,
noConfigKeys
)
}
def addKeyWithoutConfig(task: Option[AttributeKey[?]], key: AttributeKey[?]): ConfigIndex = {
new ConfigIndex(data, configIdentToName, noConfigKeys.add(task, key))
new ConfigIndex(data, configIdentToName, configNameToIdent, noConfigKeys.add(task, key))
}
def keyIndex(conf: Option[String]): AKeyIndex = conf match {
@ -179,8 +198,13 @@ private[sbt] final class ConfigIndex(
def configs: Set[String] = data.keySet
private[sbt] lazy val idents: Set[String] = configIdentToName.keySet
// guess Configuration name from an identifier.
// There's a guessing involved because we could have scoped key that Project is not aware of.
// Looks up the display identifier for a configuration name.
// Falls back to guessing if the name is not in the index.
private[sbt] def toConfigIdent(name: String): String =
configNameToIdent.getOrElse(name, Scope.guessConfigIdent(name))
// Looks up the configuration name from an identifier.
// Falls back to guessing if the identifier is not in the index.
private[sbt] def fromConfigIdent(ident: String): String =
configIdentToName.getOrElse(ident, Scope.unguessConfigIdent(ident))
}
@ -224,6 +248,9 @@ private[sbt] final class KeyIndex0(val data: BuildIndex) extends ExtendableKeyIn
private[sbt] def fromConfigIdent(proj: Option[ResolvedReference])(configIdent: String): String =
confIndex(proj).fromConfigIdent(configIdent)
private[sbt] def toConfigIdent(proj: Option[ResolvedReference])(configName: String): String =
confIndex(proj).toConfigIdent(configName)
def tasks(proj: Option[ResolvedReference], conf: Option[String]): Set[AttributeKey[?]] =
keyIndex(proj, conf).tasks
def tasks(

View File

@ -25,8 +25,9 @@ import sbt.util.Logger
private[sbt] class TaskProgress(
sleepDuration: FiniteDuration,
threshold: FiniteDuration,
logger: Logger
) extends AbstractTaskExecuteProgress
logger: Logger,
configNameToIdent: String => String = Scope.guessConfigIdent
) extends AbstractTaskExecuteProgress(configNameToIdent)
with ExecuteProgress
with AutoCloseable {
private val lastTaskCount = new AtomicInteger(0)

View File

@ -0,0 +1 @@
// intentionally empty - CheckPlugin provides settings

View File

@ -0,0 +1,25 @@
package sbt
import sbt.*
import Keys.*
object CheckPlugin extends AutoPlugin:
override def requires = plugins.JvmPlugin
override def trigger = allRequirements
lazy val MultiJvm = config("multi-jvm")
lazy val test1 = taskKey[Unit]("")
override def projectConfigurations = Seq(MultiJvm)
override def projectSettings = Seq(
MultiJvm / test1 := {},
commands += Command.command("checkShortKeyDisplay") { state =>
val configNameToIdent = Project.configNameToIdent(state)
val show = Def.showShortKey(None, configNameToIdent)
val displayed = show.show((MultiJvm / test1).scopedKey)
if !displayed.contains("MultiJvm") then
sys.error(s"Expected 'MultiJvm' in short key display but got: '$displayed'")
state.log.info(s"Config display check passed: $displayed")
state
},
)

View File

@ -0,0 +1,3 @@
# Verify that custom configuration MultiJvm displays correctly
# instead of the guessed Multi-jvm (#5211)
> checkShortKeyDisplay