From 874f680a14cf0bb5985d8e32d9040745ae31104c Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Mon, 28 Apr 2025 12:14:13 +0200 Subject: [PATCH 1/4] Update `runner` & `test-runner` to Scala 3.3 LTS --- project/deps.sc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/project/deps.sc b/project/deps.sc index 78d5c22135..eff63ff19b 100644 --- a/project/deps.sc +++ b/project/deps.sc @@ -3,11 +3,11 @@ import mill._ import scalalib._ object Scala { - def scala212 = "2.12.20" - def scala213 = "2.13.16" - def runnerScala3 = "3.0.2" // the newest version that is compatible with all Scala 3.x versions - def scala3LtsPrefix = "3.3" // used for the LTS version tags + def scala212 = "2.12.20" + def scala213 = "2.13.16" + def scala3LtsPrefix = "3.3" // used for the LTS version tags def scala3Lts = s"$scala3LtsPrefix.5" // the LTS version currently used in the build + def runnerScala3 = scala3Lts def scala3NextPrefix = "3.7" def scala3Next = s"$scala3NextPrefix.0" // the newest/next version of Scala def scala3NextAnnounced = "3.6.4" // the newest/next version of Scala that's been announced @@ -27,7 +27,7 @@ object Scala { val scala3MainVersions = (defaults ++ allScala3).distinct val mainVersions = (Seq(scala213) ++ scala3MainVersions).distinct val runnerScalaVersions = runnerScala3 +: allScala2 - val testRunnerScalaVersions = runnerScalaVersions ++ allScala3 + val testRunnerScalaVersions = (runnerScalaVersions ++ allScala3).distinct def scalaJs = "1.19.0" def scalaJsCli = scalaJs // this must be compatible with the Scala.js version From 3a1907eb95eb3bde14300eec030702124527350f Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Wed, 30 Apr 2025 13:30:10 +0200 Subject: [PATCH 2/4] Move `coursier` utils to the `core` module --- .../src/main/scala/scala/cli/commands/fmt/FmtOptions.scala | 2 +- .../cli/src/main/scala/scala/cli/commands/repl/Repl.scala | 2 +- modules/cli/src/main/scala/scala/cli/package.scala | 7 ------- modules/core/src/main/scala/scala/build/CsUtils.scala | 5 +++++ 4 files changed, 7 insertions(+), 9 deletions(-) delete mode 100644 modules/cli/src/main/scala/scala/cli/package.scala create mode 100644 modules/core/src/main/scala/scala/build/CsUtils.scala diff --git a/modules/cli/src/main/scala/scala/cli/commands/fmt/FmtOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/fmt/FmtOptions.scala index 4045d4df3a..979fe73a65 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/fmt/FmtOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/fmt/FmtOptions.scala @@ -4,6 +4,7 @@ import caseapp.* import coursier.core.Version import scala.build.EitherCps.{either, value} +import scala.build.coursierVersion import scala.build.errors.BuildException import scala.build.internal.FetchExternalBinary import scala.build.options.BuildOptions @@ -16,7 +17,6 @@ import scala.cli.commands.shared.{ SharedOptions } import scala.cli.commands.{Constants, tags} -import scala.cli.coursierVersion import scala.util.Properties // format: off diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala index 0a1e97f56e..0d9fd788a1 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala @@ -37,7 +37,7 @@ import scala.cli.config.{ConfigDb, Keys} import scala.cli.packaging.Library import scala.cli.util.ArgHelpers.* import scala.cli.util.ConfigDbUtils -import scala.cli.{CurrentParams, ScalaCli, coursierVersion} +import scala.cli.{CurrentParams, ScalaCli} import scala.jdk.CollectionConverters.* import scala.util.Properties diff --git a/modules/cli/src/main/scala/scala/cli/package.scala b/modules/cli/src/main/scala/scala/cli/package.scala deleted file mode 100644 index 96f0dc2a1c..0000000000 --- a/modules/cli/src/main/scala/scala/cli/package.scala +++ /dev/null @@ -1,7 +0,0 @@ -package scala - -import coursier.core.Version - -package object cli { - extension (s: String) def coursierVersion: Version = Version(s) -} diff --git a/modules/core/src/main/scala/scala/build/CsUtils.scala b/modules/core/src/main/scala/scala/build/CsUtils.scala new file mode 100644 index 0000000000..b447b8b2fe --- /dev/null +++ b/modules/core/src/main/scala/scala/build/CsUtils.scala @@ -0,0 +1,5 @@ +package scala.build + +import coursier.core.Version + +extension (s: String) def coursierVersion: Version = Version(s) From 9dbd7ffb9a97e47b15834696002d8e9131ee2e05 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Wed, 30 Apr 2025 16:52:18 +0200 Subject: [PATCH 3/4] Downgrade the `runner` module dependency when it's added to the classpath for Scala >= 3.0.0 and < current Scala 3 LTS version --- build.sc | 7 ++++- .../scala/scala/cli/commands/run/Run.scala | 2 ++ .../cli/integration/RunTestDefinitions.scala | 14 ++++++++++ .../cli/integration/RunTestsDefault.scala | 19 +++++++++++++ .../main/scala/scala/build/Artifacts.scala | 27 ++++++++++++++++--- project/deps.sc | 18 +++++++++++-- 6 files changed, 80 insertions(+), 7 deletions(-) diff --git a/build.sc b/build.sc index 5f387e1065..56c946b159 100644 --- a/build.sc +++ b/build.sc @@ -1,7 +1,7 @@ import $ivy.`com.lihaoyi::mill-contrib-bloop:$MILL_VERSION` import $ivy.`io.get-coursier::coursier-launcher:2.1.24` import $ivy.`io.github.alexarchambault.mill::mill-native-image-upload:0.1.29` -import $file.project.deps, deps.{Deps, Docker, InternalDeps, Java, Scala, TestDeps} +import $file.project.deps, deps.{Cli, Deps, Docker, InternalDeps, Java, Scala, TestDeps} import $file.project.publish, publish.{ghOrg, ghName, ScalaCliPublishModule, organization} import $file.project.settings, settings.{ CliLaunchers, @@ -454,6 +454,7 @@ trait Core extends ScalaCliCrossSbtModule | def runnerOrganization = "${runner(Scala.runnerScala3).pomSettings().organization}" | def runnerModuleName = "${runner(Scala.runnerScala3).artifactName()}" | def runnerVersion = "${runner(Scala.runnerScala3).publishVersion()}" + | def runnerLegacyVersion = "${Cli.runnerLegacyVersion}" | def runnerMainClass = "$runnerMainClass" | | def semanticDbPluginOrganization = "${Deps.semanticDbScalac.dep.module.organization @@ -1060,6 +1061,9 @@ trait CliIntegration extends SbtModule with ScalaCliPublishModule with HasTests | def maxAmmoniteScala213Version = "${Scala.maxAmmoniteScala213Version}" | def maxAmmoniteScala3Version = "${Scala.maxAmmoniteScala3Version}" | def maxAmmoniteScala3LtsVersion = "${Scala.maxAmmoniteScala3LtsVersion}" + | def legacyScala3Versions = Seq(${Scala.legacyScala3Versions.map(p => + s"\"$p\"" + ).mkString(", ")}) | def scalaJsVersion = "${Scala.scalaJs}" | def scalaJsCliVersion = "${Scala.scalaJsCli}" | def scalaNativeVersion = "${Deps.Versions.scalaNative}" @@ -1069,6 +1073,7 @@ trait CliIntegration extends SbtModule with ScalaCliPublishModule with HasTests | def ammoniteVersion = "${Deps.ammonite.dep.version}" | def defaultGraalVMJavaVersion = "${deps.graalVmJavaVersion}" | def defaultGraalVMVersion = "${deps.graalVmVersion}" + | def runnerLegacyVersion = "${Cli.runnerLegacyVersion}" | def scalaPyVersion = "${Deps.scalaPy.dep.version}" | def scalaPyMaxScalaNative = "${Deps.Versions.maxScalaNativeForScalaPy}" | def bloopVersion = "${Deps.bloopRifle.dep.version}" diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index 497d9763dd..2a18e2c726 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -16,6 +16,7 @@ import scala.build.errors.{BuildException, CompositeBuildException} import scala.build.input.{Inputs, ScalaCliInvokeData, SubCommand} import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig} import scala.build.internals.ConsoleUtils.ScalaCliConsole +import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.build.internals.EnvVar import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, ScalacOpt, Scope} import scala.cli.CurrentParams @@ -103,6 +104,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { addRunnerDependencyOpt = baseOptions.notForBloopOptions.addRunnerDependencyOpt.orElse { runMode(options) match { case _: RunMode.Spark | RunMode.HadoopJar => + logger.debug(s"$warnPrefix Skipping the runner dependency when running Spark/Hadoop.") Some(false) case RunMode.Default => None } diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index 0ccfe39612..2b28052df6 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -2400,4 +2400,18 @@ abstract class RunTestDefinitions expect(res.out.trim() == expectedOutput) } } + + test( + s"run a simple hello world with the runner module on the classpath and Scala $actualScalaVersion" + ) { + val expectedMessage = "Hello, world!" + val legacyRunnerWarning = "Defaulting to a legacy runner module version" + TestInputs(os.rel / "script.sc" -> s"""println("$expectedMessage")""") + .fromRoot { root => + val res = os.proc(TestUtil.cli, "run", ".", "--runner", extraOptions) + .call(cwd = root, stderr = os.Pipe) + expect(res.out.trim() == expectedMessage) + expect(!res.err.trim().contains(legacyRunnerWarning)) + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala index 62330214d2..7c8b29eb99 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala @@ -187,4 +187,23 @@ class RunTestsDefault extends RunTestDefinitions } } } + + for { + scalaVersion <- + Constants.legacyScala3Versions.sorted.reverse.distinctBy(_.split('.').take(2).mkString(".")) + expectedMessage = "Hello, world!" + expectedWarning = + s"Defaulting to a legacy runner module version: ${Constants.runnerLegacyVersion}" + } + test( + s"run a simple hello world with the runner module on the classpath and Scala $scalaVersion (legacy)" + ) { + TestInputs(os.rel / "script.sc" -> s"""println("$expectedMessage")""").fromRoot { root => + val res = + os.proc(TestUtil.cli, "run", ".", "-S", scalaVersion, TestUtil.extraOptions, "--runner") + .call(cwd = root, stderr = os.Pipe) + expect(res.out.trim() == expectedMessage) + expect(res.err.trim().contains(expectedWarning)) + } + } } diff --git a/modules/options/src/main/scala/scala/build/Artifacts.scala b/modules/options/src/main/scala/scala/build/Artifacts.scala index 23451ad8c6..2078f4219b 100644 --- a/modules/options/src/main/scala/scala/build/Artifacts.scala +++ b/modules/options/src/main/scala/scala/build/Artifacts.scala @@ -23,6 +23,7 @@ import scala.build.internal.Constants import scala.build.internal.Constants.* import scala.build.internal.CsLoggerUtil.* import scala.build.internal.Util.{PositionedScalaDependencyOps, ScalaModuleOps} +import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix import scala.collection.mutable final case class Artifacts( @@ -399,18 +400,36 @@ object Artifacts { } val (hasRunner, extraRunnerJars) = - if (scalaOpt.nonEmpty) { + if scalaOpt.nonEmpty then { val addJvmRunner0 = addJvmRunner.getOrElse(false) val runnerJars = - if (addJvmRunner0) { + if addJvmRunner0 then { val maybeSnapshotRepo = - if (runnerVersion.endsWith("SNAPSHOT")) + if runnerVersion.endsWith("SNAPSHOT") then Seq(coursier.Repositories.sonatype("snapshots")) else Nil + val scalaVersion = (for { + scalaArtifactsParams <- scalaArtifactsParamsOpt + scalaParams = scalaArtifactsParams.params + scalaVersion = scalaParams.scalaVersion + } yield scalaVersion).getOrElse(defaultScalaVersion) + val runnerVersion0 = + if scalaVersion.startsWith("3") && + scalaVersion.coursierVersion < s"$scala3LtsPrefix.0".coursierVersion + then { + logger.message( + s"""$warnPrefix Scala $scalaVersion is no longer supported by the runner module. + |$warnPrefix Defaulting to a legacy runner module version: $runnerLegacyVersion. + |$warnPrefix To use the latest runner, upgrade Scala to at least $scala3LtsPrefix.""" + .stripMargin + ) + runnerLegacyVersion + } + else runnerVersion value { artifacts( Seq(Positioned.none( - dep"$runnerOrganization::$runnerModuleName:$runnerVersion,intransitive" + dep"$runnerOrganization::$runnerModuleName:$runnerVersion0,intransitive" )), extraRepositories ++ maybeSnapshotRepo, scalaArtifactsParamsOpt.map(_.params), diff --git a/project/deps.sc b/project/deps.sc index eff63ff19b..826c100578 100644 --- a/project/deps.sc +++ b/project/deps.sc @@ -2,6 +2,10 @@ import Deps.Versions import mill._ import scalalib._ +object Cli { + def runnerLegacyVersion = "1.7.1" // last runner version to support pre-LTS Scala 3 versions +} + object Scala { def scala212 = "2.12.20" def scala213 = "2.13.16" @@ -32,9 +36,13 @@ object Scala { def scalaJs = "1.19.0" def scalaJsCli = scalaJs // this must be compatible with the Scala.js version + private def patchVer(sv: String): Int = + sv.split('.').drop(2).head.takeWhile(_.isDigit).toInt + + private def minorVer(sv: String): Int = + sv.split('.').drop(1).head.takeWhile(_.isDigit).toInt + def listAll: Seq[String] = { - def patchVer(sv: String): Int = - sv.split('.').drop(2).head.takeWhile(_.isDigit).toInt val max212 = patchVer(scala212) val max213 = patchVer(scala213) val max30 = 2 @@ -57,6 +65,12 @@ object Scala { (0 until max37).map(i => s"3.7.$i") ++ Seq(scala3Next) } + def legacyScala3Versions = + listAll + .filter(_.startsWith("3")) + .distinct + .filter(minorVer(_) < minorVer(scala3Lts)) + def maxAmmoniteScala212Version = scala212 def maxAmmoniteScala213Version = scala213 def maxAmmoniteScala3Version = "3.6.3" From 623ff3f9188933380508b404262cbcb8af1748a9 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Mon, 5 May 2025 10:42:57 +0200 Subject: [PATCH 4/4] Downgrade the `test-runner` module dependency when it's added to the classpath for Scala >= 3.0.0 and < current Scala 3 LTS version --- .../cli/integration/RunTestsDefault.scala | 3 +- .../cli/integration/TestTestsDefault.scala | 37 ++++++++++++++++-- .../scala/cli/integration/TestUtil.scala | 9 +++++ .../main/scala/scala/build/Artifacts.scala | 38 +++++++++++++------ 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala index 7c8b29eb99..ce7c0fd7c5 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala @@ -189,8 +189,7 @@ class RunTestsDefault extends RunTestDefinitions } for { - scalaVersion <- - Constants.legacyScala3Versions.sorted.reverse.distinctBy(_.split('.').take(2).mkString(".")) + scalaVersion <- TestUtil.legacyScalaVersionsOnePerMinor expectedMessage = "Hello, world!" expectedWarning = s"Defaulting to a legacy runner module version: ${Constants.runnerLegacyVersion}" diff --git a/modules/integration/src/test/scala/scala/cli/integration/TestTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/TestTestsDefault.scala index 9f17e7827f..af36ceecaa 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/TestTestsDefault.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/TestTestsDefault.scala @@ -5,6 +5,7 @@ import com.eed3si9n.expecty.Expecty.expect import java.io.File import scala.cli.integration.Constants.munitVersion +import scala.cli.integration.TestUtil.StringOps class TestTestsDefault extends TestTestDefinitions with TestDefault { test("Pure Java with Scala tests") { @@ -62,11 +63,39 @@ class TestTestsDefault extends TestTestDefinitions with TestDefault { ).fromRoot { root => val output = os.proc(TestUtil.cli, "test", extraOptions, ".", "--cross", "--power") .call(cwd = root).out.text() - def countOccurrences(a: String, b: String): Int = - if (b.isEmpty) 0 // Avoid infinite splitting - else a.sliding(b.length).count(_ == b) expect(output.contains(expectedMessage)) - expect(countOccurrences(output, expectedMessage) == crossVersions.length) + expect(output.countOccurrences(expectedMessage) == crossVersions.length) } } + + for { + scalaVersion <- TestUtil.legacyScalaVersionsOnePerMinor + expectedMessage = "Hello, world!" + expectedWarning = + s"Defaulting to a legacy test-runner module version: ${Constants.runnerLegacyVersion}" + } + test(s"run a simple test with Scala $scalaVersion (legacy)") { + TestInputs(os.rel / "example.test.scala" -> + // using JUnit to work around TASTy and macro incompatibilities + s"""//> using dep com.novocode:junit-interface:0.11 + |import org.junit.Test + | + |class MyTests { + | @Test + | def foo(): Unit = { + | assert(2 + 2 == 4) + | println("$expectedMessage") + | } + |} + |""".stripMargin).fromRoot { root => + val res = + os.proc(TestUtil.cli, "test", ".", "-S", scalaVersion, TestUtil.extraOptions) + .call(cwd = root, stderr = os.Pipe) + val out = res.out.trim() + expect(out.contains(expectedMessage)) + val err = res.err.trim() + expect(err.contains(expectedWarning)) + expect(err.countOccurrences(expectedWarning) == 1) + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala b/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala index c2af7803e1..d212018107 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala @@ -27,6 +27,9 @@ object TestUtil { val cli: Seq[String] = cliCommand(cliPath) val ltsEqualsNext: Boolean = Constants.scala3Lts equals Constants.scala3Next + lazy val legacyScalaVersionsOnePerMinor: Seq[String] = + Constants.legacyScala3Versions.sorted.reverse.distinctBy(_.split('.').take(2).mkString(".")) + def cliCommand(cliPath: String): Seq[String] = if (isNativeCli) Seq(cliPath) @@ -367,4 +370,10 @@ object TestUtil { Thread.sleep(200L) if (proc.isAlive()) proc.destroyForcibly() } + + implicit class StringOps(a: String) { + def countOccurrences(b: String): Int = + if (b.isEmpty) 0 // Avoid infinite splitting + else a.sliding(b.length).count(_ == b) + } } diff --git a/modules/options/src/main/scala/scala/build/Artifacts.scala b/modules/options/src/main/scala/scala/build/Artifacts.scala index 2078f4219b..66daa72d50 100644 --- a/modules/options/src/main/scala/scala/build/Artifacts.scala +++ b/modules/options/src/main/scala/scala/build/Artifacts.scala @@ -130,11 +130,32 @@ object Artifacts { ): Either[BuildException, Artifacts] = either { val dependencies = defaultDependencies ++ extraDependencies + val scalaVersion = (for { + scalaArtifactsParams <- scalaArtifactsParamsOpt + scalaParams = scalaArtifactsParams.params + scalaVersion = scalaParams.scalaVersion + } yield scalaVersion).getOrElse(defaultScalaVersion) + + val shouldUseLegacyRunners = + scalaVersion.startsWith("3") && + scalaVersion.coursierVersion < s"$scala3LtsPrefix.0".coursierVersion + val jvmTestRunnerDependencies = - if (addJvmTestRunner) - Seq(dep"$testRunnerOrganization::$testRunnerModuleName:$testRunnerVersion") - else - Nil + if addJvmTestRunner then { + val testRunnerVersion0 = + if shouldUseLegacyRunners then { + logger.message( + s"""$warnPrefix Scala $scalaVersion is no longer supported by the test-runner module. + |$warnPrefix Defaulting to a legacy test-runner module version: $runnerLegacyVersion. + |$warnPrefix To use the latest test-runner, upgrade Scala to at least $scala3LtsPrefix.""" + .stripMargin + ) + runnerLegacyVersion + } + else testRunnerVersion + Seq(dep"$testRunnerOrganization::$testRunnerModuleName:$testRunnerVersion0") + } + else Nil val jmhDependencies = addJmhDependencies.toSeq .map(version => dep"${Constants.jmhOrg}:${Constants.jmhGeneratorBytecodeModule}:$version") @@ -408,15 +429,8 @@ object Artifacts { if runnerVersion.endsWith("SNAPSHOT") then Seq(coursier.Repositories.sonatype("snapshots")) else Nil - val scalaVersion = (for { - scalaArtifactsParams <- scalaArtifactsParamsOpt - scalaParams = scalaArtifactsParams.params - scalaVersion = scalaParams.scalaVersion - } yield scalaVersion).getOrElse(defaultScalaVersion) val runnerVersion0 = - if scalaVersion.startsWith("3") && - scalaVersion.coursierVersion < s"$scala3LtsPrefix.0".coursierVersion - then { + if shouldUseLegacyRunners then { logger.message( s"""$warnPrefix Scala $scalaVersion is no longer supported by the runner module. |$warnPrefix Defaulting to a legacy runner module version: $runnerLegacyVersion.