[2.x] fix: excluding artifacts w/o scala version suffix (#7454) (#8756)

**Problem**

Excluding artifacts using `.exclude(org, name)` requires you to
explicitly set scala version in `name`.

**Solution**

- Deprecating `.exclude(org, name)` in favor of new
  `.exclude(OrganizationArtifactName)`
- Fixing `lmcoursier.FromSbt` ignores `excludeRule.crossVersion`
This commit is contained in:
Daniil Sivak 2026-02-25 23:06:16 +03:00 committed by GitHub
parent 1a8ac935f9
commit 12cbd877bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 156 additions and 10 deletions

View File

@ -9,6 +9,7 @@ import sbt.internal.librarymanagement.mavenint.SbtPomExtraProperties
import scala.collection.mutable.ListBuffer
import sbt.librarymanagement.syntax.*
import sbt.util.Logger
import sbt.librarymanagement.DependencyBuilders.OrganizationArtifactName
private[librarymanagement] abstract class ModuleIDExtra {
def organization: String
@ -152,9 +153,18 @@ private[librarymanagement] abstract class ModuleIDExtra {
def excludeAll(rules: ExclusionRule*): ModuleID = withExclusions(exclusions ++ rules)
/** Excludes the dependency with organization `org` and `name` from being introduced by this dependency during resolution. */
@deprecated(
"`name` parameter must contain scala version if artifact is cross-versioned. Use `exclude(\"org\" %% \"name\")`.",
since = "2.0.0"
)
def exclude(org: String, name: String): ModuleID =
excludeAll(ExclusionRule().withOrganization(org).withName(name))
/** Excludes the dependency from being introduced by this dependency during resolution. */
def exclude(rule: OrganizationArtifactName): ModuleID = {
excludeAll(rule)
}
/**
* Adds extra attributes for this module. All keys are prefixed with `e:` if they are not already so prefixed.
* This information will only be published in an ivy.xml and not in a pom.xml.

View File

@ -19,19 +19,21 @@ import sbt.librarymanagement.{ Configuration as _, * }
object FromSbt {
private def sbtModuleIdName(
moduleId: ModuleID,
private def sbtCrossName(
name: String,
crossVersion: CrossVersion,
platformOpt: Option[String],
scalaVersion: => String,
scalaBinaryVersion: => String,
optionalCrossVer: Boolean = false,
projectPlatform: Option[String],
): String = {
val name0 = moduleId.name
val name0 = name
val name1 =
moduleId.crossVersion match
crossVersion match
case _: Disabled => name0
case _ => addPlatformSuffix(name0, moduleId.platformOpt, projectPlatform)
val updatedName = CrossVersion(moduleId.crossVersion, scalaVersion, scalaBinaryVersion)
case _ => addPlatformSuffix(name0, platformOpt, projectPlatform)
val updatedName = CrossVersion(crossVersion, scalaVersion, scalaBinaryVersion)
.fold(name1)(_(name1))
if (!optionalCrossVer || updatedName.length <= name0.length)
updatedName
@ -81,7 +83,15 @@ object FromSbt {
): (Module, String) = {
val fullName =
sbtModuleIdName(module, scalaVersion, scalaBinaryVersion, optionalCrossVer, projectPlatform)
sbtCrossName(
module.name,
module.crossVersion,
module.platformOpt,
scalaVersion,
scalaBinaryVersion,
optionalCrossVer,
projectPlatform
)
val module0 = Module(
Organization(module.organization),
@ -125,7 +135,16 @@ object FromSbt {
Configuration(""),
exclusions = module.exclusions.map { rule =>
// FIXME Other `rule` fields are ignored here
(Organization(rule.organization), ModuleName(rule.name))
val ruleFullName = sbtCrossName(
rule.name,
rule.crossVersion,
platformOpt = None,
scalaVersion,
scalaBinaryVersion,
optionalCrossVer,
projectPlatform
)
(Organization(rule.organization), ModuleName(ruleFullName))
}.toSet,
Publication("", Type(""), Extension(""), Classifier("")),
optional = false,
@ -200,8 +219,10 @@ object FromSbt {
Module(
Organization(projectID.organization),
ModuleName(
sbtModuleIdName(
projectID,
sbtCrossName(
projectID.name,
projectID.crossVersion,
projectID.platformOpt,
scalaVersion,
scalaBinaryVersion,
projectPlatform = projectPlatform

View File

@ -3,6 +3,7 @@ package sbt.internal.librarymanagement
import sbt.librarymanagement.*
import sbt.librarymanagement.syntax.*
import DependencyBuilders.OrganizationArtifactName
import scala.annotation.nowarn
object InclExclSpec extends BaseIvySpecification {
val scala210 = Some("2.10.4")
@ -24,6 +25,35 @@ object InclExclSpec extends BaseIvySpecification {
testLiftJsonIsMissing(report)
}
test(
"it should exclude any version of cross-built lift-json using `.exclude(String, String)` method with direct scala version definition"
) {
@nowarn
val liftDep =
("net.liftweb" %% "lift-mapper" % "2.6-M4" % "compile")
.exclude("net.liftweb", "lift-json_2.10")
val report = getIvyReport(liftDep, scala210)
testLiftJsonIsMissing(report)
}
test(
"it should exclude any version of cross-built lift-json using `.exclude(OrganizationArtifactName)` method"
) {
val liftDep =
("net.liftweb" %% "lift-mapper" % "2.6-M4" % "compile")
.exclude("net.liftweb" %% "lift-json")
val report = getIvyReport(liftDep, scala210)
testLiftJsonIsMissing(report)
}
test("it should exclude any version of cross-built lift-json using `.excludeAll` method") {
val liftDep =
("net.liftweb" %% "lift-mapper" % "2.6-M4" % "compile")
.excludeAll("net.liftweb" %% "lift-json")
val report = getIvyReport(liftDep, scala210)
testLiftJsonIsMissing(report)
}
val scala2122 = Some("2.12.2")
test("it should exclude a concrete version of lift-json when it's full cross version") {
val excluded: ModuleID = ("org.scalameta" % "scalahost" % "1.7.0").cross(CrossVersion.full)

View File

@ -0,0 +1,27 @@
ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache"
ivyPaths := IvyPaths(
(ThisBuild / baseDirectory).value.toString,
Some(((ThisBuild / baseDirectory).value / "ivy" / "cache").toString)
)
resolvers += "test-resolver" at ((ThisBuild / baseDirectory).value / "repo").toURI.toString
organization := "org.example"
version := "0.1.0-SNAPSHOT"
scalaVersion := "2.12.20"
@scala.annotation.nowarn
lazy val libWithExcludedDeps = (project in file("lib"))
.settings(
name := "lib-with-excluded-deps",
libraryDependencies += ("org.typelevel" %% "cats-effect" % "3.6.3")
.excludeAll("org.typelevel" %% "cats-effect-kernel")
.exclude("org.typelevel", "cats-effect-std_2.12")
.exclude("org.typelevel" %% "cats-mtl"),
publishTo := Some(Resolver.file("test-publish", (ThisBuild / baseDirectory).value / "repo")),
)
lazy val dependsOnLibWithExcludedDeps = (project in file("."))
.settings(
name := "depends-on-lib-with-excluded-deps",
libraryDependencies += "org.example" %% "lib-with-excluded-deps" % "0.1.0-SNAPSHOT",
)

View File

@ -0,0 +1,39 @@
import java.io.File
import java.nio.file.Files
import scala.util.Try
object Main {
def classFound(clsName: String) = Try(
Thread.currentThread()
.getContextClassLoader()
.loadClass(clsName)
).toOption.nonEmpty
def main(args: Array[String]): Unit = {
val ioFound = classFound("cats.effect.IO")
val asyncFound = classFound("cats.effect.kernel.Async")
val mutexFound = classFound("cats.effect.std.Mutex")
val askFound = classFound("cats.mtl.Ask")
assert(
ioFound,
"Expected to find class from cats-effect"
)
assert(
!asyncFound,
"Expected not to find class from cats-effect-kernel"
)
assert(
!mutexFound,
"Expected not to find class from cats-effect-std"
)
assert(
!askFound,
"Expected not to find class from cats-mtl"
)
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
}
}

View File

@ -0,0 +1,19 @@
$ delete output
$ delete repo
$ delete coursier-cache
$ delete ivy
> set libWithExcludedDeps/publishMavenStyle := true
> libWithExcludedDeps/publish
> dependsOnLibWithExcludedDeps/run
$ exists output
$ delete output
$ delete repo
$ delete coursier-cache
$ delete ivy
> set libWithExcludedDeps/publishMavenStyle := false
> libWithExcludedDeps/publish
> dependsOnLibWithExcludedDeps/run
$ exists output