[2.x] fix: Fixes publishing platform-specific artifacts (like Scala Native)

**Problem**
Platform suffix is absent in published artifacts.
#9118 partially solved this. It didn't touch publish-side naming though, so the published files still drop the suffix in three places.

**Solution**
This fixes
- ivyless publication names - CoursierArtifactsTasks.coursierPublicationsTask
- ivyless POM <artifactId> (the coordinate written inside the .pom) - PomGenerator.crossVersionDep
- Ivy backend (useIvy := true) - CrossVersion.substituteCross via IvyActions
This commit is contained in:
Ali Rashid 2026-06-04 04:07:02 +03:00 committed by GitHub
parent e27f3df021
commit 3b097cc3cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 121 additions and 9 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. Keep in sync with
* `lmcoursier.FromSbt.addPlatformSuffix` (until lm-coursier moves under sbt).
*/
private[sbt] 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

@ -46,6 +46,8 @@ object FromSbt {
}
}
// Duplicate of sbt.librarymanagement.CrossVersion.addPlatformSuffix. Keep the two in sync
// until lm-coursier moves under sbt
private def addPlatformSuffix(
name: String,
platformOpt: Option[String],

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,52 @@
// sbt/sbt#9117: published artifact names must carry the platform suffix, matching the
// coordinate, on both publish backends. `platform` is set directly, as sbt-scala-native does.
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0"
ThisBuild / scalaVersion := "3.8.3"
ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache"
lazy val mavenRepo = settingKey[File]("shared local Maven repo for the consume round-trip")
ThisBuild / mavenRepo := (ThisBuild / baseDirectory).value / "maven-repo"
def expected(name: String) = s"${name}_native0.5_3"
def producer(useIvyFlag: Boolean): Seq[Setting[?]] = Seq(
platform := "native0.5",
crossVersion := CrossVersion.binary,
useIvy := useIvyFlag,
publishMavenStyle := true,
ivyPaths := IvyPaths(baseDirectory.value.toString, Some((target.value / "ivy2").toString)),
publishTo := Some(MavenCache("platform-publish-local", (ThisBuild / mavenRepo).value))
)
// checkPomArtifactId only for ivyless: there the artifactId comes from PomGenerator (and
// resolution does not validate POM content); the Ivy backend's is correct regardless.
def ivyLayoutCheck(base: String, checkPomArtifactId: Boolean): Setting[?] =
TaskKey[Unit]("check") := {
val nm = expected(base)
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)
if (checkPomArtifactId)
assert(IO.read(pom).contains(s"<artifactId>$nm</artifactId>"), s"POM artifactId must be $nm: ${IO.read(pom)}")
}
lazy val ivyless = (project in file("ivyless"))
.settings(name := "libivyless", producer(false), ivyLayoutCheck("libivyless", checkPomArtifactId = true))
lazy val ivyfull = (project in file("ivyfull"))
.settings(name := "libivyfull", producer(true), ivyLayoutCheck("libivyfull", checkPomArtifactId = false))
// Must not dependsOn the producers, so the coordinates resolve from the Maven repo rather
// than inter-project - otherwise a suffix-dropped published name would not be caught.
lazy val consumer = (project in file("consumer"))
.settings(
publish / skip := true,
resolvers += MavenCache("platform-publish-local", (ThisBuild / mavenRepo).value),
libraryDependencies += organization.value % expected("libivyless") % version.value,
libraryDependencies += organization.value % expected("libivyfull") % version.value
)

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,14 @@
# sbt/sbt#9117: published artifact names must carry the platform suffix (_native0.5),
# matching the coordinate, on both the ivyless and Ivy backends.
# publishLocal (ivy layout): filenames + POM artifactId
> ivyless/publishLocal
> ivyless/check
> ivyfull/publishLocal
> ivyfull/check
# publish to a local Maven repo, then resolve the artifacts back under their platform
# coordinate
> ivyless/publish
> ivyfull/publish
> consumer/update