mirror of https://github.com/sbt/sbt.git
[2.x] feat: Add "3-latest.candidate" support for Scala 3 release candidates (#8596)
Add support for `"3-latest.candidate"` to automatically resolve to the latest Scala 3 RC from Maven Central. ```scala scalaVersion := "3-latest.candidate" ``` Ref https://github.com/sbt/sbt/discussions/8590
This commit is contained in:
parent
642be58d44
commit
1b8e3317f9
|
|
@ -31,6 +31,8 @@ object CrossVersionUtil {
|
|||
raw"""$basicVersion((?:-$tagPattern)*)""".r // 0-n word suffixes, with leading dashes
|
||||
private val NonReleaseV_1 = raw"""$basicVersion(-$tagPattern)""".r // 1 word suffix, after a dash
|
||||
private[sbt] val PartialVersion = raw"""($longPattern)\.($longPattern)(?:\..+)?""".r
|
||||
// Dynamic Scala version patterns like "3-latest.candidate"
|
||||
private val DynamicScala3V = raw"""($longPattern)-latest\..*""".r
|
||||
|
||||
private[sbt] def isSbtApiCompatible(v: String): Boolean = sbtApiVersion(v).isDefined
|
||||
|
||||
|
|
@ -113,9 +115,15 @@ object CrossVersionUtil {
|
|||
}
|
||||
|
||||
def binaryScalaVersion(full: String): String =
|
||||
if full.startsWith("2.") then
|
||||
binaryVersionWithApi(full, TransitionScalaVersion)(scalaApiVersion) // Scala 2 binary version
|
||||
else binaryScala3Version(full)
|
||||
full match {
|
||||
// Handle dynamic Scala 3 version patterns like "3-latest.candidate"
|
||||
case DynamicScala3V(maj) => maj
|
||||
case _ if full.startsWith("2.") =>
|
||||
binaryVersionWithApi(full, TransitionScalaVersion)(
|
||||
scalaApiVersion
|
||||
) // Scala 2 binary version
|
||||
case _ => binaryScala3Version(full)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the binary version of the Scala, except for
|
||||
|
|
@ -124,14 +132,18 @@ object CrossVersionUtil {
|
|||
* 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
|
||||
full match {
|
||||
// Handle dynamic Scala 3 version patterns like "3-latest.candidate"
|
||||
case DynamicScala3V(maj) => maj
|
||||
case _ if full.startsWith("2.") =>
|
||||
partialVersion(full) match
|
||||
case Some((major, minor)) => s"$major.$minor"
|
||||
case None => full
|
||||
case _ =>
|
||||
partialVersion(full) match
|
||||
case Some((major, minor)) => major.toString
|
||||
case None => full
|
||||
}
|
||||
|
||||
def binarySbtVersion(full: String): String =
|
||||
sbtApiVersion(full) match {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ object ScalaArtifacts {
|
|||
name.startsWith(Scala3ReplPrefix)
|
||||
}
|
||||
|
||||
def isScala3(scalaVersion: String): Boolean = scalaVersion.startsWith("3.")
|
||||
def isScala3(scalaVersion: String): Boolean =
|
||||
scalaVersion.startsWith("3.") || scalaVersion.startsWith("3-latest.")
|
||||
|
||||
/**
|
||||
* Returns true for pre-release nightlies intentionally.
|
||||
|
|
|
|||
|
|
@ -264,6 +264,13 @@ class CrossVersionTest extends UnitSpec {
|
|||
it should "for 4.0.0-M2 return 4.0.0-M2" in {
|
||||
binaryScalaVersion("4.0.0-M2") shouldBe "4.0.0-M2"
|
||||
}
|
||||
// Dynamic Scala version patterns
|
||||
it should "for 3-latest.candidate return 3" in {
|
||||
binaryScalaVersion("3-latest.candidate") shouldBe "3"
|
||||
}
|
||||
it should "for 4-latest.candidate return 4" in {
|
||||
binaryScalaVersion("4-latest.candidate") shouldBe "4"
|
||||
}
|
||||
|
||||
"earlyScalaVersion" should "for 2.9.2 return 2.9" in {
|
||||
assert(earlyScalaVersion("2.9.2") == "2.9")
|
||||
|
|
@ -277,6 +284,13 @@ class CrossVersionTest extends UnitSpec {
|
|||
it should "for 4.0.0-M1 return 4" in {
|
||||
assert(earlyScalaVersion("4.0.0-M1") == "4")
|
||||
}
|
||||
// Dynamic Scala version patterns
|
||||
it should "for 3-latest.candidate return 3" in {
|
||||
assert(earlyScalaVersion("3-latest.candidate") == "3")
|
||||
}
|
||||
it should "for 4-latest.candidate return 4" in {
|
||||
assert(earlyScalaVersion("4-latest.candidate") == "4")
|
||||
}
|
||||
|
||||
private def patchVersion(fullVersion: String) =
|
||||
CrossVersion(CrossVersion.patch, fullVersion, "dummy") map (fn => fn("artefact"))
|
||||
|
|
|
|||
|
|
@ -734,7 +734,8 @@ object Defaults extends BuildCommon {
|
|||
val hasSbtBridge = ScalaArtifacts.isScala3(sv) || ZincLmUtil.hasScala2SbtBridge(sv)
|
||||
hasSbtBridge && managed
|
||||
})(Def.cachedTask {
|
||||
val sv = scalaVersion.value
|
||||
// Use scalaDynVersion to resolve dynamic versions (e.g., "3-latest.candidate" -> "3.8.1-RC1")
|
||||
val sv = scalaDynVersion.value
|
||||
val conv = fileConverter.value
|
||||
val s = streams.value
|
||||
val t = target.value
|
||||
|
|
@ -774,6 +775,13 @@ object Defaults extends BuildCommon {
|
|||
javacOptions :== Nil,
|
||||
scalacOptions :== Nil,
|
||||
scalaVersion := appConfiguration.value.provider.scalaProvider.version,
|
||||
derive(
|
||||
scalaDynVersion := {
|
||||
val sv = scalaVersion.value
|
||||
val log = streams.value.log
|
||||
LibraryManagement.resolveDynamicScalaVersion(sv, log)
|
||||
}
|
||||
),
|
||||
consoleProject := ConsoleProject.consoleProjectTask.value,
|
||||
consoleProject / scalaInstance := {
|
||||
val topLoader = classOf[org.jline.terminal.Terminal].getClassLoader
|
||||
|
|
@ -3119,9 +3127,12 @@ object Classpaths {
|
|||
allExcludeDependencies := excludeDependencies.value,
|
||||
scalaModuleInfo := (scalaModuleInfo or (
|
||||
Def.setting {
|
||||
// Resolve dynamic Scala version for scalaModuleInfo
|
||||
val resolvedScalaVersion =
|
||||
LibraryManagement.resolveDynamicScalaVersion((update / scalaVersion).value)
|
||||
Option(
|
||||
ScalaModuleInfo(
|
||||
(update / scalaVersion).value,
|
||||
resolvedScalaVersion,
|
||||
(update / scalaBinaryVersion).value,
|
||||
Vector.empty,
|
||||
filterImplicit = false,
|
||||
|
|
@ -3385,7 +3396,8 @@ object Classpaths {
|
|||
autoScalaLibrary.value && scalaHome.value.isEmpty && managedScalaInstance.value,
|
||||
sbtPlugin.value,
|
||||
scalaOrganization.value,
|
||||
scalaVersion.value
|
||||
// Resolve dynamic Scala version (e.g., "3-latest.candidate" -> "3.8.1-RC1")
|
||||
LibraryManagement.resolveDynamicScalaVersion(scalaVersion.value)
|
||||
),
|
||||
// Override the default to handle mixing in the sbtPlugin + scala dependencies.
|
||||
allDependencies := Def.uncached {
|
||||
|
|
@ -3397,7 +3409,8 @@ object Classpaths {
|
|||
if (isPlugin) sbtdeps +: base
|
||||
else base
|
||||
val scalaOrg = scalaOrganization.value
|
||||
val version = scalaVersion.value
|
||||
// Resolve dynamic Scala version (e.g., "3-latest.candidate" -> "3.8.1-RC1")
|
||||
val version = LibraryManagement.resolveDynamicScalaVersion(scalaVersion.value)
|
||||
val extResolvers = externalResolvers.value
|
||||
val allToolDeps =
|
||||
if scalaHome.value.isDefined || scalaModuleInfo.value.isEmpty || !managedScalaInstance.value
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ object Keys {
|
|||
val scalaInstance = taskKey[ScalaInstance]("Defines the Scala instance to use for compilation, running, and testing.").withRank(DTask)
|
||||
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 scalaDynVersion = taskKey[String]("Resolves dynamic Scala version strings like '3-latest.candidate' to concrete versions.").withRank(DSetting)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -440,4 +440,107 @@ private[sbt] object LibraryManagement {
|
|||
|
||||
def lock(app: xsbti.AppConfiguration): xsbti.GlobalLock =
|
||||
app.provider.scalaProvider.launcher.globalLock
|
||||
|
||||
// Pattern for Scala 3 release candidates: X.Y.Z-RCN (not nightlies)
|
||||
private val Scala3RCPattern = """^(\d+)\.(\d+)\.(\d+)-RC(\d+)$""".r
|
||||
|
||||
// Cache for the latest Scala 3 RC version lookup (24-hour TTL)
|
||||
private var cachedScala3RC: Option[(String, Long)] = None
|
||||
private val scala3RCCacheLock = new Object
|
||||
private val cacheTtlMillis = 24L * 60 * 60 * 1000 // 24 hours
|
||||
|
||||
/**
|
||||
* Fetches the latest Scala 3 release candidate version from Maven Central.
|
||||
* Results are cached for 24 hours.
|
||||
*
|
||||
* @param log Logger for debug output
|
||||
* @return The latest Scala 3 RC version string (e.g., "3.8.1-RC1")
|
||||
*/
|
||||
def fetchLatestScala3RC(log: Logger): String = {
|
||||
scala3RCCacheLock.synchronized {
|
||||
val now = System.currentTimeMillis()
|
||||
cachedScala3RC match {
|
||||
case Some((version, timestamp)) if (now - timestamp) < cacheTtlMillis =>
|
||||
log.debug(s"Using cached Scala 3 RC version: $version")
|
||||
version
|
||||
case _ =>
|
||||
log.info("Fetching latest Scala 3 release candidate from Maven Central...")
|
||||
val version = fetchScala3RCFromMaven(log)
|
||||
cachedScala3RC = Some((version, now))
|
||||
version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def fetchScala3RCFromMaven(log: Logger): String = {
|
||||
import scala.util.Using
|
||||
import java.net.{ HttpURLConnection, URI }
|
||||
import scala.xml.XML
|
||||
|
||||
val metadataUrl =
|
||||
"https://repo1.maven.org/maven2/org/scala-lang/scala3-library_3/maven-metadata.xml"
|
||||
val uri = new URI(metadataUrl)
|
||||
val conn = uri.toURL.openConnection().asInstanceOf[HttpURLConnection]
|
||||
conn.setConnectTimeout(10000)
|
||||
conn.setReadTimeout(30000)
|
||||
|
||||
try {
|
||||
val xml = Using.resource(conn.getInputStream) { is =>
|
||||
XML.load(is)
|
||||
}
|
||||
val versions = (xml \\ "version").map(_.text)
|
||||
|
||||
// Filter for RC versions (not nightlies) and sort to get the latest
|
||||
val rcVersions = versions.flatMap { v =>
|
||||
v match {
|
||||
case Scala3RCPattern(major, minor, patch, rc) =>
|
||||
Some((major.toInt, minor.toInt, patch.toInt, rc.toInt, v))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
if (rcVersions.isEmpty) {
|
||||
sys.error("No Scala 3 release candidates found in Maven Central")
|
||||
}
|
||||
|
||||
// Sort by version components (major, minor, patch, rc) descending
|
||||
val latestRC =
|
||||
rcVersions.sortBy { case (maj, min, pat, rc, _) => (-maj, -min, -pat, -rc) }.head._5
|
||||
log.info(s"Latest Scala 3 release candidate: $latestRC")
|
||||
latestRC
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a dynamic Scala version string to a concrete version.
|
||||
* Currently supports:
|
||||
* - "3-latest.candidate" - resolves to the latest Scala 3 RC
|
||||
*
|
||||
* @param version The version string (may be dynamic or concrete)
|
||||
* @param log Logger for debug output
|
||||
* @return The resolved concrete version string
|
||||
*/
|
||||
def resolveDynamicScalaVersion(version: String, log: Logger): String = {
|
||||
version match {
|
||||
case "3-latest.candidate" => fetchLatestScala3RC(log)
|
||||
case other => other
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a dynamic Scala version string to a concrete version.
|
||||
* Uses a silent logger for background resolution.
|
||||
*/
|
||||
def resolveDynamicScalaVersion(version: String): String = {
|
||||
resolveDynamicScalaVersion(version, Logger.Null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version string is a dynamic version that needs resolution.
|
||||
*/
|
||||
def isDynamicScalaVersion(version: String): Boolean = {
|
||||
version == "3-latest.candidate"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
ThisBuild / scalaVersion := "3-latest.candidate"
|
||||
|
||||
lazy val checkDynVersion = taskKey[Unit]("Check that scalaDynVersion resolves correctly")
|
||||
|
||||
lazy val p1 = project
|
||||
.settings(
|
||||
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.6.3"
|
||||
)
|
||||
|
||||
lazy val p2 = project
|
||||
.dependsOn(p1)
|
||||
.settings(
|
||||
checkDynVersion := {
|
||||
val log = streams.value.log
|
||||
val sv = scalaVersion.value
|
||||
val dynSv = scalaDynVersion.value
|
||||
val binSv = scalaBinaryVersion.value
|
||||
|
||||
log.info(s"scalaVersion: $sv")
|
||||
log.info(s"scalaDynVersion: $dynSv")
|
||||
log.info(s"scalaBinaryVersion: $binSv")
|
||||
|
||||
// scalaVersion should be the dynamic pattern
|
||||
assert(sv == "3-latest.candidate", s"Expected scalaVersion '3-latest.candidate', got '$sv'")
|
||||
|
||||
// scalaDynVersion should resolve to a concrete RC version (e.g., "3.8.1-RC1")
|
||||
assert(dynSv.matches("""\d+\.\d+\.\d+-RC\d+"""), s"Expected scalaDynVersion to be a concrete RC version, got '$dynSv'")
|
||||
|
||||
// scalaBinaryVersion should be "3"
|
||||
assert(binSv == "3", s"Expected scalaBinaryVersion '3', got '$binSv'")
|
||||
|
||||
log.success("All checks passed!")
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package example
|
||||
|
||||
import cats.effect.IO
|
||||
|
||||
object Example:
|
||||
def hello: IO[String] = IO.pure("Hello from Scala 3 RC!")
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
> p2/checkDynVersion
|
||||
> p1/compile
|
||||
Loading…
Reference in New Issue