diff --git a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala index e87957ceb2..769646e086 100644 --- a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala +++ b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala @@ -55,10 +55,11 @@ object ScalaCli { private def isGraalvmNativeImage: Boolean = sys.props.contains("org.graalvm.nativeimage.imagecode") - private var defaultScalaVersion: Option[String] = None - val launcherPredefinedRepositories: ListBuffer[String] = ListBuffer.empty + private var maybeLauncherOptions: Option[LauncherOptions] = None - def getDefaultScalaVersion: String = defaultScalaVersion.getOrElse(Constants.defaultScalaVersion) + def launcherOptions: LauncherOptions = maybeLauncherOptions.getOrElse(LauncherOptions()) + def getDefaultScalaVersion: String = + launcherOptions.scalaRunner.cliUserScalaVersion.getOrElse(Constants.defaultScalaVersion) private def partitionArgs(args: Array[String]): (Array[String], Array[String]) = { val systemProps = args.takeWhile(_.startsWith("-D")) @@ -232,25 +233,20 @@ object ScalaCli { System.err.println(e.message) sys.exit(1) case Right((launcherOpts, args0)) => + maybeLauncherOptions = Some(launcherOpts) launcherOpts.cliVersion.map(_.trim).filter(_.nonEmpty) match { case Some(ver) => - val powerArgs = - if (launcherOpts.powerOptions.power) Seq("--power") - else Nil - val newArgs = powerArgs ++ args0 + val powerArgs = launcherOpts.powerOptions.toCliArgs + val scalaRunnerArgs = launcherOpts.scalaRunner.toCliArgs + val newArgs = powerArgs ++ scalaRunnerArgs ++ args0 LauncherCli.runAndExit(ver, launcherOpts, newArgs) case _ if javaMajorVersion < 17 && sys.props.get("scala-cli.kind").exists(_.startsWith("jvm")) => JavaLauncherCli.runAndExit(args) case None => - launcherOpts.progName.foreach { pn => - progName = pn - } - if launcherOpts.cliUserScalaVersion.nonEmpty then - defaultScalaVersion = launcherOpts.cliUserScalaVersion - if launcherOpts.cliPredefinedRepository.nonEmpty then - launcherPredefinedRepositories.addAll(launcherOpts.cliPredefinedRepository) + launcherOpts.scalaRunner.progName + .foreach(pn => progName = pn) 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 42f12f5806..33ae56b369 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala @@ -9,6 +9,7 @@ import caseapp.core.{Arg, Error, RemainingArgs} import caseapp.{HelpMessage, Name} import coursier.core.{Repository, Version} import dependency.* +import org.codehaus.plexus.classworlds.launcher.Launcher import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} @@ -28,6 +29,7 @@ import scala.cli.commands.util.CommandHelpers import scala.cli.commands.util.ScalacOptionsUtil.* import scala.cli.config.{ConfigDb, Keys} import scala.cli.internal.ProcUtil +import scala.cli.launcher.LauncherOptions import scala.cli.util.ConfigDbUtils.* import scala.cli.{CurrentParams, ScalaCli} import scala.util.{Properties, Try} @@ -38,8 +40,9 @@ abstract class ScalaCommand[T <: HasGlobalOptions](implicit myParser: Parser[T], private val globalOptionsAtomic: AtomicReference[GlobalOptions] = new AtomicReference(GlobalOptions.default) - private def globalOptions: GlobalOptions = globalOptionsAtomic.get() - protected def defaultScalaVersion: String = ScalaCli.getDefaultScalaVersion + private def globalOptions: GlobalOptions = globalOptionsAtomic.get() + protected def launcherOptions: LauncherOptions = ScalaCli.launcherOptions + protected def defaultScalaVersion: String = ScalaCli.getDefaultScalaVersion def sharedOptions(t: T): Option[SharedOptions] = // hello borked unused warning None diff --git a/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala b/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala index 5a6c865268..65c3cae03b 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala @@ -15,6 +15,7 @@ import scala.cli.commands.ScalaCommand import scala.cli.commands.publish.ConfigUtil.* import scala.cli.commands.shared.SharedOptions import scala.cli.config.{ConfigDb, Keys} +import scala.cli.launcher.LauncherOptions import scala.concurrent.Await import scala.concurrent.duration.Duration @@ -29,6 +30,14 @@ object Bsp extends ScalaCommand[BspOptions] { val content = os.read.bytes(os.Path(optionsPath, os.pwd)) readFromArray(content)(SharedOptions.jsonCodec) }.getOrElse(options.shared) + private def latestLauncherOptions(options: BspOptions): LauncherOptions = + options.jsonLauncherOptions + .map(path => os.Path(path, os.pwd)) + .filter(path => os.exists(path) && os.isFile(path)) + .map { optionsPath => + val content = os.read.bytes(os.Path(optionsPath, os.pwd)) + readFromArray(content)(LauncherOptions.jsonCodec) + }.getOrElse(launcherOptions) private def latestEnvsFromFile(options: BspOptions): Map[String, String] = options.envs .map(path => os.Path(path, os.pwd)) @@ -48,19 +57,21 @@ object Bsp extends ScalaCommand[BspOptions] { pprint.err.log(args) val getSharedOptions: () => SharedOptions = () => latestSharedOptions(options) + val getLauncherOptions: () => LauncherOptions = () => latestLauncherOptions(options) val getEnvsFromFile: () => Map[String, String] = () => latestEnvsFromFile(options) val preprocessInputs: Seq[String] => Either[BuildException, (Inputs, BuildOptions)] = argsSeq => either { - val sharedOptions = getSharedOptions() - val envs = getEnvsFromFile() - val initialInputs = value(sharedOptions.inputs(argsSeq, () => Inputs.default())) + val sharedOptions = getSharedOptions() + val launcherOptions = getLauncherOptions() + val envs = getEnvsFromFile() + val initialInputs = value(sharedOptions.inputs(argsSeq, () => Inputs.default())) if (sharedOptions.logging.verbosity >= 3) pprint.err.log(initialInputs) - val baseOptions = buildOptions(sharedOptions, envs) + val baseOptions = buildOptions(sharedOptions, launcherOptions, envs) val latestLogger = sharedOptions.logging.logger val persistentLogger = new PersistentDiagnosticLogger(latestLogger) @@ -99,8 +110,10 @@ object Bsp extends ScalaCommand[BspOptions] { */ val initialBspOptions = { val sharedOptions = getSharedOptions() + val launcherOptions = getLauncherOptions() val envs = getEnvsFromFile() - val bspBuildOptions = buildOptions(sharedOptions, envs).orElse(finalBuildOptions) + val bspBuildOptions = buildOptions(sharedOptions, launcherOptions, envs) + .orElse(finalBuildOptions) BspReloadableOptions( buildOptions = bspBuildOptions, bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(bspBuildOptions)) @@ -111,13 +124,14 @@ object Bsp extends ScalaCommand[BspOptions] { } val bspReloadableOptionsReference = BspReloadableOptions.Reference { () => - val sharedOptions = getSharedOptions() - val envs = getEnvsFromFile() + val sharedOptions = getSharedOptions() + val launcherOptions = getLauncherOptions() + val envs = getEnvsFromFile() val bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(finalBuildOptions)) .orExit(sharedOptions.logger) BspReloadableOptions( - buildOptions = buildOptions(sharedOptions, envs), + buildOptions = buildOptions(sharedOptions, launcherOptions, envs), bloopRifleConfig = sharedOptions.bloopRifleConfig().orExit(sharedOptions.logger), logger = sharedOptions.logging.logger, verbosity = sharedOptions.logging.verbosity @@ -148,11 +162,12 @@ object Bsp extends ScalaCommand[BspOptions] { private def buildOptions( sharedOptions: SharedOptions, + launcherOptions: LauncherOptions, envs: Map[String, String] ): BuildOptions = { val logger = sharedOptions.logger val baseOptions = sharedOptions.buildOptions().orExit(logger) - val adjustedOptions = baseOptions.copy( + val withDefaults = baseOptions.copy( classPathOptions = baseOptions.classPathOptions.copy( fetchSources = baseOptions.classPathOptions.fetchSources.orElse(Some(true)) ), @@ -167,11 +182,11 @@ object Bsp extends ScalaCommand[BspOptions] { baseOptions.notForBloopOptions.addRunnerDependencyOpt.orElse(Some(false)) ) ) - envs.get("JAVA_HOME") - .filter(_ => adjustedOptions.javaOptions.javaHomeOpt.isEmpty) + val withEnvs = envs.get("JAVA_HOME") + .filter(_ => withDefaults.javaOptions.javaHomeOpt.isEmpty) .map(javaHome => - adjustedOptions.copy(javaOptions = - adjustedOptions.javaOptions.copy(javaHomeOpt = + withDefaults.copy(javaOptions = + withDefaults.javaOptions.copy(javaHomeOpt = Some(Positioned( Seq(Position.Custom("ide.env.JAVA_HOME")), os.Path(javaHome, Os.pwd) @@ -179,6 +194,16 @@ object Bsp extends ScalaCommand[BspOptions] { ) ) ) - .getOrElse(adjustedOptions) + .getOrElse(withDefaults) + val withLauncherOptions = withEnvs.copy( + classPathOptions = withEnvs.classPathOptions.copy( + extraRepositories = + (withEnvs.classPathOptions.extraRepositories ++ launcherOptions.scalaRunner.cliPredefinedRepository).distinct + ), + scalaOptions = withEnvs.scalaOptions.copy( + defaultScalaVersion = launcherOptions.scalaRunner.cliUserScalaVersion + ) + ) + withLauncherOptions } } diff --git a/modules/cli/src/main/scala/scala/cli/commands/bsp/BspOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/bsp/BspOptions.scala index 6ae5f241c5..d81af50ef1 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/bsp/BspOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/bsp/BspOptions.scala @@ -19,6 +19,12 @@ final case class BspOptions( @Tag(tags.implementation) jsonOptions: Option[String] = None, + @HelpMessage("Command-line launcher options JSON file") + @ValueDescription("path") + @Hidden + @Tag(tags.implementation) + jsonLauncherOptions: Option[String] = None, + @HelpMessage("Command-line options environment variables file") @ValueDescription("path") @Hidden 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 6f6333a02d..5de030bdc7 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 @@ -62,11 +62,10 @@ object Repl extends ScalaCommand[ReplOptions] { scalaOptions = baseOptions.scalaOptions.copy( scalaVersion = baseOptions.scalaOptions.scalaVersion .orElse { - val defaultScalaVer = ScalaCli.getDefaultScalaVersion val shouldDowngrade = { def needsDowngradeForAmmonite = { import coursier.core.Version - Version(maxAmmoniteScalaVer) < Version(defaultScalaVer) + Version(maxAmmoniteScalaVer) < Version(defaultScalaVersion) } ammonite.contains(true) && ammoniteVersionOpt.isEmpty && @@ -74,7 +73,7 @@ object Repl extends ScalaCommand[ReplOptions] { } if (shouldDowngrade) { logger.message( - s"Scala $defaultScalaVer is not yet supported with this version of Ammonite" + s"Scala $defaultScalaVersion is not yet supported with this version of Ammonite" ) logger.message(s"Defaulting to Scala $maxAmmoniteScalaVer") Some(MaybeScalaVersion(maxAmmoniteScalaVer)) diff --git a/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala b/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala index 0f1f30a1e3..0774b018af 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala @@ -19,6 +19,7 @@ import scala.cli.CurrentParams import scala.cli.commands.shared.{SharedBspFileOptions, SharedOptions} import scala.cli.commands.{CommandUtils, ScalaCommand} import scala.cli.errors.FoundVirtualInputsError +import scala.cli.launcher.LauncherOptions import scala.jdk.CollectionConverters.* object SetupIde extends ScalaCommand[SetupIdeOptions] { @@ -132,6 +133,8 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { val (bspName, bspJsonDestination) = bspDetails(inputs.workspace, options.bspFile) val scalaCliBspJsonDestination = inputs.workspace / Constants.workspaceDirName / "ide-options-v2.json" + val scalaCliBspLauncherOptsJsonDestination = + inputs.workspace / Constants.workspaceDirName / "ide-launcher-options.json" val scalaCliBspInputsJsonDestination = inputs.workspace / Constants.workspaceDirName / "ide-inputs.json" val scalaCliBspEnvsJsonDestination = @@ -151,9 +154,12 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { ) val bspArgs = - List(CommandUtils.getAbsolutePathToScalaCli(progName), "bsp") ++ + List(CommandUtils.getAbsolutePathToScalaCli(progName)) ++ + launcherOptions.toCliArgs ++ + List("bsp") ++ debugOpt ++ List("--json-options", scalaCliBspJsonDestination.toString) ++ + List("--json-launcher-options", scalaCliBspLauncherOptsJsonDestination.toString) ++ List("--envs-file", scalaCliBspEnvsJsonDestination.toString) ++ inputArgs val details = new BspConnectionDetails( @@ -175,10 +181,11 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { implicit val mapCodec: JsonValueCodec[Map[String, String]] = JsonCodecMaker.make - val json = gson.toJson(details) - val scalaCliOptionsForBspJson = writeToArray(options.shared)(SharedOptions.jsonCodec) - val scalaCliBspInputsJson = writeToArray(ideInputs) - val scalaCliBspEnvsJson = writeToArray(sys.env) + val json = gson.toJson(details) + val scalaCliOptionsForBspJson = writeToArray(options.shared)(SharedOptions.jsonCodec) + val scalaCliLaunchOptsForBspJson = writeToArray(launcherOptions)(LauncherOptions.jsonCodec) + val scalaCliBspInputsJson = writeToArray(ideInputs) + val scalaCliBspEnvsJson = writeToArray(sys.env) if (inputs.workspaceOrigin.contains(WorkspaceOrigin.HomeDir)) value(Left(new WorkspaceError( @@ -194,6 +201,11 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { scalaCliOptionsForBspJson, createFolders = true ) + os.write.over( + scalaCliBspLauncherOptsJsonDestination, + scalaCliLaunchOptsForBspJson, + createFolders = true + ) os.write.over( scalaCliBspInputsJsonDestination, scalaCliBspInputsJson, diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala index e42bfc1d61..5cea114d36 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala @@ -407,9 +407,9 @@ final case class SharedOptions( extraCompileOnlyJars = extraCompileOnlyClassPath, extraSourceJars = extraSourceJars.extractedClassPath ++ assumedSourceJars, extraRepositories = - (dependencies.repository ++ ScalaCli.launcherPredefinedRepositories).map(_.trim).filter( - _.nonEmpty - ), + (dependencies.repository ++ ScalaCli.launcherOptions.scalaRunner.cliPredefinedRepository) + .map(_.trim) + .filter(_.nonEmpty), extraDependencies = ShadowingSeq.from( SharedOptions.parseDependencies( dependencies.dependency.map(Positioned.none), 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 545e30c1ab..9315005cbd 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 @@ -34,7 +34,7 @@ object Version extends ScalaCommand[VersionOptions] { else None } if options.cliVersion then println(Constants.version) - else if options.scalaVersion then println(ScalaCli.getDefaultScalaVersion) + else if options.scalaVersion then println(defaultScalaVersion) 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): ${ScalaCli.getDefaultScalaVersion}""".stripMargin + |Scala version (default): $defaultScalaVersion""".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 9b0b2c052f..8d3525f082 100644 --- a/modules/cli/src/main/scala/scala/cli/launcher/LauncherOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/launcher/LauncherOptions.scala @@ -1,8 +1,10 @@ package scala.cli.launcher import caseapp.* +import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec +import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker -import scala.cli.commands.shared.HelpGroup +import scala.cli.commands.shared.{HelpGroup, SharedOptions} import scala.cli.commands.{Constants, tags} @HelpMessage("Run another Scala CLI version") @@ -19,36 +21,20 @@ 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, - @Group(HelpGroup.Launcher.toString) - @HelpMessage("") - @Hidden - @Tag(tags.implementation) - @Name("r") - @Name("repo") - @Name("repository") - @Name("predefinedRepository") - cliPredefinedRepository: List[String] = Nil, - @Group(HelpGroup.Launcher.toString) - @HelpMessage( - "This allows to override the program name identified by Scala CLI as itself (the default is 'scala-cli')" - ) - @Hidden - @Tag(tags.implementation) - progName: Option[String] = None, + @Recurse + scalaRunner: ScalaRunnerLauncherOptions = ScalaRunnerLauncherOptions(), @Recurse powerOptions: PowerOptions = PowerOptions() -) +) { + def toCliArgs: List[String] = + cliVersion.toList.flatMap(v => List("--cli-version", v)) ++ + cliScalaVersion.toList.flatMap(v => List("--cli-scala-version", v)) ++ + scalaRunner.toCliArgs ++ + powerOptions.toCliArgs +} object LauncherOptions { - implicit lazy val parser: Parser[LauncherOptions] = Parser.derive - implicit lazy val help: Help[LauncherOptions] = Help.derive + implicit lazy val parser: Parser[LauncherOptions] = Parser.derive + implicit lazy val help: Help[LauncherOptions] = Help.derive + implicit lazy val jsonCodec: JsonValueCodec[LauncherOptions] = JsonCodecMaker.make } diff --git a/modules/cli/src/main/scala/scala/cli/launcher/PowerOptions.scala b/modules/cli/src/main/scala/scala/cli/launcher/PowerOptions.scala index af4822c139..5fd86643e4 100644 --- a/modules/cli/src/main/scala/scala/cli/launcher/PowerOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/launcher/PowerOptions.scala @@ -17,7 +17,9 @@ case class PowerOptions( @HelpMessage("Allows to use restricted & experimental features") @Tag(tags.must) power: Boolean = false -) +) { + def toCliArgs: List[String] = if power then List("--power") else Nil +} object PowerOptions { implicit val parser: Parser[PowerOptions] = Parser.derive diff --git a/modules/cli/src/main/scala/scala/cli/launcher/ScalaRunnerLauncherOptions.scala b/modules/cli/src/main/scala/scala/cli/launcher/ScalaRunnerLauncherOptions.scala new file mode 100644 index 0000000000..1b67a94b62 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/launcher/ScalaRunnerLauncherOptions.scala @@ -0,0 +1,46 @@ +package scala.cli.launcher + +import caseapp.* +import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec +import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker + +import scala.cli.commands.shared.{HelpGroup, SharedOptions} +import scala.cli.commands.{Constants, tags} + +case class ScalaRunnerLauncherOptions( + @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, + @Group(HelpGroup.Launcher.toString) + @HelpMessage("") + @Hidden + @Tag(tags.implementation) + @Name("r") + @Name("repo") + @Name("repository") + @Name("predefinedRepository") + cliPredefinedRepository: List[String] = Nil, + @Group(HelpGroup.Launcher.toString) + @HelpMessage( + "This allows to override the program name identified by Scala CLI as itself (the default is 'scala-cli')" + ) + @Hidden + @Tag(tags.implementation) + progName: Option[String] = None +) { + def toCliArgs: List[String] = + cliUserScalaVersion.toList.flatMap(v => List("--cli-default-scala-version", v)) ++ + cliPredefinedRepository.flatMap(v => List("--repository", v)) ++ + progName.toList.flatMap(v => List("--prog-name", v)) +} + +object ScalaRunnerLauncherOptions { + implicit val parser: Parser[ScalaRunnerLauncherOptions] = Parser.derive + implicit val help: Help[ScalaRunnerLauncherOptions] = Help.derive +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala index 9c9f61311b..6582b04126 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala @@ -205,16 +205,21 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg |""".stripMargin ) inputs.fromRoot { root => - os.proc(TestUtil.cli, command, ".", extraOptions).call(cwd = root, stdout = os.Inherit) + os.proc(TestUtil.cli, "--power", command, ".", extraOptions) + .call(cwd = root, stdout = os.Inherit) val details = readBspConfig(root) val expectedIdeOptionsFile = root / Constants.workspaceDirName / "ide-options-v2.json" + val expectedIdeLaunchFile = root / Constants.workspaceDirName / "ide-launcher-options.json" val expectedIdeInputsFile = root / Constants.workspaceDirName / "ide-inputs.json" val expectedIdeEnvsFile = root / Constants.workspaceDirName / "ide-envs.json" val expectedArgv = Seq( TestUtil.cliPath, + "--power", "bsp", "--json-options", expectedIdeOptionsFile.toString, + "--json-launcher-options", + expectedIdeLaunchFile.toString, "--envs-file", expectedIdeEnvsFile.toString, root.toString @@ -244,19 +249,22 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg os.proc( "cmd", "/c", - (relativeCliCommand ++ Seq("setup-ide", path.toString) ++ extraOptions) + (relativeCliCommand ++ Seq("--power", "setup-ide", path.toString) ++ extraOptions) .mkString(" ") ) else - os.proc(relativeCliCommand, "setup-ide", path, extraOptions) + os.proc(relativeCliCommand, "--power", "setup-ide", path, extraOptions) proc.call(cwd = root, stdout = os.Inherit) val details = readBspConfig(root / "directory") val expectedArgv = List( TestUtil.cliPath, + "--power", "bsp", "--json-options", (root / "directory" / Constants.workspaceDirName / "ide-options-v2.json").toString, + "--json-launcher-options", + (root / "directory" / Constants.workspaceDirName / "ide-launcher-options.json").toString, "--envs-file", (root / "directory" / Constants.workspaceDirName / "ide-envs.json").toString, (root / "directory" / "simple.sc").toString diff --git a/modules/integration/src/test/scala/scala/cli/integration/BspTests3NextRc.scala b/modules/integration/src/test/scala/scala/cli/integration/BspTests3NextRc.scala index a42659289c..36bf0173e4 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/BspTests3NextRc.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/BspTests3NextRc.scala @@ -1,3 +1,95 @@ package scala.cli.integration -class BspTests3NextRc extends BspTestDefinitions with BspTests3Definitions with Test3NextRc +import ch.epfl.scala.bsp4j as b +import com.eed3si9n.expecty.Expecty.expect + +import scala.async.Async.{async, await} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.jdk.CollectionConverters.* +import scala.util.Properties + +class BspTests3NextRc extends BspTestDefinitions with BspTests3Definitions with Test3NextRc { + test("BSP respects --cli-default-scala-version & --predefined-repository launcher options") { + // 3.5.0-RC1-fakeversion-bin-SNAPSHOT has too long filenames for Windows. + // Yes, seriously. Which is why we can't use it there. + val sv = if (Properties.isWin) Constants.scala3NextRc else "3.5.0-RC1-fakeversion-bin-SNAPSHOT" + val inputs = TestInputs( + os.rel / "simple.sc" -> s"""assert(dotty.tools.dotc.config.Properties.versionNumberString == "$sv")""" + ) + inputs.fromRoot { root => + os.proc(TestUtil.cli, "bloop", "exit", "--power").call(cwd = root) + val localRepoPath = root / "local-repo" + if (Properties.isWin) { + val artifactNames = Seq( + "scala3-compiler_3", + "scala3-staging_3", + "scala3-tasty-inspector_3", + "scala3-sbt-bridge" + ) + for { artifactName <- artifactNames } { + val csRes = os.proc( + TestUtil.cs, + "fetch", + "--cache", + localRepoPath, + s"org.scala-lang:$artifactName:$sv" + ) + .call(cwd = root) + expect(csRes.exitCode == 0) + } + } + else { + TestUtil.initializeGit(root) + os.proc( + "git", + "clone", + "https://github.com/dotty-staging/maven-test-repo.git", + localRepoPath.toString + ).call(cwd = root) + } + val predefinedRepository = + if (Properties.isWin) + (localRepoPath / "https" / "repo1.maven.org" / "maven2").toNIO.toUri.toASCIIString + else + (localRepoPath / "thecache" / "https" / "repo1.maven.org" / "maven2").toNIO.toUri.toASCIIString + os.proc( + TestUtil.cli, + "--cli-default-scala-version", + sv, + "--predefined-repository", + predefinedRepository, + "setup-ide", + "simple.sc", + "--with-compiler", + "--offline", + "--power" + ) + .call(cwd = root) + val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" + expect(ideOptionsPath.toNIO.toFile.exists()) + val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json" + expect(ideEnvsPath.toNIO.toFile.exists()) + val ideLauncherOptionsPath = root / Constants.workspaceDirName / "ide-launcher-options.json" + expect(ideLauncherOptionsPath.toNIO.toFile.exists()) + val jsonOptions = List("--json-options", ideOptionsPath.toString) + val launcherOptions = List("--json-launcher-options", ideLauncherOptionsPath.toString) + val envOptions = List("--envs-file", ideEnvsPath.toString) + val bspOptions = jsonOptions ++ launcherOptions ++ envOptions + withBsp(inputs, Seq("."), bspOptions = bspOptions, reuseRoot = Some(root)) { + (_, _, remoteServer) => + async { + val targets = await(remoteServer.workspaceBuildTargets().asScala) + .getTargets.asScala + .filter(!_.getId.getUri.contains("-test")) + .map(_.getId()) + val compileResult = + await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala) + expect(compileResult.getStatusCode == b.StatusCode.OK) + val runResult = + await(remoteServer.buildTargetRun(new b.RunParams(targets.head)).asScala) + expect(runResult.getStatusCode == b.StatusCode.OK) + } + } + } + } +} 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 7160c0c296..8e8d26f441 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala @@ -597,38 +597,59 @@ class SipScalaTests extends ScalaCliSuite with SbtTestHelper with MillTestHelper for { withBloop <- Seq(true, false) withBloopString = if (withBloop) "with Bloop" else "with --server=false" + sv3 = if (Properties.isWin) "3.5.0-RC1" else "3.5.0-RC1-fakeversion-bin-SNAPSHOT" + sv2 = "2.13.15-bin-ccdcde3" } { test( - s"default Scala version (3.4.1-RC1) coming straight from a predefined local repository $withBloopString" + s"default Scala version ($sv3) coming straight from a predefined local repository $withBloopString" ) { TestInputs( - os.rel / "simple.sc" -> "println(dotty.tools.dotc.config.Properties.simpleVersionString)" + os.rel / "simple.sc" -> "println(dotty.tools.dotc.config.Properties.versionNumberString)" ) .fromRoot { root => val localRepoPath = root / "local-repo" - val sv = "3.4.1-RC1" - val artifactNames = - Seq("scala3-compiler_3", "scala3-staging_3", "scala3-tasty-inspector_3") ++ - (if (withBloop) Seq("scala3-sbt-bridge") else Nil) - for { artifactName <- artifactNames } { - val csRes = os.proc( - TestUtil.cs, - "fetch", - "--cache", - localRepoPath, - s"org.scala-lang:$artifactName:$sv" - ) - .call(cwd = root) - expect(csRes.exitCode == 0) + val sv = sv3 + if (Properties.isWin) { + // 3.5.0-RC1-fakeversion-bin-SNAPSHOT has too long filenames for Windows. + // Yes, seriously. Which is why we can't use it there. + val artifactNames = + Seq("scala3-compiler_3", "scala3-staging_3", "scala3-tasty-inspector_3") ++ + (if (withBloop) Seq("scala3-sbt-bridge") else Nil) + for { artifactName <- artifactNames } { + val csRes = os.proc( + TestUtil.cs, + "fetch", + "--cache", + localRepoPath, + s"org.scala-lang:$artifactName:$sv" + ) + .call(cwd = root) + expect(csRes.exitCode == 0) + } + } + else { + TestUtil.initializeGit(root) + os.proc( + "git", + "clone", + "https://github.com/dotty-staging/maven-test-repo.git", + localRepoPath.toString + ).call(cwd = root) } val buildServerOptions = if (withBloop) Nil else Seq("--server=false") + + val predefinedRepository = + if (Properties.isWin) + (localRepoPath / "https" / "repo1.maven.org" / "maven2").toNIO.toUri.toASCIIString + else + (localRepoPath / "thecache" / "https" / "repo1.maven.org" / "maven2").toNIO.toUri.toASCIIString val r = os.proc( TestUtil.cli, "--cli-default-scala-version", sv, "--predefined-repository", - (localRepoPath / "https" / "repo1.maven.org" / "maven2").toNIO.toUri.toASCIIString, + predefinedRepository, "run", "simple.sc", "--with-compiler", @@ -642,14 +663,14 @@ class SipScalaTests extends ScalaCliSuite with SbtTestHelper with MillTestHelper } test( - s"default Scala version (2.13.15-bin-ccdcde3) coming straight from a predefined local repository $withBloopString" + s"default Scala version ($sv2) coming straight from a predefined local repository $withBloopString" ) { TestInputs( os.rel / "simple.sc" -> "println(scala.util.Properties.versionNumberString)" ) .fromRoot { root => val localRepoPath = root / "local-repo" - val sv = "2.13.15-bin-ccdcde3" + val sv = sv2 val artifactNames = Seq("scala-compiler") ++ (if (withBloop) Seq("scala2-sbt-bridge") else Nil) for { artifactName <- artifactNames } { @@ -808,4 +829,21 @@ class SipScalaTests extends ScalaCliSuite with SbtTestHelper with MillTestHelper expect(!res.err.trim().contains("TASTY")) } } + + test("--cli-version and --cli-default-scala-version can be passed in tandem") { + TestInputs.empty.fromRoot { root => + val cliVersion = "1.3.1" + val scalaVersion = "3.5.1-RC1-bin-20240522-e0c030c-NIGHTLY" + val res = os.proc( + TestUtil.cli, + "--cli-version", + cliVersion, + "--cli-default-scala-version", + scalaVersion, + "version" + ).call(cwd = root) + expect(res.out.trim().contains(cliVersion)) + expect(res.out.trim().contains(scalaVersion)) + } + } } diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index 9cddd00be2..7c3c0c52f5 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -1879,6 +1879,11 @@ Available in commands: [Internal] Command-line options JSON file +### `--json-launcher-options` + +[Internal] +Command-line launcher options JSON file + ### `--envs` Aliases: `--envs-file` diff --git a/website/docs/reference/scala-command/cli-options.md b/website/docs/reference/scala-command/cli-options.md index 0d1c1d95eb..5eb4017ef7 100644 --- a/website/docs/reference/scala-command/cli-options.md +++ b/website/docs/reference/scala-command/cli-options.md @@ -1355,6 +1355,12 @@ Available in commands: Command-line options JSON file +### `--json-launcher-options` + +`IMPLEMENTATION specific` per Scala Runner specification + +Command-line launcher options JSON file + ### `--envs` Aliases: `--envs-file` diff --git a/website/docs/reference/scala-command/runner-specification.md b/website/docs/reference/scala-command/runner-specification.md index b0209677a5..8138e9e6f0 100644 --- a/website/docs/reference/scala-command/runner-specification.md +++ b/website/docs/reference/scala-command/runner-specification.md @@ -5008,6 +5008,10 @@ Exclude sources Command-line options JSON file +**--json-launcher-options** + +Command-line launcher options JSON file + **--envs** Command-line options environment variables file