From 227da37a13ac3da8c3f7207b9b19142112cb2c09 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Wed, 17 Apr 2024 09:54:37 +0200 Subject: [PATCH] Add a launcher option allowing to override the default Scala version --- .../preprocessing/ScriptPreprocessor.scala | 20 ++++---- .../src/main/scala/scala/cli/ScalaCli.scala | 6 +++ .../scala/cli/commands/ScalaCommand.scala | 22 ++++++--- .../scala/cli/commands/package0/Package.scala | 5 +- .../scala/cli/commands/version/Version.scala | 6 +-- .../scala/cli/launcher/LauncherOptions.scala | 11 ++++- .../integration/PackageTestDefinitions.scala | 2 +- .../cli/integration/PackageTestsDefault.scala | 7 +-- .../scala/cli/integration/SipScalaTests.scala | 46 +++++++++++++++++++ .../scala/build/options/BuildOptions.scala | 12 +++-- .../scala/build/options/ScalaOptions.scala | 3 +- 11 files changed, 109 insertions(+), 31 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/preprocessing/ScriptPreprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/ScriptPreprocessor.scala index 2603602c76..3d3633f303 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/ScriptPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/ScriptPreprocessor.scala @@ -158,25 +158,23 @@ case object ScriptPreprocessor extends Preprocessor { * code wrapper compatible with provided BuildOptions */ def getScriptWrapper(buildOptions: BuildOptions): CodeWrapper = { - val scalaVersionOpt = for { - maybeScalaVersion <- buildOptions.scalaOptions.scalaVersion - scalaVersion <- maybeScalaVersion.versionOpt - } yield scalaVersion + val effectiveScalaVersion = + buildOptions.scalaOptions.scalaVersion.flatMap(_.versionOpt) + .orElse(buildOptions.scalaOptions.defaultScalaVersion) + .getOrElse(Constants.defaultScalaVersion) def objectCodeWrapperForScalaVersion = // AppObjectWrapper only introduces the 'main.sc' restriction when used in Scala 3, there's no gain in using it with Scala 3 - if (scalaVersionOpt.exists(_.startsWith("2"))) - AppCodeWrapper - else - ObjectCodeWrapper + if effectiveScalaVersion.startsWith("2") then AppCodeWrapper + else ObjectCodeWrapper buildOptions.scriptOptions.forceObjectWrapper match { case Some(true) => objectCodeWrapperForScalaVersion case _ => buildOptions.scalaOptions.platform.map(_.value) match { - case Some(_: Platform.JS.type) => objectCodeWrapperForScalaVersion - case _ if scalaVersionOpt.exists(_.startsWith("2")) => AppCodeWrapper - case _ => ClassCodeWrapper + case Some(_: Platform.JS.type) => objectCodeWrapperForScalaVersion + case _ if effectiveScalaVersion.startsWith("2") => AppCodeWrapper + case _ => ClassCodeWrapper } } } diff --git a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala index 6b36ac7123..0d7dacb6d1 100644 --- a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala +++ b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala @@ -53,6 +53,10 @@ object ScalaCli { private def isGraalvmNativeImage: Boolean = sys.props.contains("org.graalvm.nativeimage.imagecode") + private var defaultScalaVersion: Option[String] = None + + def getDefaultScalaVersion: String = defaultScalaVersion.getOrElse(Constants.defaultScalaVersion) + private def partitionArgs(args: Array[String]): (Array[String], Array[String]) = { val systemProps = args.takeWhile(_.startsWith("-D")) (systemProps, args.drop(systemProps.size)) @@ -237,6 +241,8 @@ object ScalaCli { && sys.props.get("scala-cli.kind").exists(_.startsWith("jvm")) => JavaLauncherCli.runAndExit(args) case None => + if launcherOpts.cliUserScalaVersion.nonEmpty then + defaultScalaVersion = launcherOpts.cliUserScalaVersion if launcherOpts.powerOptions.power then isSipScala = false args0.toArray diff --git a/modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala b/modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala index 360e93b123..42f12f5806 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala @@ -38,7 +38,8 @@ abstract class ScalaCommand[T <: HasGlobalOptions](implicit myParser: Parser[T], private val globalOptionsAtomic: AtomicReference[GlobalOptions] = new AtomicReference(GlobalOptions.default) - private def globalOptions: GlobalOptions = globalOptionsAtomic.get() + private def globalOptions: GlobalOptions = globalOptionsAtomic.get() + protected def defaultScalaVersion: String = ScalaCli.getDefaultScalaVersion def sharedOptions(t: T): Option[SharedOptions] = // hello borked unused warning None @@ -133,7 +134,7 @@ abstract class ScalaCommand[T <: HasGlobalOptions](implicit myParser: Parser[T], .toOption .flatten .map(_.scalaVersion) - .getOrElse(Constants.defaultScalaVersion) + .getOrElse(defaultScalaVersion) val (fromIndex, completions) = cache.logger.use { coursier.complete.Complete(cache) .withInput(prefix) @@ -313,13 +314,20 @@ abstract class ScalaCommand[T <: HasGlobalOptions](implicit myParser: Parser[T], * change this behaviour. */ def buildOptions(options: T): Option[BuildOptions] = - sharedOptions(options).map(shared => shared.buildOptions().orExit(shared.logger)) + sharedOptions(options) + .map(shared => shared.buildOptions().orExit(shared.logger)) protected def buildOptionsOrExit(options: T): BuildOptions = - buildOptions(options).getOrElse { - sharedOptions(options).foreach(_.logger.debug("build options could not be initialized")) - sys.exit(1) - } + buildOptions(options) + .map(bo => + bo.copy(scalaOptions = + bo.scalaOptions.copy(defaultScalaVersion = Some(defaultScalaVersion)) + ) + ) + .getOrElse { + sharedOptions(options).foreach(_.logger.debug("build options could not be initialized")) + sys.exit(1) + } override def shouldSuppressExperimentalFeatureWarnings: Boolean = globalOptions.globalSuppress.suppressExperimentalFeatureWarning .orElse { diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala index 3fa7899067..9784e4f2ff 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala @@ -166,7 +166,10 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { } def finalBuildOptions(options: PackageOptions): BuildOptions = { - val finalBuildOptions = options.finalBuildOptions.orExit(options.shared.logger) + val initialOptions = options.finalBuildOptions.orExit(options.shared.logger) + val finalBuildOptions = initialOptions.copy(scalaOptions = + initialOptions.scalaOptions.copy(defaultScalaVersion = Some(defaultScalaVersion)) + ) val buildOptions = finalBuildOptions.copy( javaOptions = finalBuildOptions.javaOptions.copy( javaOpts = diff --git a/modules/cli/src/main/scala/scala/cli/commands/version/Version.scala b/modules/cli/src/main/scala/scala/cli/commands/version/Version.scala index 79104e783d..545e30c1ab 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/version/Version.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/version/Version.scala @@ -5,12 +5,12 @@ import caseapp.core.help.HelpFormat import scala.build.Logger import scala.build.internal.Constants -import scala.cli.CurrentParams import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup} import scala.cli.commands.update.Update import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel} import scala.cli.config.PasswordOption import scala.cli.util.ArgHelpers.* +import scala.cli.{CurrentParams, ScalaCli} object Version extends ScalaCommand[VersionOptions] { override def group: String = HelpCommandGroup.Miscellaneous.toString @@ -34,7 +34,7 @@ object Version extends ScalaCommand[VersionOptions] { else None } if options.cliVersion then println(Constants.version) - else if options.scalaVersion then println(Constants.defaultScalaVersion) + else if options.scalaVersion then println(ScalaCli.getDefaultScalaVersion) else { println(versionInfo) if !options.offline then @@ -51,5 +51,5 @@ object Version extends ScalaCommand[VersionOptions] { val version = Constants.version val detailedVersionOpt = Constants.detailedVersion.filter(_ != version).fold("")(" (" + _ + ")") s"""$fullRunnerName version: $version$detailedVersionOpt - |Scala version (default): ${Constants.defaultScalaVersion}""".stripMargin + |Scala version (default): ${ScalaCli.getDefaultScalaVersion}""".stripMargin } diff --git a/modules/cli/src/main/scala/scala/cli/launcher/LauncherOptions.scala b/modules/cli/src/main/scala/scala/cli/launcher/LauncherOptions.scala index c701e1bab6..f120fb66e1 100644 --- a/modules/cli/src/main/scala/scala/cli/launcher/LauncherOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/launcher/LauncherOptions.scala @@ -3,7 +3,7 @@ package scala.cli.launcher import caseapp.* import scala.cli.commands.shared.HelpGroup -import scala.cli.commands.tags +import scala.cli.commands.{Constants, tags} @HelpMessage("Run another Scala CLI version") final case class LauncherOptions( @@ -19,6 +19,15 @@ final case class LauncherOptions( @Hidden @Tag(tags.implementation) cliScalaVersion: Option[String] = None, + @Group(HelpGroup.Launcher.toString) + @HelpMessage( + s"The default version of Scala used when processing user inputs (current default: ${Constants.defaultScalaVersion}). Can be overridden with --scala-version. " + ) + @ValueDescription("version") + @Hidden + @Tag(tags.implementation) + @Name("cliDefaultScalaVersion") + cliUserScalaVersion: Option[String] = None, @Recurse powerOptions: PowerOptions = PowerOptions() ) diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala index 4e7d05bd0d..0b25aa6a7c 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala @@ -14,7 +14,7 @@ import scala.util.{Properties, Using} abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs { _: TestScalaVersion => - private lazy val extraOptions = scalaVersionArgs ++ TestUtil.extraOptions + protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions def maybeUseBash(cmd: os.Shellable*)(cwd: os.Path = null): os.CommandResult = { val res = os.proc(cmd*).call(cwd = cwd, check = false) diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestsDefault.scala index e0d0c94de0..c60a8c27ed 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestsDefault.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestsDefault.scala @@ -13,13 +13,14 @@ class PackageTestsDefault extends PackageTestDefinitions with TestDefault { |""".stripMargin ) inputs.fromRoot { root => - val runRes = os.proc(TestUtil.cli, "run", "--native", ".") + val runRes = os.proc(TestUtil.cli, "run", "--native", ".", extraOptions) .call(cwd = root) val runOutput = runRes.out.trim().linesIterator.filter(!_.startsWith("[info] ")).toVector expect(runOutput == Seq("Hello")) - val packageRes = os.proc(TestUtil.cli, "--power", "package", "--native", ".", "-o", "hello") - .call(cwd = root, mergeErrIntoOut = true) + val packageRes = + os.proc(TestUtil.cli, "--power", "package", "--native", ".", "-o", "hello", extraOptions) + .call(cwd = root, mergeErrIntoOut = true) val packageOutput = packageRes.out.trim() val topPackageOutput = packageOutput.linesIterator.takeWhile(!_.startsWith("Wrote ")).toVector // no compilation or Scala Native pipeline output, as this should just re-use what the run command wrote diff --git a/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala b/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala index 8ee12af011..7f68e838a2 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala @@ -513,4 +513,50 @@ class SipScalaTests extends ScalaCliSuite { expect(res.err.trim().contains(expectedError)) } } + + for { + sv <- Seq(Constants.scala212, Constants.scala213, Constants.scala3NextRc) + code = + if (sv.startsWith("3")) "println(dotty.tools.dotc.config.Properties.simpleVersionString)" + else "println(scala.util.Properties.versionNumberString)" + } { + test( + s"default Scala version overridden with $sv by a launcher parameter is respected when running a script" + ) { + TestInputs(os.rel / "simple.sc" -> code) + .fromRoot { root => + val r = os.proc( + TestUtil.cli, + "--cli-default-scala-version", + sv, + "run", + "simple.sc", + "--with-compiler" + ) + .call(cwd = root) + expect(r.out.trim() == sv) + } + } + + test( + s"default Scala version overridden with $sv by a launcher parameter is respected when printing Scala version" + ) { + TestInputs.empty.fromRoot { root => + val r = + os.proc(TestUtil.cli, "--cli-default-scala-version", sv, "version", "--scala-version") + .call(cwd = root) + expect(r.out.trim() == sv) + } + } + + test( + s"default Scala version overridden with $sv by a launcher parameter is respected when printing versions" + ) { + TestInputs.empty.fromRoot { root => + val r = os.proc(TestUtil.cli, "--cli-default-scala-version", sv, "version") + .call(cwd = root) + expect(r.out.trim().contains(sv)) + } + } + } } diff --git a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala index 92b191bd37..e57f83536d 100644 --- a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala @@ -247,7 +247,12 @@ final case class BuildOptions( // used when downloading fails private def defaultStableScalaVersions = - Seq(defaultScala212Version, defaultScala213Version, defaultScalaVersion) + Seq( + defaultScala212Version, + defaultScala213Version, + defaultScalaVersion, + scalaOptions.defaultScalaVersion.getOrElse(defaultScalaVersion) + ).distinct def javaHome(): Positioned[JavaHomeInfo] = javaCommand0 @@ -315,7 +320,8 @@ final case class BuildOptions( val defaultVersions = Set( Constants.defaultScalaVersion, Constants.defaultScala212Version, - Constants.defaultScala213Version + Constants.defaultScala213Version, + scalaOptions.defaultScalaVersion.getOrElse(Constants.defaultScalaVersion) ) val svOpt: Option[String] = scalaOptions.scalaVersion match { @@ -379,7 +385,7 @@ final case class BuildOptions( } Some(sv) - case None => Some(Constants.defaultScalaVersion) + case None => Some(scalaOptions.defaultScalaVersion.getOrElse(Constants.defaultScalaVersion)) } svOpt match { diff --git a/modules/options/src/main/scala/scala/build/options/ScalaOptions.scala b/modules/options/src/main/scala/scala/build/options/ScalaOptions.scala index 9d312739de..9679a83f71 100644 --- a/modules/options/src/main/scala/scala/build/options/ScalaOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/ScalaOptions.scala @@ -14,7 +14,8 @@ final case class ScalaOptions( extraScalaVersions: Set[String] = Set.empty, compilerPlugins: Seq[Positioned[AnyDependency]] = Nil, platform: Option[Positioned[Platform]] = None, - extraPlatforms: Map[Platform, Positioned[Unit]] = Map.empty + extraPlatforms: Map[Platform, Positioned[Unit]] = Map.empty, + defaultScalaVersion: Option[String] = None ) { def normalize: ScalaOptions = { var opt = this