From d0ae314748bd23f0b2d2d9cffdef56448e0ee500 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 16 Nov 2025 17:44:59 -0500 Subject: [PATCH] scalaEarlyVersion setting and source directories **Problem** During the milestone releases of Scala, e.g. Scala 2.13.0-M1, scalaBinaryVersion by design points to 2.13.0-M1, and also the source directory uses scala-2.13.0-M1, but in most cases we actually want to pretend compatibility and use scala-2.13 directory. **Solution** This introduces a new setting called scalaEarlyVersion, which is the scalaBinaryVersion of the release version of milestones. We have been calling this "partial version", but that broke down for Scala 3, which adopted semantic versioning. --- .../cross/CrossVersionUtil.scala | 25 ++++++++++++++----- .../librarymanagement/CrossVersionExtra.scala | 8 ++++++ .../librarymanagement/CrossVersionTest.scala | 19 +++++++++++--- main/src/main/scala/sbt/Defaults.scala | 12 ++++++--- main/src/main/scala/sbt/Keys.scala | 1 + 5 files changed, 52 insertions(+), 13 deletions(-) diff --git a/lm-core/src/main/scala/sbt/internal/librarymanagement/cross/CrossVersionUtil.scala b/lm-core/src/main/scala/sbt/internal/librarymanagement/cross/CrossVersionUtil.scala index 50fcfd632..b5142754f 100644 --- a/lm-core/src/main/scala/sbt/internal/librarymanagement/cross/CrossVersionUtil.scala +++ b/lm-core/src/main/scala/sbt/internal/librarymanagement/cross/CrossVersionUtil.scala @@ -1,8 +1,6 @@ package sbt.internal.librarymanagement package cross -import sbt.librarymanagement.ScalaArtifacts - object CrossVersionUtil { val trueString = "true" val falseString = "false" @@ -111,11 +109,26 @@ object CrossVersionUtil { } } - def binaryScalaVersion(full: String): String = { - if (ScalaArtifacts.isScala3(full)) binaryScala3Version(full) - else + def binaryScalaVersion(full: String): String = + if full.startsWith("2.") then binaryVersionWithApi(full, TransitionScalaVersion)(scalaApiVersion) // Scala 2 binary version - } + else binaryScala3Version(full) + + /** + * Returns the binary version of the Scala, except for + * prereleases version, returns the binary version of the release equivalent. + * This was called the partial version in Scala 2.x. + * In Scala 3 onwards, it would be the major version. + */ + def earlyScalaVersion(full: String): String = + if full.startsWith("2.") then + partialVersion(full) match + case Some((major, minor)) => s"$major.$minor" + case None => full + else + partialVersion(full) match + case Some((major, minor)) => major.toString + case None => full def binarySbtVersion(full: String): String = sbtApiVersion(full) match { diff --git a/lm-core/src/main/scala/sbt/librarymanagement/CrossVersionExtra.scala b/lm-core/src/main/scala/sbt/librarymanagement/CrossVersionExtra.scala index 56c30b67c..6b1035ada 100644 --- a/lm-core/src/main/scala/sbt/librarymanagement/CrossVersionExtra.scala +++ b/lm-core/src/main/scala/sbt/librarymanagement/CrossVersionExtra.scala @@ -224,6 +224,14 @@ private[librarymanagement] abstract class CrossVersionFunctions { /** Extracts the major and minor components of a version string `s` or returns `None` if the version is improperly formatted. */ def partialVersion(s: String): Option[(Long, Long)] = CrossVersionUtil.partialVersion(s) + /** + * Returns the binary version of the Scala, except for + * prereleases version, returns the binary version of the release equivalent. + * This was called the partial version in Scala 2.x. + * In Scala 3 onwards, it would be the major version. + */ + def earlyScalaVersion(full: String): String = CrossVersionUtil.earlyScalaVersion(full) + /** * Computes the binary Scala version from the `full` version. * Full Scala versions earlier than [[sbt.librarymanagement.CrossVersion.TransitionScalaVersion]] are returned as is. diff --git a/lm-core/src/test/scala/sbt/librarymanagement/CrossVersionTest.scala b/lm-core/src/test/scala/sbt/librarymanagement/CrossVersionTest.scala index 374982858..57c8e2ddc 100644 --- a/lm-core/src/test/scala/sbt/librarymanagement/CrossVersionTest.scala +++ b/lm-core/src/test/scala/sbt/librarymanagement/CrossVersionTest.scala @@ -231,9 +231,6 @@ class CrossVersionTest extends UnitSpec { it should "for 3.0.0-RC1 return 3.0.0-RC1" in { binaryScalaVersion("3.0.0-RC1") shouldBe "3.0.0-RC1" } - - // Not set in stone but 3 is the favorite candidate so far - // (see https://github.com/lampepfl/dotty/issues/10244) it should "for 3.0.0 return 3" in { binaryScalaVersion("3.0.0") shouldBe "3" } @@ -264,6 +261,22 @@ class CrossVersionTest extends UnitSpec { it should "for 3.0.1-SNAPSHOT return 3" in { binaryScalaVersion("3.0.1-SNAPSHOT") shouldBe "3" } + it should "for 4.0.0-M2 return 4.0.0-M2" in { + binaryScalaVersion("4.0.0-M2") shouldBe "4.0.0-M2" + } + + "earlyScalaVersion" should "for 2.9.2 return 2.9" in { + assert(earlyScalaVersion("2.9.2") == "2.9") + } + it should "for 2.13.0-M1 return 2.13" in { + assert(earlyScalaVersion("2.13.0-M1") == "2.13") + } + it should "for 3.0.0-M1 return 3" in { + assert(earlyScalaVersion("3.0.0-M1") == "3") + } + it should "for 4.0.0-M1 return 4" in { + assert(earlyScalaVersion("4.0.0-M1") == "4") + } private def patchVersion(fullVersion: String) = CrossVersion(CrossVersion.patch, fullVersion, "dummy") map (fn => fn("artefact")) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index d6ac7c1aa..cbff21afb 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -566,12 +566,14 @@ object Defaults extends BuildCommon { scalaSource := sourceDirectory.value / "scala", javaSource := sourceDirectory.value / "java", unmanagedSourceDirectories := { - val isDotty = ScalaInstance.isDotty(scalaVersion.value) - val epochVersion = if (isDotty) "3" else "2" + val early = scalaEarlyVersion.value + val epochVersion = + if scalaVersion.value.startsWith("2.") then "2" + else early makeCrossSources( scalaSource.value, javaSource.value, - scalaBinaryVersion.value, + early, epochVersion, crossPaths.value ) ++ @@ -762,7 +764,8 @@ object Defaults extends BuildCommon { scalaVersion := appConfiguration.value.provider.scalaProvider.version, derive(crossScalaVersions := Seq(scalaVersion.value)), derive(compilersSetting), - derive(scalaBinaryVersion := binaryScalaVersion(scalaVersion.value)) + derive(scalaBinaryVersion := binaryScalaVersion(scalaVersion.value)), + derive(scalaEarlyVersion := CrossVersion.earlyScalaVersion(scalaVersion.value)), ) ) @@ -3490,6 +3493,7 @@ object Classpaths { // to fix https://github.com/sbt/sbt/issues/2686 scalaVersion := appConfiguration.value.provider.scalaProvider.version, scalaBinaryVersion := binaryScalaVersion(scalaVersion.value), + scalaEarlyVersion := CrossVersion.earlyScalaVersion(scalaVersion.value), scalaOrganization := ScalaArtifacts.Organization, scalaModuleInfo := { Some( diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index ac421f4ee..94678796e 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -217,6 +217,7 @@ object Keys { val scalaOrganization = settingKey[String]("Organization/group ID of the Scala used in the project. Default value is 'org.scala-lang'. This is an advanced setting used for clones of the Scala Language. It should be disregarded in standard use cases.").withRank(CSetting) val scalaVersion = settingKey[String]("The version of Scala used for building.").withRank(APlusSetting) val scalaBinaryVersion = settingKey[String]("The Scala version substring describing binary compatibility.").withRank(BPlusSetting) + val scalaEarlyVersion = settingKey[String]("The Scala version substring describing the binary compatibility, except for prereleases it returns the binary version of the release.").withRank(DSetting) val crossScalaVersions = settingKey[Seq[String]]("The versions of Scala used when cross-building.").withRank(BPlusSetting) val crossVersion = settingKey[CrossVersion]("Configures handling of the Scala version when cross-building.").withRank(CSetting) val platform = settingKey[String]("Configures the default suffix to be used for %% operator.").withRank(CSetting)