Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions modules/build/src/main/scala/scala/build/ScopedSources.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class ScalaCliCommands(
new HelpCmd(help),
installcompletions.InstallCompletions,
installhome.InstallHome,
listtargets.ListTargets,
`new`.New,
repl.Repl,
package0.Package,
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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
}
12 changes: 10 additions & 2 deletions modules/cli/src/main/scala/scala/cli/exportCmd/JsonProject.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
)

Expand Down Expand Up @@ -49,13 +50,20 @@ final case class JsonProject(buildInfo: BuildInfo) extends Project {
}
}

extension (n: NativeOptionsInfo) {
def sorted(using ord: Ordering[String]) = n.copy(
toolingDependencies = n.toolingDependencies.sorted(using JsonProject.ordering)
)
}

extension (s: ScopedBuildInfo) {
def sorted(using ord: Ordering[String]) = s.copy(
s.sources.sorted,
s.scalacOptions.sorted,
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,22 @@ 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,
injectTestRunner = true
)

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))
Expand Down
Loading
Loading