From 9a48ed818f7c65074ac804149e03a1e4fd4728e4 Mon Sep 17 00:00:00 2001
From: Dream <42954461+eureka928@users.noreply.github.com>
Date: Sun, 29 Mar 2026 00:37:00 -0400
Subject: [PATCH] [2.x] fix: Preserve configuration visibility in ivy.xml when
publishing with coursier (#8988)
**Problem**
When publishing with coursier, the `visibility` attribute of ``
elements in `ivy.xml` was hardcoded to `"public"`. Configurations
defined as hidden (e.g. `scala-tool`) were incorrectly published with
`visibility="public"` instead of `"private"`.
The root cause was that `Inputs.configExtendsSeq` only extracted config
names and extends relationships, discarding the `isPublic` flag from
sbt's `Configuration` objects.
**Solution**
Add a `privateConfigNames: Set[String]` field to the lmcoursier
`Project` data class (with `@since` for backward compatibility).
Populate it from `Configuration.isPublic` in `CoursierInputsTasks`,
and use it in both `IvyXml` implementations to set the correct
visibility attribute.
Fixes sbt/sbt#5455
---
.../lmcoursier/definitions/Project.scala | 5 +++--
.../src/main/scala/lmcoursier/Inputs.scala | 5 +++++
.../src/main/scala/lmcoursier/IvyXml.scala | 3 ++-
.../sbt/coursierint/CoursierInputsTasks.scala | 19 +++++++++++--------
.../internal/librarymanagement/IvyXml.scala | 4 +++-
5 files changed, 24 insertions(+), 12 deletions(-)
diff --git a/lm-coursier/definitions/src/main/scala/lmcoursier/definitions/Project.scala b/lm-coursier/definitions/src/main/scala/lmcoursier/definitions/Project.scala
index 97b5e7af4..65f42ecb7 100644
--- a/lm-coursier/definitions/src/main/scala/lmcoursier/definitions/Project.scala
+++ b/lm-coursier/definitions/src/main/scala/lmcoursier/definitions/Project.scala
@@ -1,6 +1,6 @@
package lmcoursier.definitions
-import dataclass.data
+import dataclass.{ data, since }
@data class Project(
module: Module,
@@ -10,5 +10,6 @@ import dataclass.data
properties: Seq[(String, String)],
packagingOpt: Option[Type],
publications: Seq[(Configuration, Publication)],
- info: Info
+ info: Info,
+ @since privateConfigs: Set[Configuration] = Set.empty
)
diff --git a/lm-coursier/src/main/scala/lmcoursier/Inputs.scala b/lm-coursier/src/main/scala/lmcoursier/Inputs.scala
index 561e0df5f..4cdbfb504 100644
--- a/lm-coursier/src/main/scala/lmcoursier/Inputs.scala
+++ b/lm-coursier/src/main/scala/lmcoursier/Inputs.scala
@@ -21,6 +21,11 @@ object Inputs {
configurations
.map(cfg => Configuration(cfg.name) -> cfg.extendsConfigs.map(c => Configuration(c.name)))
+ def privateConfigs(
+ configurations: Seq[sbt.librarymanagement.Configuration]
+ ): Set[Configuration] =
+ configurations.filterNot(_.isPublic).map(cfg => Configuration(cfg.name)).toSet
+
def coursierConfigurationsMap(
configurations: Seq[sbt.librarymanagement.Configuration]
): Map[Configuration, Set[Configuration]] = {
diff --git a/lm-coursier/src/main/scala/lmcoursier/IvyXml.scala b/lm-coursier/src/main/scala/lmcoursier/IvyXml.scala
index d40f2256f..02ba01912 100644
--- a/lm-coursier/src/main/scala/lmcoursier/IvyXml.scala
+++ b/lm-coursier/src/main/scala/lmcoursier/IvyXml.scala
@@ -63,7 +63,8 @@ object IvyXml {
} % infoAttrs
val confElems = project.configurations.toVector.collect { (name, extends0) =>
- val n =
+ val visibility = if (project.privateConfigs.contains(name)) "private" else "public"
+ val n =
if (extends0.nonEmpty)
n % .attributes
else
diff --git a/main/src/main/scala/sbt/coursierint/CoursierInputsTasks.scala b/main/src/main/scala/sbt/coursierint/CoursierInputsTasks.scala
index cbfe1615a..1b4c9d40d 100644
--- a/main/src/main/scala/sbt/coursierint/CoursierInputsTasks.scala
+++ b/main/src/main/scala/sbt/coursierint/CoursierInputsTasks.scala
@@ -37,15 +37,18 @@ object CoursierInputsTasks {
): CProject = {
val configMap = Inputs.configExtendsSeq(configurations).toMap
+ val privConfigs = Inputs.privateConfigs(configurations)
- val proj0 = FromSbt.project(
- projId,
- dependencies,
- configMap,
- sv,
- sbv,
- projectPlatform,
- )
+ val proj0 = FromSbt
+ .project(
+ projId,
+ dependencies,
+ configMap,
+ sv,
+ sbv,
+ projectPlatform,
+ )
+ .withPrivateConfigs(privConfigs)
val proj1 = auOpt match {
case Some(au) =>
proj0.withProperties(proj0.properties :+ (SbtPomExtraProperties.POM_API_KEY -> au.toString))
diff --git a/sbt-ivy/src/main/scala/sbt/internal/librarymanagement/IvyXml.scala b/sbt-ivy/src/main/scala/sbt/internal/librarymanagement/IvyXml.scala
index 3d195b2f9..1ffa743b8 100644
--- a/sbt-ivy/src/main/scala/sbt/internal/librarymanagement/IvyXml.scala
+++ b/sbt-ivy/src/main/scala/sbt/internal/librarymanagement/IvyXml.scala
@@ -127,7 +127,9 @@ object IvyXml {
val confElems = project.configurations.toVector.collect {
case (name, extends0) if !shadedConfigOpt.exists(_.value == name.value) =>
val extends1 = shadedConfigOpt.fold(extends0)(c => extends0.filter(_.value != c.value))
- val n =
+ val visibility =
+ if (project.privateConfigs.contains(name)) "private" else "public"
+ val n =
if (extends1.nonEmpty)
n % .attributes
else