[2.x] fix: Ensure correct platform suffix in published artifact names

This commit is contained in:
Ali Rashid 2026-06-02 00:01:38 +03:00
parent fcd247e7c6
commit 8884488146
No known key found for this signature in database
GPG Key ID: 390A98FD741968E1
11 changed files with 116 additions and 29 deletions

View File

@ -106,11 +106,7 @@ private[librarymanagement] abstract class ArtifactFunctions {
val cross = CrossVersion(module.crossVersion, scalaVersion.full, scalaVersion.binary)
val withPlatform = module.crossVersion match {
case _: Disabled => artifact.name
case _ =>
module.platformOpt match {
case Some(p) if p.nonEmpty && p != Platform.jvm => s"${artifact.name}_$p"
case _ => artifact.name
}
case _ => CrossVersion.addPlatformSuffix(artifact.name, module.platformOpt, None)
}
val base = CrossVersion.applyCross(withPlatform, cross)
base + "-" + module.revision + classifierStr + "." + artifact.extension

View File

@ -166,6 +166,20 @@ private[librarymanagement] abstract class CrossVersionFunctions {
private[sbt] def crossName(name: String, cross: String): String =
name + "_" + cross
/**
* Appends the platform suffix (e.g. `native0.5`, `sjs1`) to `name`, preferring an explicit
* `platformOpt` over the `projectPlatform`. `""` and `jvm` add no suffix.
*/
def addPlatformSuffix(
name: String,
platformOpt: Option[String],
projectPlatform: Option[String]
): String =
(platformOpt orElse projectPlatform) match {
case Some(p) if p.nonEmpty && p != Platform.jvm => crossName(name, p)
case _ => name
}
/** Cross-versions `exclude` according to its `crossVersion`. */
private[sbt] def substituteCross(
exclude: ExclusionRule,

View File

@ -32,7 +32,7 @@ object FromSbt {
val name1 =
crossVersion match
case _: Disabled => name0
case _ => addPlatformSuffix(name0, platformOpt, projectPlatform)
case _ => CrossVersion.addPlatformSuffix(name0, platformOpt, projectPlatform)
val updatedName = CrossVersion(crossVersion, scalaVersion, scalaBinaryVersion)
.fold(name1)(_(name1))
if (!optionalCrossVer || updatedName.length <= name0.length)
@ -46,25 +46,6 @@ object FromSbt {
}
}
private def addPlatformSuffix(
name: String,
platformOpt: Option[String],
projectPlatform: Option[String]
): String = {
def addSuffix(platformName: String): String =
platformName match {
case "" | "jvm" => name
case _ => s"${name}_$platformName"
}
(platformOpt, projectPlatform) match {
case (Some(p), _) =>
addSuffix(p) // Use explicit platform if set (don't override with project platform)
case (None, Some(p)) =>
addSuffix(p) // Only use project platform if dependency has no explicit platform
case _ => name
}
}
private def attributes(attr: Map[String, String]): Map[String, String] =
attr
.map { (k, v) =>

View File

@ -234,8 +234,18 @@ object IvyActions {
}
private def crossVersionMap(moduleSettings: ModuleSettings): Option[String => String] =
moduleSettings match {
case i: InlineConfiguration => CrossVersion(i.module, i.scalaModuleInfo)
case _ => None
case i: InlineConfiguration =>
// Platform suffix before cross suffix, matching the coordinate (sbt/sbt#9117).
CrossVersion(i.module, i.scalaModuleInfo).map { fn => (name: String) =>
fn(
CrossVersion.addPlatformSuffix(
name,
i.module.platformOpt,
i.scalaModuleInfo.flatMap(_.platform)
)
)
}
case _ => None
}
def mapArtifacts(
module: ModuleDescriptor,

View File

@ -31,6 +31,7 @@ object CoursierArtifactsTasks {
val projId = sbt.Keys.projectID.value
val sv = sbt.Keys.scalaVersion.value
val sbv = sbt.Keys.scalaBinaryVersion.value
val projectPlatform = sbt.Keys.scalaModuleInfo.value.flatMap(_.platform)
val ivyConfs = sbt.Keys.ivyConfigurations.value
val extracted = Project.extract(s)
import extracted.*
@ -97,8 +98,13 @@ object CoursierArtifactsTasks {
def artifactPublication(artifact: Artifact) = {
// Platform suffix before cross suffix, matching the coordinate
val base = projId.crossVersion match
case _: Disabled => artifact.name
case _ =>
CrossVersion.addPlatformSuffix(artifact.name, projId.platformOpt, projectPlatform)
val name = CrossVersion(projId.crossVersion, sv, sbv)
.fold(artifact.name)(_(artifact.name))
.fold(base)(_(base))
CPublication(
name,

View File

@ -61,9 +61,14 @@ private[sbt] object PomGenerator:
</project>
private def crossVersionDep(dep: ModuleID, scalaInfo: Option[ScalaModuleInfo]): ModuleID =
// Platform suffix before cross suffix, matching the coordinate (sbt/sbt#9117).
val base = dep.crossVersion match
case _: Disabled => dep.name
case _ =>
CrossVersion.addPlatformSuffix(dep.name, dep.platformOpt, scalaInfo.flatMap(_.platform))
val crossFn = CrossVersion(dep, scalaInfo)
val crossDep = crossFn match
case Some(fn) => dep.withName(fn(dep.name)).withCrossVersion(CrossVersion.disabled)
case Some(fn) => dep.withName(fn(base)).withCrossVersion(CrossVersion.disabled)
case None => dep
if crossDep.exclusions.isEmpty || scalaInfo.isEmpty then crossDep
else

View File

@ -0,0 +1,51 @@
// sbt/sbt#9117: published artifact filenames must carry the platform suffix
// (e.g. _native0.5), matching the module coordinate / directory.
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "3.8.3"
ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache"
ThisBuild / platform := "native0.5"
ThisBuild / crossVersion := CrossVersion.binary
// expected cross+platform base name, identical to the coordinate directory
def expected(name: String) = s"${name}_native0.5_3"
// ivyless backend: the published filenames come from the coursier publication names,
// and the POM artifactId from PomGenerator.
lazy val ivyless = (project in file("ivyless"))
.settings(
useIvy := false,
ivyPaths := IvyPaths(baseDirectory.value.toString, Some((target.value / "ivy2").toString)),
TaskKey[Unit]("check") := {
val nm = expected(moduleName.value)
val dir = target.value / "ivy2" / "local" / organization.value / nm / version.value
def req(f: File): Unit = assert(f.exists, s"expected $f to exist")
req(dir / "jars" / s"$nm.jar")
req(dir / "srcs" / s"$nm-sources.jar")
val pom = dir / "poms" / s"$nm.pom"
req(pom)
assert(IO.read(pom).contains(s"<artifactId>$nm</artifactId>"), s"POM artifactId must be $nm: ${IO.read(pom)}")
}
)
// Ivy backend (sbt-ivy): the published filenames come from CrossVersion.substituteCross.
lazy val ivyfull = (project in file("ivyfull"))
.settings(
useIvy := true,
publishMavenStyle := true,
ivyPaths := IvyPaths(baseDirectory.value.toString, Some((target.value / "ivy2").toString)),
publishTo := Some(MavenCache("test-maven", target.value / "maven-repo")),
TaskKey[Unit]("check") := {
val nm = expected(moduleName.value)
val ver = version.value
def req(f: File): Unit = assert(f.exists, s"expected $f to exist")
val ivyDir = target.value / "ivy2" / "local" / organization.value / nm / ver
req(ivyDir / "jars" / s"$nm.jar")
req(ivyDir / "srcs" / s"$nm-sources.jar")
req(ivyDir / "poms" / s"$nm.pom")
val mvnDir = target.value / "maven-repo" / organization.value.replace('.', '/') / nm / ver
req(mvnDir / s"$nm-$ver.jar")
req(mvnDir / s"$nm-$ver.pom")
}
)

View File

@ -0,0 +1,4 @@
package lib
object Lib:
def greeting: String = "hi"

View File

@ -0,0 +1,4 @@
package lib
object Lib:
def greeting: String = "hi"

View File

@ -0,0 +1,5 @@
// sbt-ivy provides the Ivy publish backend exercised by the `ivyfull` project.
libraryDependencies += {
val sbtV = sbtVersion.value
("org.scala-sbt" % s"sbt-ivy_sbt${sbtV}_${scalaBinaryVersion.value}" % sbtV).intransitive()
}

View File

@ -0,0 +1,11 @@
# sbt/sbt#9117: published artifact filenames must carry the platform suffix
# (_native0.5), matching the coordinate, on both the ivyless and Ivy backends.
# ivyless backend: publication names + POM artifactId
> ivyless/publishLocal
> ivyless/check
# Ivy backend (useIvy := true): publishLocal (ivy layout) and publish (maven layout)
> ivyfull/publishLocal
> ivyfull/publish
> ivyfull/check