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.
This commit is contained in:
Eugene Yokota 2025-11-16 17:44:59 -05:00
parent 2459663387
commit d0ae314748
5 changed files with 52 additions and 13 deletions

View File

@ -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 {

View File

@ -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.

View File

@ -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"))

View File

@ -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(

View File

@ -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)