From bd4ed07b44f24186ea6b377ecdf01404f4f49915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 19 Apr 2026 01:08:52 +0200 Subject: [PATCH 1/9] Include Native tools and deps in export --json --- .../scala/cli/exportCmd/JsonProject.scala | 13 ++++++- .../ExportJsonTestDefinitions.scala | 32 +++++++++++++++- .../scala/scala/build/info/BuildInfo.scala | 38 +++++++++++++++++-- 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala index c63bff1c77..9b118ad5c1 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala @@ -5,13 +5,14 @@ import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import java.io.PrintStream -import scala.build.info.{BuildInfo, ExportDependencyFormat, ScopedBuildInfo} +import scala.build.info.{BuildInfo, ExportDependencyFormat, NativeOptionsInfo, ScopedBuildInfo} import scala.util.Using final case class JsonProject(buildInfo: BuildInfo) extends Project { def sorted = this.copy( buildInfo = buildInfo.copy( - scopes = buildInfo.scopes.map { case (k, v) => k -> v.sorted } + scopes = buildInfo.scopes.map { case (k, v) => k -> v.sorted }, + nativeOptions = buildInfo.nativeOptions.map(_.sorted) ) ) @@ -49,6 +50,14 @@ final case class JsonProject(buildInfo: BuildInfo) extends Project { } } +extension (n: NativeOptionsInfo) { + def sorted(using ord: Ordering[String]) = n.copy( + compilerPlugins = n.compilerPlugins.sorted(using JsonProject.ordering), + runtimeDependencies = n.runtimeDependencies.sorted(using JsonProject.ordering), + toolingDependencies = n.toolingDependencies.sorted(using JsonProject.ordering) + ) +} + extension (s: ScopedBuildInfo) { def sorted(using ord: Ordering[String]) = s.copy( s.sources.sorted, diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala index 3ff2499767..5496dd8e57 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala @@ -107,11 +107,41 @@ abstract class ExportJsonTestDefinitions extends ScalaCliSuite with TestScalaVer val jsonContents = readJson(exportJsonProc.out.text()) + val nativeVersion = Constants.scalaNativeVersion val expectedJsonContents = s"""{ |"scalaVersion":"3.2.2", |"platform":"Native", - |"scalaNativeVersion":"${Constants.scalaNativeVersion}", + |"scalaNativeVersion":"$nativeVersion", + |"nativeOptions": { + | "scalaNativeVersion":"$nativeVersion", + | "compilerPlugins": [ + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"nscplugin","fullName":"nscplugin_3.2.2"}, + | "version":"$nativeVersion" + | } + | ], + | "runtimeDependencies": [ + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"javalib_native0.5","fullName":"javalib_native0.5_3"}, + | "version":"$nativeVersion" + | }, + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"scala3lib_native0.5","fullName":"scala3lib_native0.5_3"}, + | "version":"3.2.2+$nativeVersion" + | } + | ], + | "toolingDependencies": [ + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"scala-native-cli","fullName":"scala-native-cli_2.12"}, + | "version":"$nativeVersion" + | } + | ] + |}, |"scopes": { | "main": { | "sources": ["${withEscapedBackslashes(root / "Main.scala")}"], diff --git a/modules/options/src/main/scala/scala/build/info/BuildInfo.scala b/modules/options/src/main/scala/scala/build/info/BuildInfo.scala index 482621b751..e430f1ce6b 100644 --- a/modules/options/src/main/scala/scala/build/info/BuildInfo.scala +++ b/modules/options/src/main/scala/scala/build/info/BuildInfo.scala @@ -6,6 +6,13 @@ import scala.build.info.BuildInfo.escapeBackslashes import scala.build.internal.Constants import scala.build.options.* +final case class NativeOptionsInfo( + scalaNativeVersion: String, + compilerPlugins: Seq[ExportDependencyFormat] = Nil, + runtimeDependencies: Seq[ExportDependencyFormat] = Nil, + toolingDependencies: Seq[ExportDependencyFormat] = Nil +) + final case class BuildInfo( projectVersion: Option[String] = None, scalaVersion: Option[String] = None, @@ -14,6 +21,7 @@ final case class BuildInfo( scalaJsVersion: Option[String] = None, jsEsVersion: Option[String] = None, scalaNativeVersion: Option[String] = None, + nativeOptions: Option[NativeOptionsInfo] = None, mainClass: Option[String] = None, scopes: Map[String, ScopedBuildInfo] = Map.empty, scalaCliVersion: Option[String] = None @@ -152,11 +160,35 @@ object BuildInfo { ) } - private def scalaNativeSettings(options: ScalaNativeOptions): BuildInfo = + private def scalaNativeSettings(options: BuildOptions): BuildInfo = { + val nativeOptions = options.scalaNativeOptions + val nativeVersion = nativeOptions.finalVersion + val scalaParamsOpt = options.scalaParams.getOrElse(None) + val sv = scalaParamsOpt.map(_.scalaVersion) + .orElse(options.scalaOptions.defaultScalaVersion) + .getOrElse(Constants.defaultScalaVersion) + + val runtimeDeps = nativeOptions.nativeDependencies(sv) + .map(ExportDependencyFormat(_, scalaParamsOpt)) + val compilerPluginDeps = nativeOptions.compilerPlugins + .map(ExportDependencyFormat(_, scalaParamsOpt)) + val toolingDeps = Seq(ExportDependencyFormat( + "org.scala-native", + ArtifactId("scala-native-cli", "scala-native-cli_2.12"), + nativeVersion + )) + BuildInfo( platform = Some(Platform.Native.repr), - scalaNativeVersion = Some(options.finalVersion) + scalaNativeVersion = Some(nativeVersion), + nativeOptions = Some(NativeOptionsInfo( + scalaNativeVersion = nativeVersion, + compilerPlugins = compilerPluginDeps, + runtimeDependencies = runtimeDeps, + toolingDependencies = toolingDeps + )) ) + } private def jvmSettings(options: BuildOptions): BuildInfo = BuildInfo( @@ -173,7 +205,7 @@ object BuildInfo { case Some(Platform.JS) => scalaJsSettings(options.scalaJsOptions) case Some(Platform.Native) => - scalaNativeSettings(options.scalaNativeOptions) + scalaNativeSettings(options) case _ => jvmSettings(options) } From 2f2d21e6c1cf71c38a2872e86c58ea8d73a7a970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Tue, 5 May 2026 01:20:39 +0200 Subject: [PATCH 2/9] Add list-targets subcommand to expose the build matrix as JSON Closes #4241 --- .../scala/scala/cli/ScalaCliCommands.scala | 1 + .../commands/listtargets/ListTargets.scala | 73 +++++++++++++++++++ .../listtargets/ListTargetsOptions.scala | 29 ++++++++ .../cli/integration/ListTargetsTests.scala | 56 ++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargets.scala create mode 100644 modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargetsOptions.scala create mode 100644 modules/integration/src/test/scala/scala/cli/integration/ListTargetsTests.scala diff --git a/modules/cli/src/main/scala/scala/cli/ScalaCliCommands.scala b/modules/cli/src/main/scala/scala/cli/ScalaCliCommands.scala index 8e9fd968f7..023ce995dd 100644 --- a/modules/cli/src/main/scala/scala/cli/ScalaCliCommands.scala +++ b/modules/cli/src/main/scala/scala/cli/ScalaCliCommands.scala @@ -42,6 +42,7 @@ class ScalaCliCommands( new HelpCmd(help), installcompletions.InstallCompletions, installhome.InstallHome, + listtargets.ListTargets, `new`.New, repl.Repl, package0.Package, diff --git a/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargets.scala b/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargets.scala new file mode 100644 index 0000000000..e0ae9b9fb4 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargets.scala @@ -0,0 +1,73 @@ +package scala.cli.commands.listtargets + +import caseapp.* +import com.github.plokhotnyuk.jsoniter_scala.core.{JsonValueCodec, WriterConfig, writeToStream} +import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker + +import scala.build.* +import scala.build.errors.BuildException +import scala.build.input.Inputs +import scala.build.options.BuildOptions +import scala.cli.CurrentParams +import scala.cli.commands.shared.SharedOptions +import scala.cli.commands.{ScalaCommand, SpecificationLevel} + +object ListTargets extends ScalaCommand[ListTargetsOptions] { + override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.EXPERIMENTAL + override def names: List[List[String]] = List( + List("list-targets") + ) + override def sharedOptions(options: ListTargetsOptions): Option[SharedOptions] = + Some(options.shared) + + private final case class TargetEntry( + platform: String, + scalaVersion: Option[String] + ) + + private given JsonValueCodec[List[TargetEntry]] = JsonCodecMaker.make + + private def loadCrossSources( + inputs: Inputs, + buildOptions: BuildOptions, + logger: Logger + ): Either[BuildException, CrossSources] = + CrossSources.forInputs( + inputs, + Sources.defaultPreprocessors( + buildOptions.archiveCache, + buildOptions.internal.javaClassNameVersionOpt, + () => buildOptions.javaHome().value.javaCommand + ), + logger, + buildOptions.suppressWarningOptions, + buildOptions.internal.exclude, + download = buildOptions.downloader + ).map(_._1) + + private def targetOf(options: BuildOptions): TargetEntry = { + val platform = options.platform.value.repr + val sv = options.scalaParams.toOption.flatten.map(_.scalaVersion) + .orElse(options.scalaOptions.scalaVersion.flatMap(_.versionOpt)) + .orElse(options.scalaOptions.defaultScalaVersion) + TargetEntry(platform, sv) + } + + override def runCommand( + options: ListTargetsOptions, + args: RemainingArgs, + logger: Logger + ): Unit = { + val initialBuildOptions = buildOptionsOrExit(options) + val inputs = options.shared.inputs(args.all).orExit(logger) + CurrentParams.workspaceOpt = Some(inputs.workspace) + + val crossSources = loadCrossSources(inputs, initialBuildOptions, logger).orExit(logger) + val resolvedOptions = crossSources.sharedOptions(initialBuildOptions) + + val targets = (resolvedOptions +: resolvedOptions.crossOptions).map(targetOf).distinct.toList + + writeToStream(targets, System.out, WriterConfig.withIndentionStep(1)) + System.out.println() + } +} diff --git a/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargetsOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargetsOptions.scala new file mode 100644 index 0000000000..ca62d9a227 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargetsOptions.scala @@ -0,0 +1,29 @@ +package scala.cli.commands.listtargets + +import caseapp.* + +import scala.cli.commands.shared.{HasSharedOptions, SharedOptions} + +@HelpMessage(ListTargetsOptions.helpMessage, "", ListTargetsOptions.detailedHelpMessage) +final case class ListTargetsOptions( + @Recurse + shared: SharedOptions = SharedOptions() +) extends HasSharedOptions + +object ListTargetsOptions { + implicit lazy val parser: Parser[ListTargetsOptions] = Parser.derive + implicit lazy val help: Help[ListTargetsOptions] = Help.derive + + private val helpHeader = + "Print the full matrix of declared build targets (platform x Scala version) as JSON." + val helpMessage: String = helpHeader + val detailedHelpMessage: String = + s"""$helpHeader + | + |Reads `using` directives and CLI options from the inputs and emits one entry per + |declared target, so external tools can enumerate the matrix without parsing + |directives themselves. + | + |Each entry has shape `{ "platform": "JVM"|"JS"|"Native", "scalaVersion": "..." }`. + |The `scalaVersion` field is omitted for pure-Java projects.""".stripMargin +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/ListTargetsTests.scala b/modules/integration/src/test/scala/scala/cli/integration/ListTargetsTests.scala new file mode 100644 index 0000000000..5ff44136d6 --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ListTargetsTests.scala @@ -0,0 +1,56 @@ +package scala.cli.integration + +import com.eed3si9n.expecty.Expecty.expect + +class ListTargetsTests extends ScalaCliSuite { + + private def normalize(s: String): String = s.replaceAll("\\s", "") + + test("list-targets emits the full platform x scala-version matrix") { + val inputs = TestInputs( + os.rel / "Main.scala" -> + """//> using platforms jvm native + |//> using scala 3.6.4 3.5.0 + |@main def hello = println("hi") + |""".stripMargin + ) + + inputs.fromRoot { root => + val res = os.proc(TestUtil.cli, "--power", "list-targets", ".").call(cwd = root) + val out = normalize(res.out.text()) + + val expected = normalize( + """[ + | { "platform": "JVM", "scalaVersion": "3.6.4" }, + | { "platform": "Native", "scalaVersion": "3.6.4" }, + | { "platform": "JVM", "scalaVersion": "3.5.0" }, + | { "platform": "Native", "scalaVersion": "3.5.0" } + |]""".stripMargin + ) + + expect(out == expected) + } + } + + test("list-targets with no cross directives returns a single entry") { + val inputs = TestInputs( + os.rel / "Main.scala" -> + """//> using scala 3.6.4 + |@main def hello = println("hi") + |""".stripMargin + ) + + inputs.fromRoot { root => + val res = os.proc(TestUtil.cli, "--power", "list-targets", ".").call(cwd = root) + val out = normalize(res.out.text()) + + val expected = normalize( + """[ + | { "platform": "JVM", "scalaVersion": "3.6.4" } + |]""".stripMargin + ) + + expect(out == expected) + } + } +} From ac8efc33756826ad10800c84291cebc372ed9410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Tue, 5 May 2026 11:56:15 +0200 Subject: [PATCH 3/9] format, render docs --- .../commands/listtargets/ListTargets.scala | 4 +- .../listtargets/ListTargetsOptions.scala | 2 +- website/docs/reference/cli-options.md | 54 +++++++++---------- website/docs/reference/commands.md | 17 ++++++ 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargets.scala b/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargets.scala index e0ae9b9fb4..0612d81b2b 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargets.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargets.scala @@ -14,7 +14,7 @@ import scala.cli.commands.{ScalaCommand, SpecificationLevel} object ListTargets extends ScalaCommand[ListTargetsOptions] { override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.EXPERIMENTAL - override def names: List[List[String]] = List( + override def names: List[List[String]] = List( List("list-targets") ) override def sharedOptions(options: ListTargetsOptions): Option[SharedOptions] = @@ -47,7 +47,7 @@ object ListTargets extends ScalaCommand[ListTargetsOptions] { private def targetOf(options: BuildOptions): TargetEntry = { val platform = options.platform.value.repr - val sv = options.scalaParams.toOption.flatten.map(_.scalaVersion) + val sv = options.scalaParams.toOption.flatten.map(_.scalaVersion) .orElse(options.scalaOptions.scalaVersion.flatMap(_.versionOpt)) .orElse(options.scalaOptions.defaultScalaVersion) TargetEntry(platform, sv) diff --git a/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargetsOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargetsOptions.scala index ca62d9a227..0970038c02 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargetsOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/listtargets/ListTargetsOptions.scala @@ -16,7 +16,7 @@ object ListTargetsOptions { private val helpHeader = "Print the full matrix of declared build targets (platform x Scala version) as JSON." - val helpMessage: String = helpHeader + val helpMessage: String = helpHeader val detailedHelpMessage: String = s"""$helpHeader | diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index 45f360181f..54730628ac 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -46,7 +46,7 @@ are assumed to be Scala compiler options and will be propagated to Scala Compile Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -62,7 +62,7 @@ Set JMH version (default: 1.37) Available in commands: -[`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall) +[`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall) @@ -219,7 +219,7 @@ Force overwriting values for key Available in commands: -[`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall) +[`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall) @@ -258,7 +258,7 @@ Run given command against all provided Scala versions and/or platforms Available in commands: -[`bloop`](./commands.md#bloop), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bloop`](./commands.md#bloop), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -278,7 +278,7 @@ Debug mode (attach by default) Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -511,7 +511,7 @@ Pass scalafmt version before running it (3.11.0 by default). If passed, this ove Available in commands: -[`add-path`](./commands.md#add-path), [`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`default-file`](./commands.md#default-file), [`dependency-update`](./commands.md#dependency-update), [`directories`](./commands.md#directories), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`help`](./commands.md#help), [`install completions` , `install-completions`](./commands.md#install-completions), [`install-home`](./commands.md#install-home), [`new`](./commands.md#new), [`package`](./commands.md#package), [`pgp pull`](./commands.md#pgp-pull), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`github secret list` , `gh secret list`](./commands.md#github-secret-list), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall), [`uninstall completions` , `uninstall-completions`](./commands.md#uninstall-completions), [`update`](./commands.md#update), [`version`](./commands.md#version) +[`add-path`](./commands.md#add-path), [`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`default-file`](./commands.md#default-file), [`dependency-update`](./commands.md#dependency-update), [`directories`](./commands.md#directories), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`help`](./commands.md#help), [`install completions` , `install-completions`](./commands.md#install-completions), [`install-home`](./commands.md#install-home), [`list-targets`](./commands.md#list-targets), [`new`](./commands.md#new), [`package`](./commands.md#package), [`pgp pull`](./commands.md#pgp-pull), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`github secret list` , `gh secret list`](./commands.md#github-secret-list), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall), [`uninstall completions` , `uninstall-completions`](./commands.md#uninstall-completions), [`update`](./commands.md#update), [`version`](./commands.md#version) @@ -531,7 +531,7 @@ Suppress warnings about using deprecated features Available in commands: -[`add-path`](./commands.md#add-path), [`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`default-file`](./commands.md#default-file), [`dependency-update`](./commands.md#dependency-update), [`directories`](./commands.md#directories), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`help`](./commands.md#help), [`install completions` , `install-completions`](./commands.md#install-completions), [`install-home`](./commands.md#install-home), [`new`](./commands.md#new), [`package`](./commands.md#package), [`pgp create`](./commands.md#pgp-create), [`pgp key-id`](./commands.md#pgp-key-id), [`pgp pull`](./commands.md#pgp-pull), [`pgp push`](./commands.md#pgp-push), [`pgp sign`](./commands.md#pgp-sign), [`pgp verify`](./commands.md#pgp-verify), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`github secret list` , `gh secret list`](./commands.md#github-secret-list), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall), [`uninstall completions` , `uninstall-completions`](./commands.md#uninstall-completions), [`update`](./commands.md#update), [`version`](./commands.md#version) +[`add-path`](./commands.md#add-path), [`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`default-file`](./commands.md#default-file), [`dependency-update`](./commands.md#dependency-update), [`directories`](./commands.md#directories), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`help`](./commands.md#help), [`install completions` , `install-completions`](./commands.md#install-completions), [`install-home`](./commands.md#install-home), [`list-targets`](./commands.md#list-targets), [`new`](./commands.md#new), [`package`](./commands.md#package), [`pgp create`](./commands.md#pgp-create), [`pgp key-id`](./commands.md#pgp-key-id), [`pgp pull`](./commands.md#pgp-pull), [`pgp push`](./commands.md#pgp-push), [`pgp sign`](./commands.md#pgp-sign), [`pgp verify`](./commands.md#pgp-verify), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`github secret list` , `gh secret list`](./commands.md#github-secret-list), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall), [`uninstall completions` , `uninstall-completions`](./commands.md#uninstall-completions), [`update`](./commands.md#update), [`version`](./commands.md#version) @@ -555,7 +555,7 @@ Print help message, including hidden options, and exit Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -661,7 +661,7 @@ Add java properties. Note that options equal `-Dproperty=value` are assumed to b Available in commands: -[`bloop`](./commands.md#bloop), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bloop`](./commands.md#bloop), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -711,7 +711,7 @@ Port for BSP debugging Available in commands: -[`add-path`](./commands.md#add-path), [`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`default-file`](./commands.md#default-file), [`dependency-update`](./commands.md#dependency-update), [`directories`](./commands.md#directories), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`help`](./commands.md#help), [`install completions` , `install-completions`](./commands.md#install-completions), [`install-home`](./commands.md#install-home), [`new`](./commands.md#new), [`package`](./commands.md#package), [`pgp pull`](./commands.md#pgp-pull), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`github secret list` , `gh secret list`](./commands.md#github-secret-list), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall), [`uninstall completions` , `uninstall-completions`](./commands.md#uninstall-completions), [`update`](./commands.md#update), [`version`](./commands.md#version) +[`add-path`](./commands.md#add-path), [`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`default-file`](./commands.md#default-file), [`dependency-update`](./commands.md#dependency-update), [`directories`](./commands.md#directories), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`help`](./commands.md#help), [`install completions` , `install-completions`](./commands.md#install-completions), [`install-home`](./commands.md#install-home), [`list-targets`](./commands.md#list-targets), [`new`](./commands.md#new), [`package`](./commands.md#package), [`pgp pull`](./commands.md#pgp-pull), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`github secret list` , `gh secret list`](./commands.md#github-secret-list), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall), [`uninstall completions` , `uninstall-completions`](./commands.md#uninstall-completions), [`update`](./commands.md#update), [`version`](./commands.md#version) @@ -749,7 +749,7 @@ List main classes available in the current context Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1012,7 +1012,7 @@ Key server to push / pull keys from Available in commands: -[`add-path`](./commands.md#add-path), [`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`default-file`](./commands.md#default-file), [`dependency-update`](./commands.md#dependency-update), [`directories`](./commands.md#directories), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`help`](./commands.md#help), [`install completions` , `install-completions`](./commands.md#install-completions), [`install-home`](./commands.md#install-home), [`new`](./commands.md#new), [`package`](./commands.md#package), [`pgp pull`](./commands.md#pgp-pull), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`github secret list` , `gh secret list`](./commands.md#github-secret-list), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall), [`uninstall completions` , `uninstall-completions`](./commands.md#uninstall-completions), [`update`](./commands.md#update), [`version`](./commands.md#version) +[`add-path`](./commands.md#add-path), [`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`default-file`](./commands.md#default-file), [`dependency-update`](./commands.md#dependency-update), [`directories`](./commands.md#directories), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`help`](./commands.md#help), [`install completions` , `install-completions`](./commands.md#install-completions), [`install-home`](./commands.md#install-home), [`list-targets`](./commands.md#list-targets), [`new`](./commands.md#new), [`package`](./commands.md#package), [`pgp pull`](./commands.md#pgp-pull), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`github secret list` , `gh secret list`](./commands.md#github-secret-list), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall), [`uninstall completions` , `uninstall-completions`](./commands.md#uninstall-completions), [`update`](./commands.md#update), [`version`](./commands.md#version) @@ -1234,7 +1234,7 @@ Dummy mode - don't upload any secret to GitHub Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1343,7 +1343,7 @@ Run Java commands using a manifest-based class path (shortens command length) Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1442,7 +1442,7 @@ Whether to run the Scala.js CLI on the JVM or using a native executable Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1516,7 +1516,7 @@ Enable/disable Scala Native multithreading support Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1534,7 +1534,7 @@ Add a `scalac` option. Note that options starting with `-g`, `-language`, `-opt` Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1574,7 +1574,7 @@ Run scalafix rule(s) explicitly, overriding the configuration file default. Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1621,7 +1621,7 @@ Aliases: `-n` Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1737,7 +1737,7 @@ Option with deprecated alias (internal, do not use) Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1786,7 +1786,7 @@ A synonym to --markdown-snippet, which defaults the sub-command to `run` when no Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1800,7 +1800,7 @@ Generate BuildInfo for project Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -1924,7 +1924,7 @@ A github token used to access GitHub. Not needed in most cases. Available in commands: -[`add-path`](./commands.md#add-path), [`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`default-file`](./commands.md#default-file), [`dependency-update`](./commands.md#dependency-update), [`directories`](./commands.md#directories), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`help`](./commands.md#help), [`install completions` , `install-completions`](./commands.md#install-completions), [`install-home`](./commands.md#install-home), [`new`](./commands.md#new), [`package`](./commands.md#package), [`pgp pull`](./commands.md#pgp-pull), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`github secret list` , `gh secret list`](./commands.md#github-secret-list), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall), [`uninstall completions` , `uninstall-completions`](./commands.md#uninstall-completions), [`update`](./commands.md#update), [`version`](./commands.md#version) +[`add-path`](./commands.md#add-path), [`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop output`](./commands.md#bloop-output), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`default-file`](./commands.md#default-file), [`dependency-update`](./commands.md#dependency-update), [`directories`](./commands.md#directories), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`help`](./commands.md#help), [`install completions` , `install-completions`](./commands.md#install-completions), [`install-home`](./commands.md#install-home), [`list-targets`](./commands.md#list-targets), [`new`](./commands.md#new), [`package`](./commands.md#package), [`pgp pull`](./commands.md#pgp-pull), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`github secret list` , `gh secret list`](./commands.md#github-secret-list), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall), [`uninstall completions` , `uninstall-completions`](./commands.md#uninstall-completions), [`update`](./commands.md#update), [`version`](./commands.md#version) @@ -1948,7 +1948,7 @@ Enable actionable diagnostics Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`version`](./commands.md#version) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`version`](./commands.md#version) @@ -2126,7 +2126,7 @@ Force overwriting destination files Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -2345,7 +2345,7 @@ Time to wait between staging repository operation retries, in milliseconds. Available in commands: -[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) @@ -2385,7 +2385,7 @@ Available in commands: Available in commands: -[`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) +[`bsp`](./commands.md#bsp), [`clean`](./commands.md#clean), [`compile`](./commands.md#compile), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fix`](./commands.md#fix), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`list-targets`](./commands.md#list-targets), [`package`](./commands.md#package), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test) diff --git a/website/docs/reference/commands.md b/website/docs/reference/commands.md index afb2b8c70b..dc3f242a5f 100644 --- a/website/docs/reference/commands.md +++ b/website/docs/reference/commands.md @@ -180,6 +180,23 @@ For detailed documentation refer to our website: https://scala-cli.virtuslab.org Accepts option groups: [global suppress warning](./cli-options.md#global-suppress-warning-options), [install completions](./cli-options.md#install-completions-options), [logging](./cli-options.md#logging-options), [power](./cli-options.md#power-options), [verbosity](./cli-options.md#verbosity-options) +## list-targets + +Print the full matrix of declared build targets (platform x Scala version) as JSON. + +Reads `using` directives and CLI options from the inputs and emits one entry per +declared target, so external tools can enumerate the matrix without parsing +directives themselves. + +Each entry has shape `{ "platform": "JVM"|"JS"|"Native", "scalaVersion": "..." }`. +The `scalaVersion` field is omitted for pure-Java projects. + +The `list-targets` sub-command is experimental. +Please bear in mind that non-ideal user experience should be expected. +If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli + +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [workspace](./cli-options.md#workspace-options) + ## new New giter8 template. From 9273ed8e99f2c29287785488063b283025e40235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 6 May 2026 20:41:35 +0200 Subject: [PATCH 4/9] Include JVM test-runner in export --json output The runner is added inside Artifacts.apply (gated on addJvmTestRunner) - after BuildInfo has been materialized, so export --json reported only user-declared test deps. Extract the runner-version selection (with legacy fallbacks for old Scala/Java) into a shared helper and call it from ScopedSources.getScopedBuildInfo and JsonProjectDescriptor.export so the test scope's dependencies reflect what scala-cli would resolve at test time. Injection is gated on scope == Test, non-empty sources, JVM platform, and a Scala (non-Java-only) build, mirroring Artifacts. --- .../scala/scala/build/ScopedSources.scala | 10 +- .../cli/exportCmd/JsonProjectDescriptor.scala | 8 +- .../ExportJsonTestDefinitions.scala | 133 ++++++++++++++++++ .../main/scala/scala/build/Artifacts.scala | 101 +++++++++---- .../scala/build/info/ScopedBuildInfo.scala | 44 +++++- 5 files changed, 261 insertions(+), 35 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/ScopedSources.scala b/modules/build/src/main/scala/scala/build/ScopedSources.scala index f77bc1baf1..a401a69c09 100644 --- a/modules/build/src/main/scala/scala/build/ScopedSources.scala +++ b/modules/build/src/main/scala/scala/build/ScopedSources.scala @@ -90,7 +90,7 @@ final case class ScopedSources( Sources.InMemory( Left("build-info"), os.rel / "BuildInfo.scala", - value(buildInfo(combinedOptions, workspace)).generateContents().getBytes( + value(buildInfo(combinedOptions, workspace, logger)).generateContents().getBytes( StandardCharsets.UTF_8 ), None @@ -123,7 +123,11 @@ final case class ScopedSources( buildOptionsFor(scope) .foldRight(baseOptions)(_.orElse(_)) - def buildInfo(baseOptions: BuildOptions, workspace: os.Path): Either[BuildException, BuildInfo] = + def buildInfo( + baseOptions: BuildOptions, + workspace: os.Path, + logger: Logger + ): Either[BuildException, BuildInfo] = either { def getScopedBuildInfo(scope: Scope): ScopedBuildInfo = val combinedOptions = combinedBuildOptions(scope, baseOptions) @@ -133,7 +137,7 @@ final case class ScopedSources( unwrappedScripts.flatMap(_.valueFor(scope).toSeq).flatMap(_.originalPath.toOption)) .map(_._2.toString) - ScopedBuildInfo(combinedOptions, sourcePaths ++ inMemoryPaths) + ScopedBuildInfo.forScope(combinedOptions, sourcePaths ++ inMemoryPaths, scope, logger) val baseBuildInfo = value(BuildInfo(combinedBuildOptions(Scope.Main, baseOptions), workspace)) diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala index 378f4a2c17..b950723242 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala @@ -16,16 +16,16 @@ final case class JsonProjectDescriptor( sourcesMain: Sources, sourcesTest: Sources ): Either[BuildException, JsonProject] = { - def getScopedBuildInfo(options: BuildOptions, sources: Sources) = + def getScopedBuildInfo(options: BuildOptions, sources: Sources, scope: Scope) = val sourcePaths = sources.paths.map(_._1.toString) val inMemoryPaths = sources.inMemory.flatMap(_.originalPath.toSeq.map(_._2.toString)) - ScopedBuildInfo(options, sourcePaths ++ inMemoryPaths) + ScopedBuildInfo.forScope(options, sourcePaths ++ inMemoryPaths, scope, logger) for { baseBuildInfo <- BuildInfo(optionsMain, workspace) - mainBuildInfo = getScopedBuildInfo(optionsMain, sourcesMain) - testBuildInfo = getScopedBuildInfo(optionsTest, sourcesTest) + mainBuildInfo = getScopedBuildInfo(optionsMain, sourcesMain, Scope.Main) + testBuildInfo = getScopedBuildInfo(optionsTest, sourcesTest, Scope.Test) } yield JsonProject(baseBuildInfo .withScope(Scope.Main.name, mainBuildInfo) .withScope(Scope.Test.name, testBuildInfo)) diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala index 5496dd8e57..ec88c19c81 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala @@ -212,6 +212,139 @@ abstract class ExportJsonTestDefinitions extends ScalaCliSuite with TestScalaVer } } + test("export json injects JVM test-runner into test scope") { + val inputs = TestInputs( + os.rel / "Main.scala" -> + """object Main { + | def main(args: Array[String]): Unit = println("hi") + |} + |""".stripMargin, + os.rel / "unit.test.scala" -> + s"""//> using dep org.scalameta::munit::${Constants.munitVersion} + | + |class MyTest extends munit.FunSuite { test("ok") { assert(true) } } + |""".stripMargin + ) + inputs.fromRoot { root => + val exportJsonProc = + os.proc(TestUtil.cli, "--power", "export", "--json", ".", "--jvm", "temurin:17") + .call(cwd = root) + val jsonContents = readJson(exportJsonProc.out.text()) + val expectedFullName = s"test-runner_${Constants.scala3NextPrefix.split('.').head}" + // The test scope should include both munit and the scala-cli test-runner. + expect(jsonContents.contains("\"name\":\"test-runner\"")) + expect(jsonContents.contains(s"\"fullName\":\"$expectedFullName\"")) + expect(jsonContents.contains("\"groupId\":\"org.virtuslab.scala-cli\"")) + expect(jsonContents.contains("\"name\":\"munit\"")) + } + } + + test("export json includes JVM test-runner even when no test framework dep is declared") { + val inputs = TestInputs( + os.rel / "Main.scala" -> + """object Main { + | def main(args: Array[String]): Unit = println("hi") + |} + |""".stripMargin, + os.rel / "unit.test.scala" -> + """class MyTest { def foo() = () } + |""".stripMargin + ) + inputs.fromRoot { root => + val exportJsonProc = + os.proc(TestUtil.cli, "--power", "export", "--json", ".", "--jvm", "temurin:17") + .call(cwd = root) + val jsonContents = readJson(exportJsonProc.out.text()) + expect(jsonContents.contains("\"name\":\"test-runner\"")) + expect(jsonContents.contains("\"groupId\":\"org.virtuslab.scala-cli\"")) + } + } + + test("export json includes legacy JVM test-runner for Scala 2.12") { + val inputs = TestInputs( + os.rel / "Main.scala" -> + """object Main { + | def main(args: Array[String]): Unit = println("hi") + |} + |""".stripMargin, + os.rel / "unit.test.scala" -> + """class MyTest { def foo() = () } + |""".stripMargin + ) + inputs.fromRoot { root => + val exportJsonProc = + os.proc( + TestUtil.cli, + "--power", + "export", + "--json", + ".", + "--jvm", + "temurin:17", + "--scala", + Constants.scala212 + ) + .call(cwd = root) + val jsonContents = readJson(exportJsonProc.out.text()) + expect(jsonContents.contains("\"fullName\":\"test-runner_2.12\"")) + expect(jsonContents.contains("\"groupId\":\"org.virtuslab.scala-cli\"")) + expect(jsonContents.contains(s"\"version\":\"${Constants.runnerScala2LegacyVersion}\"")) + } + } + + test("export json includes legacy JVM test-runner for Scala 2.13") { + val inputs = TestInputs( + os.rel / "Main.scala" -> + """object Main { + | def main(args: Array[String]): Unit = println("hi") + |} + |""".stripMargin, + os.rel / "unit.test.scala" -> + """class MyTest { def foo() = () } + |""".stripMargin + ) + inputs.fromRoot { root => + val exportJsonProc = + os.proc( + TestUtil.cli, + "--power", + "export", + "--json", + ".", + "--jvm", + "temurin:17", + "--scala", + Constants.scala213 + ) + .call(cwd = root) + val jsonContents = readJson(exportJsonProc.out.text()) + expect(jsonContents.contains("\"fullName\":\"test-runner_2.13\"")) + expect(jsonContents.contains("\"groupId\":\"org.virtuslab.scala-cli\"")) + expect(jsonContents.contains(s"\"version\":\"${Constants.runnerScala2LegacyVersion}\"")) + } + } + + test("export json does not inject test-runner for Native target") { + val inputs = TestInputs( + os.rel / "Main.scala" -> + """object Main { + | def main(args: Array[String]): Unit = println("hi") + |} + |""".stripMargin, + os.rel / "unit.test.scala" -> + """class MyTest { def foo() = () } + |""".stripMargin + ) + inputs.fromRoot { root => + val exportJsonProc = + os.proc(TestUtil.cli, "--power", "export", "--json", ".", "--native") + .call(cwd = root) + val jsonContents = readJson(exportJsonProc.out.text()) + expect(!jsonContents.contains("\"name\":\"test-runner\"")) + expect(!jsonContents.contains("org.virtuslab.scala-cli")) + } + } + test("export json with js") { val inputs = TestInputs( os.rel / "Main.scala" -> diff --git a/modules/options/src/main/scala/scala/build/Artifacts.scala b/modules/options/src/main/scala/scala/build/Artifacts.scala index c3cfa4c8ea..c6dd3dcf8d 100644 --- a/modules/options/src/main/scala/scala/build/Artifacts.scala +++ b/modules/options/src/main/scala/scala/build/Artifacts.scala @@ -118,6 +118,79 @@ object Artifacts { addScalapy: Option[String] ) + /** Selects the test-runner module version that scala-cli would resolve at test time. + * + * Falls back to a legacy version when the Scala or Java version is no longer supported by the + * current test-runner module. Mirrors the logic used inside [[Artifacts.apply]] so the export + * stays faithful to what test-time resolution would produce. + */ + def jvmTestRunnerVersion( + scalaVersion: String, + jvmVersion: Int, + logger: Logger, + logLegacyWarnings: Boolean + ): String = { + val shouldUseLegacyJava8Runners = jvmVersion < Constants.scala38MinJavaVersion + val shouldUseLegacyScala3Runners = + scalaVersion.startsWith("3") && + scalaVersion.coursierVersion < s"$scala3LtsPrefix.0".coursierVersion + val shouldUseLegacyScala2Runners = scalaVersion.startsWith("2") + val shouldUseLegacyScalaRunners = shouldUseLegacyScala3Runners || shouldUseLegacyScala2Runners + val shouldUseLegacyRunners = shouldUseLegacyScalaRunners || shouldUseLegacyJava8Runners + + val runnerLegacyVersion = + if scalaVersion.startsWith("3") then runnerScala30LegacyVersion + else runnerScala2LegacyVersion + + if shouldUseLegacyRunners then { + if logLegacyWarnings then { + if shouldUseLegacyScalaRunners then + logger.message( + s"$warnPrefix Scala $scalaVersion is no longer supported by the test-runner module." + ) + if shouldUseLegacyJava8Runners then + logger.message( + s"$warnPrefix Java $jvmVersion is no longer supported by the test-runner module." + ) + logger.message( + s"$warnPrefix Defaulting to a legacy test-runner module version: $runnerLegacyVersion." + ) + if shouldUseLegacyScalaRunners then + logger.message( + s"$warnPrefix To use the latest test-runner, upgrade Scala to at least $scala3LtsPrefix." + ) + if shouldUseLegacyJava8Runners then + logger.message( + s"$warnPrefix To use the latest test-runner, upgrade Java to at least ${Constants.defaultJavaVersion}." + ) + } + runnerLegacyVersion + } + else testRunnerVersion + } + + /** The test-runner dependency rendered for [[scala.build.info.ExportDependencyFormat]] consumers + * (e.g. `scala-cli export --json`). + * + * The artifact name is `test-runner_` to match how scala-cli resolves it at + * test time. + */ + def jvmTestRunnerExportDependency( + scalaVersion: String, + scalaBinaryVersion: String, + jvmVersion: Int, + logger: Logger + ): scala.build.info.ExportDependencyFormat = { + import scala.build.info.{ArtifactId, ExportDependencyFormat} + val version = jvmTestRunnerVersion(scalaVersion, jvmVersion, logger, logLegacyWarnings = false) + val fullName = s"$testRunnerModuleName${"_"}$scalaBinaryVersion" + ExportDependencyFormat( + groupId = testRunnerOrganization, + artifactId = ArtifactId(testRunnerModuleName, fullName), + version = version + ) + } + def apply( scalaArtifactsParamsOpt: Option[ScalaArtifactsParams], javacPluginDependencies: Seq[Positioned[AnyDependency]], @@ -159,34 +232,8 @@ object Artifacts { val jvmTestRunnerDependencies = if addJvmTestRunner then { - val runnerLegacyVersion = - if scalaVersion.startsWith("3") - then runnerScala30LegacyVersion - else runnerScala2LegacyVersion val testRunnerVersion0 = - if shouldUseLegacyRunners then { - if shouldUseLegacyScalaRunners then - logger.message( - s"$warnPrefix Scala $scalaVersion is no longer supported by the test-runner module." - ) - if shouldUseLegacyJava8Runners then - logger.message( - s"$warnPrefix Java $jvmVersion is no longer supported by the test-runner module." - ) - logger.message( - s"$warnPrefix Defaulting to a legacy test-runner module version: $runnerLegacyVersion." - ) - if shouldUseLegacyScalaRunners then - logger.message( - s"$warnPrefix To use the latest test-runner, upgrade Scala to at least $scala3LtsPrefix." - ) - if shouldUseLegacyJava8Runners then - logger.message( - s"$warnPrefix To use the latest test-runner, upgrade Java to at least ${Constants.defaultJavaVersion}." - ) - runnerLegacyVersion - } - else testRunnerVersion + jvmTestRunnerVersion(scalaVersion, jvmVersion, logger, logLegacyWarnings = true) Seq(dep"$testRunnerOrganization::$testRunnerModuleName:$testRunnerVersion0") } else Nil diff --git a/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala b/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala index 391f6b3f61..61ccaa0c58 100644 --- a/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala +++ b/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala @@ -5,7 +5,8 @@ import coursier.maven.MavenRepository import coursier.{Dependency, LocalRepositories, Repositories} import dependency.AnyDependency -import scala.build.options.{BuildOptions, ConfigMonoid} +import scala.build.Logger +import scala.build.options.{BuildOptions, ConfigMonoid, Platform, Scope} final case class ScopedBuildInfo( sources: Seq[String] = Nil, @@ -61,6 +62,47 @@ object ScopedBuildInfo { ) .reduceLeft(_ + _) + /** Build a [[ScopedBuildInfo]] for [[scope]] and inject the JVM test-runner dependency that + * scala-cli silently adds at test time, so consumers of `export --json` see the same classpath + * scala-cli would use. + * + * Injection conditions match [[scala.build.Artifacts.apply]]: scope is Test, the scope is + * non-empty (has sources), the platform is JVM, and the build has a Scala version (i.e. is not + * Java-only). + */ + def forScope( + options: BuildOptions, + sourcePaths: Seq[String], + scope: Scope, + logger: Logger + ): ScopedBuildInfo = { + val base = apply(options, sourcePaths) + if scope == Scope.Test && sourcePaths.nonEmpty then + withJvmTestRunner(base, options, logger) + else base + } + + private def withJvmTestRunner( + base: ScopedBuildInfo, + options: BuildOptions, + logger: Logger + ): ScopedBuildInfo = { + val isJvm = options.platform.value == Platform.JVM + val scalaParams = options.scalaParams.toOption.flatten + val isScalaBuild = scalaParams.nonEmpty + if isJvm && isScalaBuild then { + val params = scalaParams.get + val dep = scala.build.Artifacts.jvmTestRunnerExportDependency( + scalaVersion = params.scalaVersion, + scalaBinaryVersion = params.scalaBinaryVersion, + jvmVersion = options.javaHome().value.version, + logger = logger + ) + base.copy(dependencies = base.dependencies :+ dep) + } + else base + } + private def scalacOptionsSettings(options: BuildOptions): ScopedBuildInfo = ScopedBuildInfo(scalacOptions = options.scalaOptions.scalacOptions.toSeq.map(_.value.value)) From 16098baa78b1202ce0641aefe3d9a11ca74066b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 8 May 2026 01:40:19 +0200 Subject: [PATCH 5/9] fmt --- .../src/main/scala/scala/build/info/ScopedBuildInfo.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala b/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala index 61ccaa0c58..2a919d4813 100644 --- a/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala +++ b/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala @@ -91,8 +91,8 @@ object ScopedBuildInfo { val scalaParams = options.scalaParams.toOption.flatten val isScalaBuild = scalaParams.nonEmpty if isJvm && isScalaBuild then { - val params = scalaParams.get - val dep = scala.build.Artifacts.jvmTestRunnerExportDependency( + val params = scalaParams.get + val dep = scala.build.Artifacts.jvmTestRunnerExportDependency( scalaVersion = params.scalaVersion, scalaBinaryVersion = params.scalaBinaryVersion, jvmVersion = options.javaHome().value.version, From 896b18fdd111e02fe901b8bfd19391503068d162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 8 May 2026 17:56:59 +0200 Subject: [PATCH 6/9] Gate test-runner injection in ScopedBuildInfo behind a flag Only inject the JVM test-runner dependency into the Test scope when generating BuildInfo for export --json. The runtime BuildInfo.scala generator (ScopedSources.buildInfo) must not inject it: scala-cli supplies the test-runner at runtime, so users' BuildInfo.Test.dependencies would otherwise contain a dep they never declared, breaking RunTestsDefault."BuildInfo fields should be reachable". --- .../scala/cli/exportCmd/JsonProjectDescriptor.scala | 8 +++++++- .../scala/scala/build/info/ScopedBuildInfo.scala | 13 ++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala index b950723242..a0bd5b3bbc 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProjectDescriptor.scala @@ -20,7 +20,13 @@ final case class JsonProjectDescriptor( val sourcePaths = sources.paths.map(_._1.toString) val inMemoryPaths = sources.inMemory.flatMap(_.originalPath.toSeq.map(_._2.toString)) - ScopedBuildInfo.forScope(options, sourcePaths ++ inMemoryPaths, scope, logger) + ScopedBuildInfo.forScope( + options, + sourcePaths ++ inMemoryPaths, + scope, + logger, + injectTestRunner = true + ) for { baseBuildInfo <- BuildInfo(optionsMain, workspace) diff --git a/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala b/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala index 2a919d4813..2e7f9b1344 100644 --- a/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala +++ b/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala @@ -62,9 +62,11 @@ object ScopedBuildInfo { ) .reduceLeft(_ + _) - /** Build a [[ScopedBuildInfo]] for [[scope]] and inject the JVM test-runner dependency that - * scala-cli silently adds at test time, so consumers of `export --json` see the same classpath - * scala-cli would use. + /** Build a [[ScopedBuildInfo]] for [[scope]]. When [[injectTestRunner]] is true, also inject the + * JVM test-runner dependency that scala-cli silently adds at test time, so consumers of + * `export --json` see the same classpath scala-cli would use. The runtime `BuildInfo.scala` + * generator must leave it false — the test-runner is supplied by scala-cli at runtime, not + * declared by the user. * * Injection conditions match [[scala.build.Artifacts.apply]]: scope is Test, the scope is * non-empty (has sources), the platform is JVM, and the build has a Scala version (i.e. is not @@ -74,10 +76,11 @@ object ScopedBuildInfo { options: BuildOptions, sourcePaths: Seq[String], scope: Scope, - logger: Logger + logger: Logger, + injectTestRunner: Boolean = false ): ScopedBuildInfo = { val base = apply(options, sourcePaths) - if scope == Scope.Test && sourcePaths.nonEmpty then + if injectTestRunner && scope == Scope.Test && sourcePaths.nonEmpty then withJvmTestRunner(base, options, logger) else base } From e9c581bf59c15816a762fc754eabe42ade0d9f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Mon, 11 May 2026 01:27:58 +0200 Subject: [PATCH 7/9] empty From 29ddfe813612ba7200d7b8e2edf8c4b435f41380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 15 May 2026 22:54:20 +0200 Subject: [PATCH 8/9] Expose per-scope injected deps in export --json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scala-cli builds the main and test scopes as two separate Build objects, each running its own Coursier resolution. Downstream tools that need to reproduce that resolution (e.g. lockfile generators) have to re-derive which direct deps scala-cli injects per scope — JVM test-runner, Scala Native runtime libs, Native test-interface, JS test-bridge — and the conditions under which it does so. The export today only surfaces the runtime-deps slice via the top-level nativeOptions, leaving consumers to guess the rest. Add a per-scope `injectedDependencies` field to ScopedBuildInfo, populated by `export --json` (always false in the runtime BuildInfo.scala generator). It mirrors what `Artifacts.apply` adds beyond user-declared deps for each scope: - Native: javalib_native + scala3lib_native/scalalib_native + nscplugin for both Main and Test - JS: scalajs-library + scalajs-compiler (Scala 2) for both Main and Test - JVM Test: test-runner_ (already in dependencies via earlier commit; moved here for consistency) - Native Test: test-interface_native_ - JS Test: scalajs-test-bridge Document the two-scope-two-resolutions model in the forScope scaladoc so consumers know each scope's resolution can produce a different transitive winner. Co-Authored-By: Claude Opus 4.7 --- .../scala/cli/exportCmd/JsonProject.scala | 1 + .../ExportJsonTestDefinitions.scala | 77 ++++++++- .../scala/build/info/ScopedBuildInfo.scala | 162 +++++++++++++++--- 3 files changed, 215 insertions(+), 25 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala index 9b118ad5c1..96ea29e6e1 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala @@ -65,6 +65,7 @@ extension (s: ScopedBuildInfo) { s.scalaCompilerPlugins.sorted(using JsonProject.ordering), s.dependencies.sorted(using JsonProject.ordering), s.compileOnlyDependencies.sorted(using JsonProject.ordering), + s.injectedDependencies.sorted(using JsonProject.ordering), s.resolvers.sorted, s.resourceDirs.sorted, s.customJarsDecls.sorted diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala index ec88c19c81..2bec277cab 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala @@ -166,6 +166,23 @@ abstract class ExportJsonTestDefinitions extends ScalaCliSuite with TestScalaVer | "version":"0.7.8" | } | ], + | "injectedDependencies": [ + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"javalib_native0.5","fullName":"javalib_native0.5_3"}, + | "version":"$nativeVersion" + | }, + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"nscplugin","fullName":"nscplugin_3.2.2"}, + | "version":"$nativeVersion" + | }, + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"scala3lib_native0.5","fullName":"scala3lib_native0.5_3"}, + | "version":"3.2.2+$nativeVersion" + | } + | ], | "resolvers": [ | "https://repo1.maven.org/maven2", | "ivy:file:.../local-repo/...", @@ -195,6 +212,28 @@ abstract class ExportJsonTestDefinitions extends ScalaCliSuite with TestScalaVer | "version": "0.7.8" | } | ], + | "injectedDependencies": [ + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"javalib_native0.5","fullName":"javalib_native0.5_3"}, + | "version":"$nativeVersion" + | }, + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"nscplugin","fullName":"nscplugin_3.2.2"}, + | "version":"$nativeVersion" + | }, + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"scala3lib_native0.5","fullName":"scala3lib_native0.5_3"}, + | "version":"3.2.2+$nativeVersion" + | }, + | { + | "groupId":"org.scala-native", + | "artifactId":{"name":"test-interface_native0.5","fullName":"test-interface_native0.5_3"}, + | "version":"$nativeVersion" + | } + | ], | "resolvers": [ | "https://oss.sonatype.org/content/repositories/snapshots", | "https://repo1.maven.org/maven2", @@ -324,7 +363,7 @@ abstract class ExportJsonTestDefinitions extends ScalaCliSuite with TestScalaVer } } - test("export json does not inject test-runner for Native target") { + test("export json does not inject JVM test-runner for Native target") { val inputs = TestInputs( os.rel / "Main.scala" -> """object Main { @@ -340,11 +379,40 @@ abstract class ExportJsonTestDefinitions extends ScalaCliSuite with TestScalaVer os.proc(TestUtil.cli, "--power", "export", "--json", ".", "--native") .call(cwd = root) val jsonContents = readJson(exportJsonProc.out.text()) + // The JVM test-runner is JVM-only; Native targets get a Scala Native + // test-interface dep instead (verified by a separate test below). expect(!jsonContents.contains("\"name\":\"test-runner\"")) expect(!jsonContents.contains("org.virtuslab.scala-cli")) } } + test("export json injects Scala Native test-interface into test scope of Native target") { + val inputs = TestInputs( + os.rel / "Main.scala" -> + """object Main { + | def main(args: Array[String]): Unit = println("hi") + |} + |""".stripMargin, + os.rel / "unit.test.scala" -> + """class MyTest { def foo() = () } + |""".stripMargin + ) + inputs.fromRoot { root => + val exportJsonProc = + os.proc(TestUtil.cli, "--power", "export", "--json", ".", "--native") + .call(cwd = root) + val jsonContents = readJson(exportJsonProc.out.text()) + val snBinary = Constants.scalaNativeVersion.split('.').take(2).mkString(".") + val expectedFullName = + s"test-interface_native${snBinary}_${Constants.scala3NextPrefix.split('.').head}" + // The test scope's injectedDependencies should include the Scala Native test-interface + // module pinned at scala-cli's bundled Scala Native version. + expect(jsonContents.contains("\"name\":\"test-interface_native" + snBinary + "\"")) + expect(jsonContents.contains(s"\"fullName\":\"$expectedFullName\"")) + expect(jsonContents.contains(s"\"version\":\"${Constants.scalaNativeVersion}\"")) + } + } + test("export json with js") { val inputs = TestInputs( os.rel / "Main.scala" -> @@ -408,6 +476,13 @@ abstract class ExportJsonTestDefinitions extends ScalaCliSuite with TestScalaVer | "version": "0.7.8" | } | ], + | "injectedDependencies": [ + | { + | "groupId":"org.scala-js", + | "artifactId":{"name":"scalajs-library","fullName":"scalajs-library_2.13"}, + | "version":"${Constants.scalaJsVersion}" + | } + | ], | "resolvers": [ | "https://repo1.maven.org/maven2", | "ivy:file:.../local-repo/...", diff --git a/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala b/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala index 2e7f9b1344..90fb02383c 100644 --- a/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala +++ b/modules/options/src/main/scala/scala/build/info/ScopedBuildInfo.scala @@ -3,7 +3,7 @@ package scala.build.info import coursier.ivy.IvyRepository import coursier.maven.MavenRepository import coursier.{Dependency, LocalRepositories, Repositories} -import dependency.AnyDependency +import dependency.* import scala.build.Logger import scala.build.options.{BuildOptions, ConfigMonoid, Platform, Scope} @@ -14,6 +14,13 @@ final case class ScopedBuildInfo( scalaCompilerPlugins: Seq[ExportDependencyFormat] = Nil, dependencies: Seq[ExportDependencyFormat] = Nil, compileOnlyDependencies: Seq[ExportDependencyFormat] = Nil, + /** Dependencies scala-cli adds to this scope's Coursier resolution beyond what the user declared + * — e.g. the JVM test-runner, Scala Native runtime libs, the Native test-interface. Populated by + * `export --json` (so consumers like packaging tools see the same effective resolution input + * scala-cli would use at test/build time); empty in the generated runtime `BuildInfo` since the + * user didn't write these and scala-cli supplies them automatically. + */ + injectedDependencies: Seq[ExportDependencyFormat] = Nil, resolvers: Seq[String] = Nil, resourceDirs: Seq[String] = Nil, customJarsDecls: Seq[String] = Nil @@ -63,14 +70,36 @@ object ScopedBuildInfo { .reduceLeft(_ + _) /** Build a [[ScopedBuildInfo]] for [[scope]]. When [[injectTestRunner]] is true, also inject the - * JVM test-runner dependency that scala-cli silently adds at test time, so consumers of - * `export --json` see the same classpath scala-cli would use. The runtime `BuildInfo.scala` - * generator must leave it false — the test-runner is supplied by scala-cli at runtime, not - * declared by the user. + * direct dependencies that scala-cli adds to this scope's Coursier resolution at `test` time, so + * consumers of `export --json` see the same effective resolution input scala-cli would use. The + * runtime `BuildInfo.scala` generator must leave it false — these deps are supplied by scala-cli + * at runtime, not declared by the user. + * + * scala-cli builds the main and test scopes as two separate `Build`s, each with its own Coursier + * resolution. This method models that: each scope's `dependencies` field is the effective + * direct-dep set for that scope's standalone resolution. When the two scopes pick conflicting + * transitive winners (e.g. a test framework pinning a higher Scala Native version than the main + * scope's `scala3lib_native`), each scope still gets its own winner — the export preserves that + * asymmetry by listing per-scope effective inputs. * - * Injection conditions match [[scala.build.Artifacts.apply]]: scope is Test, the scope is - * non-empty (has sources), the platform is JVM, and the build has a Scala version (i.e. is not - * Java-only). + * Injected items per scope (all gated on `injectTestRunner = true`, mirroring how + * `addTestRunnerDependency = true` on the shared `BuildOptions` flows into both scopes inside + * scala-cli's `test` command): + * - Native runtime deps (`javalib_native`, `scala3lib_native`/`scalalib_native`) and the + * `nscplugin` compiler plugin: both scopes, when platform is Native. + * - JS runtime deps (`scalajs-library`) and the Scala 2 `scalajs-compiler` plugin: both + * scopes, when platform is JS. + * - JVM test-runner (`org.virtuslab.scala-cli:test-runner_`): Test scope, when + * platform is JVM and the build has a Scala version. + * - Native test-interface (`org.scala-native:test-interface_native_`): + * Test scope, when platform is Native and Scala Native is at least 0.4.3. Coursier may still + * pick a higher transitive version as the test scope's winner, but the main scope's + * resolution pins scala-cli's bundled version, so an offline cache built from this export + * carries both. + * - JS test-bridge (`org.scala-js:scalajs-test-bridge`): Test scope, when platform is JS. + * + * Each injection is also gated on the relevant scope being non-empty (matching + * [[scala.build.Artifacts.apply]]'s gating). */ def forScope( options: BuildOptions, @@ -80,30 +109,115 @@ object ScopedBuildInfo { injectTestRunner: Boolean = false ): ScopedBuildInfo = { val base = apply(options, sourcePaths) - if injectTestRunner && scope == Scope.Test && sourcePaths.nonEmpty then - withJvmTestRunner(base, options, logger) + if injectTestRunner && sourcePaths.nonEmpty then + withInjectedDeps(base, options, scope, logger) else base } - private def withJvmTestRunner( + /** Inject the direct dependencies scala-cli adds to this scope's Coursier resolution at test + * time. See [[forScope]] for the per-platform/scope conditions. + */ + private def withInjectedDeps( base: ScopedBuildInfo, options: BuildOptions, + scope: Scope, logger: Logger ): ScopedBuildInfo = { - val isJvm = options.platform.value == Platform.JVM - val scalaParams = options.scalaParams.toOption.flatten - val isScalaBuild = scalaParams.nonEmpty - if isJvm && isScalaBuild then { - val params = scalaParams.get - val dep = scala.build.Artifacts.jvmTestRunnerExportDependency( - scalaVersion = params.scalaVersion, - scalaBinaryVersion = params.scalaBinaryVersion, - jvmVersion = options.javaHome().value.version, - logger = logger - ) - base.copy(dependencies = base.dependencies :+ dep) + val scalaParamsOpt = options.scalaParams.toOption.flatten + if scalaParamsOpt.isEmpty then base + else { + val scalaParams = scalaParamsOpt.get + val platform = options.platform.value + val isTest = scope == Scope.Test + val platformDeps = platform match { + case Platform.Native => nativeScopeDeps(options, scalaParamsOpt) + case Platform.JS => jsScopeDeps(options, scalaParamsOpt) + case _ => Nil + } + val testRunnerDepOpt = (platform, isTest) match { + case (Platform.JVM, true) => + Some(scala.build.Artifacts.jvmTestRunnerExportDependency( + scalaVersion = scalaParams.scalaVersion, + scalaBinaryVersion = scalaParams.scalaBinaryVersion, + jvmVersion = options.javaHome().value.version, + logger = logger + )) + case (Platform.Native, true) => + nativeTestInterfaceExportDependency(options, scalaParamsOpt) + case (Platform.JS, true) => + Some(jsTestBridgeExportDependency(options)) + case _ => None + } + val injected = platformDeps ++ testRunnerDepOpt.toSeq + if injected.isEmpty then base + else base.copy(injectedDependencies = base.injectedDependencies ++ injected) } - else base + } + + /** Native runtime deps + compiler plugin that scala-cli adds to every Native scope's resolution + * (both Main and Test). These come from [[ScalaNativeOptions.nativeDependencies]] and + * [[ScalaNativeOptions.compilerPlugins]], which feed `BuildOptions.defaultDependencies`. + */ + private def nativeScopeDeps( + options: BuildOptions, + scalaParamsOpt: Option[dependency.ScalaParameters] + ): Seq[ExportDependencyFormat] = { + val nativeOptions = options.scalaNativeOptions + val sv = scalaParamsOpt.map(_.scalaVersion) + .getOrElse(scala.build.internal.Constants.defaultScalaVersion) + val runtime = nativeOptions.nativeDependencies(sv) + val compilerPlugins = nativeOptions.compilerPlugins + (runtime ++ compilerPlugins).map(ExportDependencyFormat(_, scalaParamsOpt)) + } + + /** JS runtime deps + compiler plugin (Scala 2 only) that scala-cli adds to every JS scope's + * resolution. Counterpart of [[nativeScopeDeps]] for the JS platform. + */ + private def jsScopeDeps( + options: BuildOptions, + scalaParamsOpt: Option[dependency.ScalaParameters] + ): Seq[ExportDependencyFormat] = { + val jsOptions = options.scalaJsOptions + val sv = scalaParamsOpt.map(_.scalaVersion) + .getOrElse(scala.build.internal.Constants.defaultScalaVersion) + val runtime = jsOptions.jsDependencies(sv) + val compilerPlugins = jsOptions.compilerPlugins(sv) + (runtime ++ compilerPlugins).map(ExportDependencyFormat(_, scalaParamsOpt)) + } + + /** Mirrors [[scala.build.options.BuildOptions.addNativeTestInterface]]: returns the + * `test-interface_native_:` dep that scala-cli's test-time + * resolution injects, when Scala Native is at least 0.4.3 (the version that started shipping a + * separate `test-interface` artifact). + */ + private def nativeTestInterfaceExportDependency( + options: BuildOptions, + scalaParamsOpt: Option[dependency.ScalaParameters] + ): Option[ExportDependencyFormat] = { + val snVersion = options.scalaNativeOptions.finalVersion + val minVer = coursier.core.Version("0.4.3") + if minVer.compareTo(coursier.core.Version(snVersion)) <= 0 then + Some(ExportDependencyFormat( + dep"org.scala-native::test-interface::$snVersion", + scalaParamsOpt + )) + else None + } + + /** Mirrors the JS test-bridge injection in [[scala.build.Artifacts.apply]]: the artifact id + * differs between Scala 2.x (cross-versioned with the project's Scala binary) and Scala 3 + * (always `scalajs-test-bridge_2.13`). + */ + private def jsTestBridgeExportDependency( + options: BuildOptions + ): ExportDependencyFormat = { + val scalaParams = options.scalaParams.toOption.flatten + val sv = scalaParams.map(_.scalaVersion).getOrElse("") + val jsVersion = options.scalaJsOptions.finalVersion + val dep0 = + if sv.startsWith("2.") then dep"org.scala-js::scalajs-test-bridge:$jsVersion" + else dep"org.scala-js:scalajs-test-bridge_2.13:$jsVersion" + ExportDependencyFormat(dep0, scalaParams) } private def scalacOptionsSettings(options: BuildOptions): ScopedBuildInfo = From 35ee63da7c7e21c35eeef04b934331262eea0a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 16 May 2026 00:26:29 +0200 Subject: [PATCH 9/9] Drop redundant nativeOptions.compilerPlugins/runtimeDependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These fields were introduced in "Include Native tools and deps in export --json" to surface the Scala Native runtime libs and `nscplugin` for downstream consumers. The follow-up "Expose per-scope injected deps in export --json" now reports the same artifacts per scope under `scopes..injectedDependencies`, making the top-level copy redundant. Drop the two fields from `NativeOptionsInfo` and stop populating them in `BuildInfo.scalaNativeSettings`. `toolingDependencies` stays — `scala-native-cli` is shared across scopes and doesn't fit the per-scope `injectedDependencies` model. Co-Authored-By: Claude Opus 4.7 --- .../scala/cli/exportCmd/JsonProject.scala | 2 -- .../ExportJsonTestDefinitions.scala | 19 ------------------- .../scala/scala/build/info/BuildInfo.scala | 18 ++---------------- 3 files changed, 2 insertions(+), 37 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala index 96ea29e6e1..3e34de5bed 100644 --- a/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala +++ b/modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala @@ -52,8 +52,6 @@ final case class JsonProject(buildInfo: BuildInfo) extends Project { extension (n: NativeOptionsInfo) { def sorted(using ord: Ordering[String]) = n.copy( - compilerPlugins = n.compilerPlugins.sorted(using JsonProject.ordering), - runtimeDependencies = n.runtimeDependencies.sorted(using JsonProject.ordering), toolingDependencies = n.toolingDependencies.sorted(using JsonProject.ordering) ) } diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala index 2bec277cab..92ea5f9661 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportJsonTestDefinitions.scala @@ -115,25 +115,6 @@ abstract class ExportJsonTestDefinitions extends ScalaCliSuite with TestScalaVer |"scalaNativeVersion":"$nativeVersion", |"nativeOptions": { | "scalaNativeVersion":"$nativeVersion", - | "compilerPlugins": [ - | { - | "groupId":"org.scala-native", - | "artifactId":{"name":"nscplugin","fullName":"nscplugin_3.2.2"}, - | "version":"$nativeVersion" - | } - | ], - | "runtimeDependencies": [ - | { - | "groupId":"org.scala-native", - | "artifactId":{"name":"javalib_native0.5","fullName":"javalib_native0.5_3"}, - | "version":"$nativeVersion" - | }, - | { - | "groupId":"org.scala-native", - | "artifactId":{"name":"scala3lib_native0.5","fullName":"scala3lib_native0.5_3"}, - | "version":"3.2.2+$nativeVersion" - | } - | ], | "toolingDependencies": [ | { | "groupId":"org.scala-native", diff --git a/modules/options/src/main/scala/scala/build/info/BuildInfo.scala b/modules/options/src/main/scala/scala/build/info/BuildInfo.scala index e430f1ce6b..c3fd1cbc86 100644 --- a/modules/options/src/main/scala/scala/build/info/BuildInfo.scala +++ b/modules/options/src/main/scala/scala/build/info/BuildInfo.scala @@ -8,8 +8,6 @@ import scala.build.options.* final case class NativeOptionsInfo( scalaNativeVersion: String, - compilerPlugins: Seq[ExportDependencyFormat] = Nil, - runtimeDependencies: Seq[ExportDependencyFormat] = Nil, toolingDependencies: Seq[ExportDependencyFormat] = Nil ) @@ -161,18 +159,8 @@ object BuildInfo { } private def scalaNativeSettings(options: BuildOptions): BuildInfo = { - val nativeOptions = options.scalaNativeOptions - val nativeVersion = nativeOptions.finalVersion - val scalaParamsOpt = options.scalaParams.getOrElse(None) - val sv = scalaParamsOpt.map(_.scalaVersion) - .orElse(options.scalaOptions.defaultScalaVersion) - .getOrElse(Constants.defaultScalaVersion) - - val runtimeDeps = nativeOptions.nativeDependencies(sv) - .map(ExportDependencyFormat(_, scalaParamsOpt)) - val compilerPluginDeps = nativeOptions.compilerPlugins - .map(ExportDependencyFormat(_, scalaParamsOpt)) - val toolingDeps = Seq(ExportDependencyFormat( + val nativeVersion = options.scalaNativeOptions.finalVersion + val toolingDeps = Seq(ExportDependencyFormat( "org.scala-native", ArtifactId("scala-native-cli", "scala-native-cli_2.12"), nativeVersion @@ -183,8 +171,6 @@ object BuildInfo { scalaNativeVersion = Some(nativeVersion), nativeOptions = Some(NativeOptionsInfo( scalaNativeVersion = nativeVersion, - compilerPlugins = compilerPluginDeps, - runtimeDependencies = runtimeDeps, toolingDependencies = toolingDeps )) )