From 3b097cc3cc91449b52928e50bb64edadd0bc627b Mon Sep 17 00:00:00 2001 From: Ali Rashid Date: Thu, 4 Jun 2026 04:07:02 +0300 Subject: [PATCH] [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 (the coordinate written inside the .pom) - PomGenerator.crossVersionDep - Ivy backend (useIvy := true) - CrossVersion.substituteCross via IvyActions --- .../sbt/librarymanagement/ArtifactExtra.scala | 6 +-- .../librarymanagement/CrossVersionExtra.scala | 14 +++++ .../src/main/scala/lmcoursier/FromSbt.scala | 2 + .../librarymanagement/IvyActions.scala | 14 ++++- .../coursierint/CoursierArtifactsTasks.scala | 8 ++- .../scala/sbt/internal/PomGenerator.scala | 7 ++- .../platform-publish/build.sbt | 52 +++++++++++++++++++ .../ivyfull/src/main/scala/Lib.scala | 4 ++ .../ivyless/src/main/scala/Lib.scala | 4 ++ .../platform-publish/project/plugins.sbt | 5 ++ .../platform-publish/test | 14 +++++ 11 files changed, 121 insertions(+), 9 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..aafc4a1c2 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. 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, diff --git a/lm-coursier/src/main/scala/lmcoursier/FromSbt.scala b/lm-coursier/src/main/scala/lmcoursier/FromSbt.scala index 7ec1fa920..0516dcd32 100644 --- a/lm-coursier/src/main/scala/lmcoursier/FromSbt.scala +++ b/lm-coursier/src/main/scala/lmcoursier/FromSbt.scala @@ -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], 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..379a3fe74 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/platform-publish/build.sbt @@ -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"$nm"), 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 + ) 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..bd3213bee --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/platform-publish/test @@ -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