From 88844881469dacb6b09bfdeb262e13eeabc1495d Mon Sep 17 00:00:00 2001 From: Ali Rashid Date: Tue, 2 Jun 2026 00:01:38 +0300 Subject: [PATCH] [2.x] fix: Ensure correct platform suffix in published artifact names --- .../sbt/librarymanagement/ArtifactExtra.scala | 6 +-- .../librarymanagement/CrossVersionExtra.scala | 14 +++++ .../src/main/scala/lmcoursier/FromSbt.scala | 21 +------- .../librarymanagement/IvyActions.scala | 14 ++++- .../coursierint/CoursierArtifactsTasks.scala | 8 ++- .../scala/sbt/internal/PomGenerator.scala | 7 ++- .../platform-publish/build.sbt | 51 +++++++++++++++++++ .../ivyfull/src/main/scala/Lib.scala | 4 ++ .../ivyless/src/main/scala/Lib.scala | 4 ++ .../platform-publish/project/plugins.sbt | 5 ++ .../platform-publish/test | 11 ++++ 11 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 sbt-app/src/sbt-test/dependency-management/platform-publish/build.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/platform-publish/ivyfull/src/main/scala/Lib.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/platform-publish/ivyless/src/main/scala/Lib.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/platform-publish/project/plugins.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/platform-publish/test diff --git a/lm-core/src/main/scala/sbt/librarymanagement/ArtifactExtra.scala b/lm-core/src/main/scala/sbt/librarymanagement/ArtifactExtra.scala index 8aa062237..9614d6ba4 100644 --- a/lm-core/src/main/scala/sbt/librarymanagement/ArtifactExtra.scala +++ b/lm-core/src/main/scala/sbt/librarymanagement/ArtifactExtra.scala @@ -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 diff --git a/lm-core/src/main/scala/sbt/librarymanagement/CrossVersionExtra.scala b/lm-core/src/main/scala/sbt/librarymanagement/CrossVersionExtra.scala index c9d3d5db3..45a67d52a 100644 --- a/lm-core/src/main/scala/sbt/librarymanagement/CrossVersionExtra.scala +++ b/lm-core/src/main/scala/sbt/librarymanagement/CrossVersionExtra.scala @@ -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, diff --git a/lm-coursier/src/main/scala/lmcoursier/FromSbt.scala b/lm-coursier/src/main/scala/lmcoursier/FromSbt.scala index 7ec1fa920..b02f4c3ad 100644 --- a/lm-coursier/src/main/scala/lmcoursier/FromSbt.scala +++ b/lm-coursier/src/main/scala/lmcoursier/FromSbt.scala @@ -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) => diff --git a/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala b/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala index 0048a79eb..bf726718c 100644 --- a/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala +++ b/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala @@ -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, diff --git a/main/src/main/scala/sbt/coursierint/CoursierArtifactsTasks.scala b/main/src/main/scala/sbt/coursierint/CoursierArtifactsTasks.scala index f36594add..a7b5a9064 100644 --- a/main/src/main/scala/sbt/coursierint/CoursierArtifactsTasks.scala +++ b/main/src/main/scala/sbt/coursierint/CoursierArtifactsTasks.scala @@ -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, diff --git a/main/src/main/scala/sbt/internal/PomGenerator.scala b/main/src/main/scala/sbt/internal/PomGenerator.scala index 5fc7c1ed8..5744194c0 100644 --- a/main/src/main/scala/sbt/internal/PomGenerator.scala +++ b/main/src/main/scala/sbt/internal/PomGenerator.scala @@ -61,9 +61,14 @@ private[sbt] object PomGenerator: 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 diff --git a/sbt-app/src/sbt-test/dependency-management/platform-publish/build.sbt b/sbt-app/src/sbt-test/dependency-management/platform-publish/build.sbt new file mode 100644 index 000000000..daad5f6fb --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/platform-publish/build.sbt @@ -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"$nm"), 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") + } + ) diff --git a/sbt-app/src/sbt-test/dependency-management/platform-publish/ivyfull/src/main/scala/Lib.scala b/sbt-app/src/sbt-test/dependency-management/platform-publish/ivyfull/src/main/scala/Lib.scala new file mode 100644 index 000000000..4da7a9970 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/platform-publish/ivyfull/src/main/scala/Lib.scala @@ -0,0 +1,4 @@ +package lib + +object Lib: + def greeting: String = "hi" diff --git a/sbt-app/src/sbt-test/dependency-management/platform-publish/ivyless/src/main/scala/Lib.scala b/sbt-app/src/sbt-test/dependency-management/platform-publish/ivyless/src/main/scala/Lib.scala new file mode 100644 index 000000000..4da7a9970 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/platform-publish/ivyless/src/main/scala/Lib.scala @@ -0,0 +1,4 @@ +package lib + +object Lib: + def greeting: String = "hi" diff --git a/sbt-app/src/sbt-test/dependency-management/platform-publish/project/plugins.sbt b/sbt-app/src/sbt-test/dependency-management/platform-publish/project/plugins.sbt new file mode 100644 index 000000000..0a6624c35 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/platform-publish/project/plugins.sbt @@ -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() +} diff --git a/sbt-app/src/sbt-test/dependency-management/platform-publish/test b/sbt-app/src/sbt-test/dependency-management/platform-publish/test new file mode 100644 index 000000000..835426e8d --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/platform-publish/test @@ -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