diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala
index ac72d747fb..24f7fb9ce6 100644
--- a/modules/build/src/main/scala/scala/build/Build.scala
+++ b/modules/build/src/main/scala/scala/build/Build.scala
@@ -32,6 +32,7 @@ trait Build {
def scope: Scope
def outputOpt: Option[os.Path]
def success: Boolean
+ def cancelled: Boolean
def diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]]
def successfulOpt: Option[Build.Successful]
@@ -54,6 +55,7 @@ object Build {
logger: Logger
) extends Build {
def success: Boolean = true
+ def cancelled: Boolean = false
def successfulOpt: Some[this.type] = Some(this)
def outputOpt: Some[os.Path] = Some(output)
def dependencyClassPath: Seq[os.Path] = sources.resourceDirs ++ artifacts.classPath
@@ -215,9 +217,11 @@ object Build {
project: Project,
diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]]
) extends Build {
- def success: Boolean = false
- def successfulOpt: None.type = None
- def outputOpt: None.type = None
+ def success: Boolean = false
+
+ override def cancelled: Boolean = false
+ def successfulOpt: None.type = None
+ def outputOpt: None.type = None
}
final case class Cancelled(
@@ -227,6 +231,7 @@ object Build {
reason: String
) extends Build {
def success: Boolean = false
+ def cancelled: Boolean = true
def successfulOpt: None.type = None
def outputOpt: None.type = None
def diagnostics: None.type = None
diff --git a/modules/build/src/test/scala/scala/build/tests/TestInputs.scala b/modules/build/src/test/scala/scala/build/tests/TestInputs.scala
index 3923ac58ec..2106f99d95 100644
--- a/modules/build/src/test/scala/scala/build/tests/TestInputs.scala
+++ b/modules/build/src/test/scala/scala/build/tests/TestInputs.scala
@@ -66,7 +66,7 @@ final case class TestInputs(
buildThreads: BuildThreads, // actually only used when bloopConfigOpt is non-empty
bloopConfigOpt: Option[BloopRifleConfig],
fromDirectory: Boolean = false
- )(f: (os.Path, Inputs, Build) => T) =
+ )(f: (os.Path, Inputs, Build) => T): T =
withBuild(options, buildThreads, bloopConfigOpt, fromDirectory)((p, i, maybeBuild) =>
maybeBuild match {
case Left(e) => throw e
@@ -74,6 +74,19 @@ final case class TestInputs(
}
)
+ def withLoadedBuilds[T](
+ options: BuildOptions,
+ buildThreads: BuildThreads, // actually only used when bloopConfigOpt is non-empty
+ bloopConfigOpt: Option[BloopRifleConfig],
+ fromDirectory: Boolean = false
+ )(f: (os.Path, Inputs, Builds) => T) =
+ withBuilds(options, buildThreads, bloopConfigOpt, fromDirectory)((p, i, builds) =>
+ builds match {
+ case Left(e) => throw e
+ case Right(b) => f(p, i, b)
+ }
+ )
+
def withBuilds[T](
options: BuildOptions,
buildThreads: BuildThreads, // actually only used when bloopConfigOpt is non-empty
diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala
index cd2fe6cb74..e785d3b1a2 100644
--- a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala
+++ b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala
@@ -5,6 +5,7 @@ import caseapp.*
import caseapp.core.help.HelpFormat
import coursier.launcher.*
import dependency.*
+import os.Path
import packager.config.*
import packager.deb.DebianPackage
import packager.docker.DockerPackage
@@ -316,7 +317,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
value(bootstrap(build, destPath, value(mainClass), () => alreadyExistsCheck(), logger))
destPath
case PackageType.LibraryJar =>
- val libraryJar = Library.libraryJar(build)
+ val libraryJar = Library.libraryJar(Seq(build))
value(alreadyExistsCheck())
if (force) os.copy.over(libraryJar, destPath, createFolders = true)
else os.copy(libraryJar, destPath, createFolders = true)
@@ -337,7 +338,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
case a: PackageType.Assembly =>
value {
assembly(
- build,
+ Seq(build),
destPath,
a.mainClassInManifest match {
case None =>
@@ -367,7 +368,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
case PackageType.Spark =>
value {
assembly(
- build,
+ Seq(build),
destPath,
mainClassOpt,
// The Spark modules are assumed to be already on the class path,
@@ -393,7 +394,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
case _ => None
val cachedDest = value(buildNative(
- build = build,
+ builds = Seq(build),
mainClass = mainClassO,
targetType = tpe,
destPath = Some(destPath),
@@ -535,10 +536,10 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
val dest = workDir / "doc.jar"
val cacheData =
CachedBinary.getCacheData(
- build,
- extraArgs.toList,
- dest,
- workDir
+ builds = Seq(build),
+ config = extraArgs.toList,
+ dest = dest,
+ workDir = workDir
)
if (cacheData.changed) {
@@ -550,7 +551,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
outputStream = os.write.outputStream(dest, createFolders = true)
Library.writeLibraryJarTo(
outputStream,
- build,
+ Seq(build),
hasActualManifest = false,
contentDirOverride = Some(contentDir)
)
@@ -667,7 +668,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
case Platform.Native =>
val dest =
value(buildNative(
- build = build,
+ builds = Seq(build),
mainClass = Some(mainClass),
targetType = PackageType.Native.Application,
destPath = None,
@@ -697,7 +698,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
isFullOpt <- build.options.scalaJsOptions.fullOpt
linkerConfig = build.options.scalaJsOptions.linkerConfig(logger)
linkResult <- linkJs(
- build,
+ Seq(build),
destPath,
mainClass,
addTestInitializer = false,
@@ -807,38 +808,34 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
* the whole dependency graph.
*/
def providedFiles(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
provided: Seq[dependency.AnyModule],
logger: Logger
): Either[BuildException, Seq[os.Path]] = either {
logger.debug(s"${provided.length} provided dependencies")
- val res = build.artifacts.resolution.getOrElse {
+ val res = builds.map(_.artifacts.resolution.getOrElse {
sys.error("Internal error: expected resolution to have been kept")
- }
- val modules = value {
+ })
+ val modules: Seq[coursier.Module] = value {
provided
- .map(_.toCs(build.scalaParams))
+ .map(_.toCs(builds.head.scalaParams)) // Scala params should be the same for all scopes
.sequence
.left.map(CompositeBuildException(_))
}
val modulesSet = modules.toSet
val providedDeps = res
- .dependencyArtifacts
- .map(_._1)
+ .flatMap(_.dependencyArtifacts.map(_._1))
.filter(dep => modulesSet.contains(dep.module))
- val providedRes = res.subset(providedDeps)
- val fileMap = build.artifacts.detailedRuntimeArtifacts
- .map {
- case (_, _, artifact, path) =>
- artifact -> path
- }
+ val providedRes = res.map(_.subset(providedDeps))
+ val fileMap = builds.flatMap(_.artifacts.detailedRuntimeArtifacts).distinct
+ .map { case (_, _, artifact, path) => artifact -> path }
.toMap
- val providedFiles = coursier.Artifacts.artifacts(providedRes, Set.empty, None, None, true)
+ val providedFiles = providedRes
+ .flatMap(r => coursier.Artifacts.artifacts(r, Set.empty, None, None, true))
+ .distinct
.map(_._3)
- .map { a =>
- fileMap.getOrElse(a, sys.error(s"should not happen (missing: $a)"))
- }
+ .map(a => fileMap.getOrElse(a, sys.error(s"should not happen (missing: $a)")))
logger.debug {
val it = Iterator(s"${providedFiles.size} provided JAR(s)") ++
providedFiles.toVector.map(_.toString).sorted.iterator.map(f => s" $f")
@@ -848,7 +845,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
}
def assembly(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
destPath: os.Path,
mainClassOpt: Option[String],
extraProvided: Seq[dependency.AnyModule],
@@ -856,39 +853,44 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
alreadyExistsCheck: () => Either[BuildException, Unit],
logger: Logger
): Either[BuildException, Unit] = either {
- val compiledClasses = os.walk(build.output).filter(os.isFile(_))
- val (extraClasseFolders, extraJars) =
- build.options.classPathOptions.extraClassPath.partition(os.isDir(_))
- val extraClasses = extraClasseFolders.flatMap(os.walk(_)).filter(os.isFile(_))
-
- val byteCodeZipEntries = (compiledClasses ++ extraClasses).map { path =>
- val name = path.relativeTo(build.output).toString
- val content = os.read.bytes(path)
- val lastModified = os.mtime(path)
- val ent = new ZipEntry(name)
- ent.setLastModifiedTime(FileTime.fromMillis(lastModified))
- ent.setSize(content.length)
- (ent, content)
- }
+ val compiledClassesByOutputDir: Seq[(Path, Path)] =
+ builds.flatMap(build =>
+ os.walk(build.output).filter(os.isFile(_)).map(build.output -> _)
+ ).distinct
+ val (extraClassesFolders, extraJars) =
+ builds.flatMap(_.options.classPathOptions.extraClassPath).partition(os.isDir(_))
+ val extraClassesByDefaultOutputDir =
+ extraClassesFolders.flatMap(os.walk(_)).filter(os.isFile(_)).map(builds.head.output -> _)
+
+ val byteCodeZipEntries =
+ (compiledClassesByOutputDir ++ extraClassesByDefaultOutputDir).map { (outputDir, path) =>
+ val name = path.relativeTo(outputDir).toString
+ val content = os.read.bytes(path)
+ val lastModified = os.mtime(path)
+ val ent = new ZipEntry(name)
+ ent.setLastModifiedTime(FileTime.fromMillis(lastModified))
+ ent.setSize(content.length)
+ (ent, content)
+ }
- val provided = build.options.notForBloopOptions.packageOptions.provided ++ extraProvided
- val allJars = build.artifacts.runtimeArtifacts.map(_._2) ++ extraJars.filter(os.exists(_))
+ val provided = builds.head.options.notForBloopOptions.packageOptions.provided ++ extraProvided
+ val allJars =
+ builds.flatMap(_.artifacts.runtimeArtifacts.map(_._2)) ++ extraJars.filter(os.exists(_))
val jars =
if (provided.isEmpty) allJars
else {
- val providedFilesSet = value(providedFiles(build, provided, logger)).toSet
+ val providedFilesSet = value(providedFiles(builds, provided, logger)).toSet
allJars.filterNot(providedFilesSet.contains)
}
val preambleOpt =
- if (withPreamble)
+ if withPreamble then
Some {
Preamble()
.withOsKind(Properties.isWin)
.callsItself(Properties.isWin)
}
- else
- None
+ else None
val params = Parameters.Assembly()
.withExtraZipEntries(byteCodeZipEntries)
.withFiles(jars.map(_.toIO))
@@ -947,7 +949,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
}
def linkJs(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
dest: os.Path,
mainClassOpt: Option[String],
addTestInitializer: Boolean,
@@ -957,19 +959,19 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
logger: Logger,
scratchDirOpt: Option[os.Path] = None
): Either[BuildException, os.Path] = {
- val mainJar = Library.libraryJar(build)
- val classPath = mainJar +: build.artifacts.classPath
+ val jar = Library.libraryJar(builds)
+ val classPath = Seq(jar) ++ builds.flatMap(_.artifacts.classPath)
val input = ScalaJsLinker.LinkJSInput(
- options = build.options.notForBloopOptions.scalaJsLinkerOptions,
+ options = builds.head.options.notForBloopOptions.scalaJsLinkerOptions,
javaCommand =
- build.options.javaHome().value.javaCommand, // FIXME Allow users to use another JVM here?
+ builds.head.options.javaHome().value.javaCommand, // FIXME Allow users to use another JVM here?
classPath = classPath,
mainClassOrNull = mainClassOpt.orNull,
addTestInitializer = addTestInitializer,
config = config,
fullOpt = fullOpt,
noOpt = noOpt,
- scalaJsVersion = build.options.scalaJsOptions.finalVersion
+ scalaJsVersion = builds.head.options.scalaJsOptions.finalVersion
)
val linkingDir = LinkingDir.getOrCreate(input, scratchDirOpt)
@@ -980,8 +982,8 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
input,
linkingDir,
logger,
- build.options.finalCache,
- build.options.archiveCache
+ builds.head.options.finalCache,
+ builds.head.options.archiveCache
)
}
val relMainJs = os.rel / "main.js"
@@ -1012,9 +1014,9 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
}
else {
os.copy(mainJs, dest, replaceExisting = true)
- if (build.options.scalaJsOptions.emitSourceMaps && os.exists(sourceMapJs)) {
+ if (builds.head.options.scalaJsOptions.emitSourceMaps && os.exists(sourceMapJs)) {
val sourceMapDest =
- build.options.scalaJsOptions.sourceMapsDest.getOrElse(os.Path(s"$dest.map"))
+ builds.head.options.scalaJsOptions.sourceMapsDest.getOrElse(os.Path(s"$dest.map"))
val updatedMainJs = ScalaJsLinker.updateSourceMappingURL(dest)
os.write.over(dest, updatedMainJs)
os.copy(sourceMapJs, sourceMapDest, replaceExisting = true)
@@ -1031,28 +1033,29 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
}
def buildNative(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
mainClass: Option[String], // when building a static/dynamic library, we don't need a main class
targetType: PackageType.Native,
destPath: Option[os.Path],
logger: Logger
): Either[BuildException, os.Path] = either {
- val dest = build.inputs.nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
+ val dest = builds.head.inputs.nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
val cliOptions =
- build.options.scalaNativeOptions.configCliOptions(build.sources.resourceDirs.nonEmpty)
+ builds.head.options.scalaNativeOptions.configCliOptions(builds.exists(
+ _.sources.resourceDirs.nonEmpty
+ ))
- val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false)
+ val setupPython = builds.head.options.notForBloopOptions.doSetupPython.getOrElse(false)
val pythonLdFlags =
- if (setupPython)
+ if setupPython then
value {
val python = Python()
val flagsOrError = python.ldflags
logger.debug(s"Python ldflags: $flagsOrError")
flagsOrError.orPythonDetectionError
}
- else
- Nil
+ else Nil
val pythonCliOptions = pythonLdFlags.flatMap(f => Seq("--linking-option", f)).toList
val libraryLinkingOptions: Seq[String] =
@@ -1076,21 +1079,21 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
libraryLinkingOptions ++
mainClass.toSeq.flatMap(m => Seq("--main", m))
- val nativeWorkDir = build.inputs.nativeWorkDir
+ val nativeWorkDir = builds.head.inputs.nativeWorkDir
os.makeDir.all(nativeWorkDir)
val cacheData =
CachedBinary.getCacheData(
- build,
+ builds,
allCliOptions,
dest,
nativeWorkDir
)
if (cacheData.changed) {
- NativeResourceMapper.copyCFilesToScalaNativeDir(build, nativeWorkDir)
- val mainJar = Library.libraryJar(build)
- val classpath = mainJar.toString +: build.artifacts.classPath.map(_.toString)
+ builds.foreach(build => NativeResourceMapper.copyCFilesToScalaNativeDir(build, nativeWorkDir))
+ val jar = Library.libraryJar(builds)
+ val classpath = (Seq(jar) ++ builds.flatMap(_.artifacts.classPath)).map(_.toString).distinct
val args =
allCliOptions ++
logger.scalaNativeCliInternalLoggerOptions ++
@@ -1101,7 +1104,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
nativeWorkDir.toString()
) ++ classpath
- val scalaNativeCli = build.artifacts.scalaOpt
+ val scalaNativeCli = builds.flatMap(_.artifacts.scalaOpt).headOption
.getOrElse {
sys.error("Expected Scala artifacts to be fetched")
}
@@ -1109,17 +1112,16 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
val exitCode =
Runner.runJvm(
- build.options.javaHome().value.javaCommand,
- build.options.javaOptions.javaOpts.toSeq.map(_.value.value),
+ builds.head.options.javaHome().value.javaCommand,
+ builds.head.options.javaOptions.javaOpts.toSeq.map(_.value.value),
scalaNativeCli,
"scala.scalanative.cli.ScalaNativeLd",
args,
logger
).waitFor()
- if (exitCode == 0)
+ if exitCode == 0 then
CachedBinary.updateProjectAndOutputSha(dest, nativeWorkDir, cacheData.projectSha)
- else
- throw new ScalaNativeBuildError
+ else throw new ScalaNativeBuildError
}
dest
diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala
index ddbd009bfb..528b9a8d69 100644
--- a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala
+++ b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala
@@ -565,7 +565,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
case Right(cls) => Some(cls)
}
}
- val libraryJar = Library.libraryJar(build, mainClassOpt)
+ val libraryJar = Library.libraryJar(Seq(build), mainClassOpt)
val dest = workingDir / org / s"$moduleName-$ver.jar"
os.copy.over(libraryJar, dest, createFolders = true)
dest
diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala
index e101a89cdb..75ac208ff5 100644
--- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala
+++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala
@@ -176,7 +176,7 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers {
buildOptions = builds.head.options,
allArtifacts = builds.map(_.artifacts),
mainJarsOrClassDirs =
- if (asJar) builds.map(Library.libraryJar(_)) else builds.map(_.output),
+ if asJar then Seq(Library.libraryJar(builds)) else builds.map(_.output),
allowExit = allowExit,
runMode = runMode,
successfulBuilds = builds
diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala
index 68c2b2a86a..676ceddcf3 100644
--- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala
+++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala
@@ -10,13 +10,14 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicReference
import scala.build.EitherCps.{either, value}
+import scala.build.Ops.*
import scala.build.*
-import scala.build.errors.BuildException
+import scala.build.errors.{BuildException, CompositeBuildException}
import scala.build.input.{Inputs, ScalaCliInvokeData, SubCommand}
import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig}
import scala.build.internals.ConsoleUtils.ScalaCliConsole
import scala.build.internals.EnvVar
-import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, ScalacOpt}
+import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, ScalacOpt, Scope}
import scala.cli.CurrentParams
import scala.cli.commands.package0.Package
import scala.cli.commands.publish.ConfigUtil.*
@@ -152,13 +153,13 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
val compilerMaker = options.shared.compilerMaker(threads)
def maybeRun(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
allowTerminate: Boolean,
runMode: RunMode,
showCommand: Boolean,
scratchDirOpt: Option[os.Path]
- ): Either[BuildException, Option[(Process, CompletableFuture[_])]] = either {
- val potentialMainClasses = build.foundMainClasses()
+ ): Either[BuildException, Option[(Process, CompletableFuture[?])]] = either {
+ val potentialMainClasses = builds.flatMap(_.foundMainClasses()).distinct
if (options.sharedRun.mainClass.mainClassLs.contains(true))
value {
options.sharedRun.mainClass
@@ -168,11 +169,11 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
else {
val processOrCommand = value {
maybeRunOnce(
- build,
+ builds,
programArgs,
logger,
allowExecve = allowTerminate,
- jvmRunner = build.artifacts.hasJvmRunner,
+ jvmRunner = builds.exists(_.artifacts.hasJvmRunner),
potentialMainClasses,
runMode,
showCommand,
@@ -256,7 +257,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
docCompilerMakerOpt = None,
logger = logger,
crossBuilds = cross,
- buildTests = false,
+ buildTests = options.sharedRun.scope.test,
partial = None,
actionableDiagnostics = actionableDiagnostics,
postAction = () =>
@@ -267,15 +268,16 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
onExitProcess.cancel(true)
ProcUtil.interruptProcess(process, logger)
}
- res.orReport(logger).map(_.main).foreach {
- case s: Build.Successful =>
+ res.orReport(logger).map(_.builds).foreach {
+ case b if b.forall(_.success) =>
+ val successfulBuilds = b.collect { case s: Build.Successful => s }
for ((proc, _) <- processOpt.get() if proc.isAlive)
// If the process doesn't exit, send SIGKILL
ProcUtil.forceKillProcess(proc, logger)
shouldReadInput.set(false)
mainThreadOpt.get().foreach(_.interrupt())
val maybeProcess = maybeRun(
- s,
+ successfulBuilds,
allowTerminate = false,
runMode = runMode(options),
showCommand = options.sharedRun.command,
@@ -292,7 +294,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
}
(proc, onExit)
}
- s.copyOutput(options.shared)
+ successfulBuilds.foreach(_.copyOutput(options.shared))
if options.sharedRun.watch.restart then processOpt.set(maybeProcess)
else {
for ((proc, onExit) <- maybeProcess)
@@ -300,8 +302,9 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
shouldReadInput.set(true)
mainThreadOpt.get().foreach(_.interrupt())
}
- case _: Build.Failed =>
+ case b if b.exists(bb => !bb.success && !bb.cancelled) =>
System.err.println("Compilation failed")
+ case _ => ()
}
}
mainThreadOpt.set(Some(Thread.currentThread()))
@@ -319,25 +322,25 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
watcher.dispose()
}
}
- else {
- val builds =
- Build.build(
- inputs,
- initialBuildOptions,
- compilerMaker,
- None,
- logger,
- crossBuilds = cross,
- buildTests = false,
- partial = None,
- actionableDiagnostics = actionableDiagnostics
- )
- .orExit(logger)
- builds.main match {
- case s: Build.Successful =>
- s.copyOutput(options.shared)
+ else
+ Build.build(
+ inputs,
+ initialBuildOptions,
+ compilerMaker,
+ None,
+ logger,
+ crossBuilds = cross,
+ buildTests = options.sharedRun.scope.test,
+ partial = None,
+ actionableDiagnostics = actionableDiagnostics
+ )
+ .orExit(logger)
+ .builds match {
+ case b if b.forall(_.success) =>
+ val successfulBuilds = b.collect { case s: Build.Successful => s }
+ successfulBuilds.foreach(_.copyOutput(options.shared))
val res = maybeRun(
- s,
+ successfulBuilds,
allowTerminate = true,
runMode = runMode(options),
showCommand = options.sharedRun.command,
@@ -346,15 +349,15 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
.orExit(logger)
for ((process, onExit) <- res)
ProcUtil.waitForProcess(process, onExit)
- case _: Build.Failed =>
+ case b if b.exists(bb => !bb.success && !bb.cancelled) =>
System.err.println("Compilation failed")
sys.exit(1)
+ case _ => ()
}
- }
}
private def maybeRunOnce(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
args: Seq[String],
logger: Logger,
allowExecve: Boolean,
@@ -366,23 +369,42 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
asJar: Boolean
): Either[BuildException, Either[Seq[String], (Process, Option[() => Unit])]] = either {
- val mainClassOpt = build.options.mainClass.filter(_.nonEmpty) // trim it too?
+ val mainClassOpt = builds.head.options.mainClass.filter(_.nonEmpty) // trim it too?
.orElse {
- if build.options.jmhOptions.enableJmh.contains(true) && !build.options.jmhOptions.canRunJmh
+ if builds.head.options.jmhOptions.enableJmh.contains(
+ true
+ ) && !builds.head.options.jmhOptions.canRunJmh
then Some("org.openjdk.jmh.Main")
else None
}
- val mainClass = mainClassOpt match {
+ val mainClass: String = mainClassOpt match {
case Some(cls) => cls
- case None => value(build.retainedMainClass(logger, mainClasses = potentialMainClasses))
+ case None =>
+ val retainedMainClassesByScope: Map[Scope, String] = value {
+ builds
+ .map { build =>
+ build.retainedMainClass(logger, mainClasses = potentialMainClasses)
+ .map(mainClass => build.scope -> mainClass)
+ }
+ .sequence
+ .left
+ .map(CompositeBuildException(_))
+ .map(_.toMap)
+ }
+ if retainedMainClassesByScope.size == 1 then retainedMainClassesByScope.head._2
+ else
+ retainedMainClassesByScope
+ .get(Scope.Main)
+ .orElse(retainedMainClassesByScope.get(Scope.Test))
+ .get
}
- val verbosity = build.options.internal.verbosity.getOrElse(0).toString
+ val verbosity = builds.head.options.internal.verbosity.getOrElse(0).toString
val (finalMainClass, finalArgs) =
if (jvmRunner) (Constants.runnerMainClass, mainClass +: verbosity +: args)
else (mainClass, args)
val res = runOnce(
- build,
+ builds,
finalMainClass,
finalArgs,
logger,
@@ -417,7 +439,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
}
private def runOnce(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
mainClass: String,
args: Seq[String],
logger: Logger,
@@ -427,13 +449,12 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
scratchDirOpt: Option[os.Path],
asJar: Boolean
): Either[BuildException, Either[Seq[String], (Process, Option[() => Unit])]] = either {
-
- build.options.platform.value match {
+ builds.head.options.platform.value match {
case Platform.JS =>
val esModule =
- build.options.scalaJsOptions.moduleKindStr.exists(m => m == "es" || m == "esmodule")
+ builds.head.options.scalaJsOptions.moduleKindStr.exists(m => m == "es" || m == "esmodule")
- val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger)
+ val linkerConfig = builds.head.options.scalaJsOptions.linkerConfig(logger)
val jsDest = {
val delete = scratchDirOpt.isEmpty
scratchDirOpt.foreach(os.makeDir.all(_))
@@ -446,17 +467,17 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
}
val res =
Package.linkJs(
- build,
+ builds,
jsDest,
Some(mainClass),
addTestInitializer = false,
linkerConfig,
- value(build.options.scalaJsOptions.fullOpt),
- build.options.scalaJsOptions.noOpt.getOrElse(false),
+ value(builds.head.options.scalaJsOptions.fullOpt),
+ builds.head.options.scalaJsOptions.noOpt.getOrElse(false),
logger,
scratchDirOpt
).map { outputPath =>
- val jsDom = build.options.scalaJsOptions.dom.getOrElse(false)
+ val jsDom = builds.head.options.scalaJsOptions.dom.getOrElse(false)
if (showCommand)
Left(Runner.jsCommand(outputPath.toIO, args, jsDom = jsDom))
else {
@@ -467,7 +488,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
logger,
allowExecve = allowExecve,
jsDom = jsDom,
- sourceMap = build.options.scalaJsOptions.emitSourceMaps,
+ sourceMap = builds.head.options.scalaJsOptions.emitSourceMaps,
esModule = esModule
)
}
@@ -477,7 +498,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
}
value(res)
case Platform.Native =>
- val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false)
+ val setupPython = builds.head.options.notForBloopOptions.doSetupPython.getOrElse(false)
val (pythonExecutable, pythonLibraryPaths, pythonExtraEnv) =
if (setupPython) {
val (exec, libPaths) = value {
@@ -492,7 +513,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
// Putting the workspace in PYTHONPATH, see
// https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174
// for context.
- (exec, libPaths, pythonPathEnv(build.inputs.workspace))
+ (exec, libPaths, pythonPathEnv(builds.head.inputs.workspace))
}
else
(None, Nil, Map())
@@ -522,7 +543,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
pythonExecutable.fold(Map.empty)(py => Map("SCALAPY_PYTHON_PROGRAMNAME" -> py))
val extraEnv = libraryPathsEnv ++ programNameEnv ++ pythonExtraEnv
val maybeResult = withNativeLauncher(
- build,
+ builds,
mainClass,
logger
) { launcher =>
@@ -547,8 +568,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
case Platform.JVM =>
runMode match {
case RunMode.Default =>
- val baseJavaProps = build.options.javaOptions.javaOpts.toSeq.map(_.value.value)
- val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false)
+ val baseJavaProps = builds.head.options.javaOptions.javaOpts.toSeq.map(_.value.value)
+ val setupPython = builds.head.options.notForBloopOptions.doSetupPython.getOrElse(false)
val (pythonJavaProps, pythonExtraEnv) =
if (setupPython) {
val scalapyProps = value {
@@ -563,35 +584,35 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
// Putting the workspace in PYTHONPATH, see
// https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174
// for context.
- (props, pythonPathEnv(build.inputs.workspace))
+ (props, pythonPathEnv(builds.head.inputs.workspace))
}
else
(Nil, Map.empty[String, String])
val allJavaOpts = pythonJavaProps ++ baseJavaProps
- if (showCommand) {
- val command = Runner.jvmCommand(
- build.options.javaHome().value.javaCommand,
- allJavaOpts,
- build.fullClassPathMaybeAsJar(asJar),
- mainClass,
- args,
- extraEnv = pythonExtraEnv,
- useManifest = build.options.notForBloopOptions.runWithManifest,
- scratchDirOpt = scratchDirOpt
- )
- Left(command)
- }
+ if showCommand then
+ Left {
+ Runner.jvmCommand(
+ builds.head.options.javaHome().value.javaCommand,
+ allJavaOpts,
+ builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct,
+ mainClass,
+ args,
+ extraEnv = pythonExtraEnv,
+ useManifest = builds.head.options.notForBloopOptions.runWithManifest,
+ scratchDirOpt = scratchDirOpt
+ )
+ }
else {
val proc = Runner.runJvm(
- build.options.javaHome().value.javaCommand,
+ builds.head.options.javaHome().value.javaCommand,
allJavaOpts,
- build.fullClassPathMaybeAsJar(asJar),
+ builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct,
mainClass,
args,
logger,
allowExecve = allowExecve,
extraEnv = pythonExtraEnv,
- useManifest = build.options.notForBloopOptions.runWithManifest,
+ useManifest = builds.head.options.notForBloopOptions.runWithManifest,
scratchDirOpt = scratchDirOpt
)
Right((proc, None))
@@ -599,7 +620,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
case mode: RunMode.SparkSubmit =>
value {
RunSpark.run(
- build,
+ builds,
mainClass,
args,
mode.submitArgs,
@@ -612,7 +633,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
case mode: RunMode.StandaloneSparkSubmit =>
value {
RunSpark.runStandalone(
- build,
+ builds,
mainClass,
args,
mode.submitArgs,
@@ -625,7 +646,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
case RunMode.HadoopJar =>
value {
RunHadoop.run(
- build,
+ builds,
mainClass,
args,
logger,
@@ -639,7 +660,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
}
def withLinkedJs[T](
- build: Build.Successful,
+ builds: Seq[Build.Successful],
mainClassOpt: Option[String],
addTestInitializer: Boolean,
config: ScalaJsLinkerConfig,
@@ -650,7 +671,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
)(f: os.Path => T): Either[BuildException, T] = {
val dest = os.temp(prefix = "main", suffix = if (esModule) ".mjs" else ".js")
try Package.linkJs(
- build,
+ builds,
dest,
mainClassOpt,
addTestInitializer,
@@ -665,12 +686,12 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
}
def withNativeLauncher[T](
- build: Build.Successful,
+ builds: Seq[Build.Successful],
mainClass: String,
logger: Logger
)(f: os.Path => T): Either[BuildException, T] =
Package.buildNative(
- build = build,
+ builds = builds,
mainClass = Some(mainClass),
targetType = PackageType.Native.Application,
destPath = None,
diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/SharedRunOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/run/SharedRunOptions.scala
index 832207f2c3..c5fcb3a809 100644
--- a/modules/cli/src/main/scala/scala/cli/commands/run/SharedRunOptions.scala
+++ b/modules/cli/src/main/scala/scala/cli/commands/run/SharedRunOptions.scala
@@ -51,7 +51,9 @@ final case class SharedRunOptions(
@Hidden
@Tag(tags.implementation)
@HelpMessage("Run Java commands using a manifest-based class path (shortens command length)")
- useManifest: Option[Boolean] = None
+ useManifest: Option[Boolean] = None,
+ @Recurse
+ scope: ScopeOptions = ScopeOptions()
)
// format: on
diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/MainClassOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/MainClassOptions.scala
index f944a6869e..d871cb2bea 100644
--- a/modules/cli/src/main/scala/scala/cli/commands/shared/MainClassOptions.scala
+++ b/modules/cli/src/main/scala/scala/cli/commands/shared/MainClassOptions.scala
@@ -19,6 +19,10 @@ final case class MainClassOptions(
@Name("mainClassList")
@Name("listMainClass")
@Name("listMainClasses")
+ @Name("listMainMethods")
+ @Name("listMainMethod")
+ @Name("mainMethodList")
+ @Name("mainMethodLs")
@Tag(tags.should)
@Tag(tags.inShortHelp)
mainClassLs: Option[Boolean] = None
diff --git a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala
index 5592698087..3979546dfb 100644
--- a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala
+++ b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala
@@ -201,7 +201,7 @@ object Test extends ScalaCommand[TestOptions] {
build.options.scalaJsOptions.moduleKindStr.exists(m => m == "es" || m == "esmodule")
value {
Run.withLinkedJs(
- build,
+ Seq(build),
None,
addTestInitializer = true,
linkerConfig,
@@ -225,7 +225,7 @@ object Test extends ScalaCommand[TestOptions] {
case Platform.Native =>
value {
Run.withNativeLauncher(
- build,
+ Seq(build),
"scala.scalanative.testinterface.TestMain",
logger
) { launcher =>
diff --git a/modules/cli/src/main/scala/scala/cli/commands/util/RunHadoop.scala b/modules/cli/src/main/scala/scala/cli/commands/util/RunHadoop.scala
index 9ab521da57..b6f099e58a 100644
--- a/modules/cli/src/main/scala/scala/cli/commands/util/RunHadoop.scala
+++ b/modules/cli/src/main/scala/scala/cli/commands/util/RunHadoop.scala
@@ -10,7 +10,7 @@ import scala.cli.commands.packaging.Spark
object RunHadoop {
def run(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
mainClass: String,
args: Seq[String],
logger: Logger,
@@ -18,7 +18,6 @@ object RunHadoop {
showCommand: Boolean,
scratchDirOpt: Option[os.Path]
): Either[BuildException, Either[Seq[String], (Process, Option[() => Unit])]] = either {
-
// FIXME Get Spark.hadoopModules via provided settings?
val providedModules = Spark.hadoopModules
scratchDirOpt.foreach(os.makeDir.all(_))
@@ -30,7 +29,7 @@ object RunHadoop {
)
value {
PackageCmd.assembly(
- build,
+ builds,
assembly,
// "hadoop jar" doesn't accept a main class as second argument if the jar as first argument has a main class in its manifest…
None,
@@ -41,9 +40,9 @@ object RunHadoop {
)
}
- val javaOpts = build.options.javaOptions.javaOpts.toSeq.map(_.value.value)
+ val javaOpts = builds.head.options.javaOptions.javaOpts.toSeq.map(_.value.value)
val extraEnv =
- if (javaOpts.isEmpty) Map[String, String]()
+ if javaOpts.isEmpty then Map[String, String]()
else
Map(
"HADOOP_CLIENT_OPTS" -> javaOpts.mkString(" ") // no escaping…
@@ -51,17 +50,14 @@ object RunHadoop {
val hadoopJarCommand = Seq("hadoop", "jar")
val finalCommand =
hadoopJarCommand ++ Seq(assembly.toString, mainClass) ++ args
- if (showCommand)
- Left(Runner.envCommand(extraEnv) ++ finalCommand)
+ if showCommand then Left(Runner.envCommand(extraEnv) ++ finalCommand)
else {
val proc =
- if (allowExecve)
- Runner.maybeExec("hadoop", finalCommand, logger, extraEnv = extraEnv)
- else
- Runner.run(finalCommand, logger, extraEnv = extraEnv)
+ if allowExecve then Runner.maybeExec("hadoop", finalCommand, logger, extraEnv = extraEnv)
+ else Runner.run(finalCommand, logger, extraEnv = extraEnv)
Right((
proc,
- if (scratchDirOpt.isEmpty) Some(() => os.remove(assembly, checkExists = true))
+ if scratchDirOpt.isEmpty then Some(() => os.remove(assembly, checkExists = true))
else None
))
}
diff --git a/modules/cli/src/main/scala/scala/cli/commands/util/RunSpark.scala b/modules/cli/src/main/scala/scala/cli/commands/util/RunSpark.scala
index 0356661da3..462d5628da 100644
--- a/modules/cli/src/main/scala/scala/cli/commands/util/RunSpark.scala
+++ b/modules/cli/src/main/scala/scala/cli/commands/util/RunSpark.scala
@@ -14,7 +14,7 @@ import scala.util.Properties
object RunSpark {
def run(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
mainClass: String,
args: Seq[String],
submitArgs: Seq[String],
@@ -27,11 +27,11 @@ object RunSpark {
// FIXME Get Spark.sparkModules via provided settings?
val providedModules = Spark.sparkModules
val providedFiles =
- value(PackageCmd.providedFiles(build, providedModules, logger)).toSet
- val depCp = build.dependencyClassPath.filterNot(providedFiles)
- val javaHomeInfo = build.options.javaHome().value
- val javaOpts = build.options.javaOptions.javaOpts.toSeq.map(_.value.value)
- val ext = if (Properties.isWin) ".cmd" else ""
+ value(PackageCmd.providedFiles(builds, providedModules, logger)).toSet
+ val depCp = builds.flatMap(_.dependencyClassPath).distinct.filterNot(providedFiles)
+ val javaHomeInfo = builds.head.options.javaHome().value
+ val javaOpts = builds.head.options.javaOptions.javaOpts.toSeq.map(_.value.value)
+ val ext = if Properties.isWin then ".cmd" else ""
val submitCommand: String =
EnvVar.Spark.sparkHome.valueOpt
.map(os.Path(_, os.pwd))
@@ -44,7 +44,7 @@ object RunSpark {
else Seq("--jars", depCp.mkString(","))
scratchDirOpt.foreach(os.makeDir.all(_))
- val library = Library.libraryJar(build)
+ val library = Library.libraryJar(builds)
val finalCommand =
Seq(submitCommand, "--class", mainClass) ++
@@ -54,24 +54,23 @@ object RunSpark {
Seq(library.toString) ++
args
val envUpdates = javaHomeInfo.envUpdates(sys.env)
- if (showCommand)
- Left(Runner.envCommand(envUpdates) ++ finalCommand)
+ if showCommand then Left(Runner.envCommand(envUpdates) ++ finalCommand)
else {
val proc =
- if (allowExecve)
+ if allowExecve then
Runner.maybeExec("spark-submit", finalCommand, logger, extraEnv = envUpdates)
- else
- Runner.run(finalCommand, logger, extraEnv = envUpdates)
+ else Runner.run(finalCommand, logger, extraEnv = envUpdates)
Right((
proc,
- if (scratchDirOpt.isEmpty) Some(() => os.remove(library, checkExists = true))
+ if scratchDirOpt.isEmpty then
+ Some(() => os.remove(library, checkExists = true))
else None
))
}
}
def runStandalone(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
mainClass: String,
args: Seq[String],
submitArgs: Seq[String],
@@ -83,18 +82,20 @@ object RunSpark {
// FIXME Get Spark.sparkModules via provided settings?
val providedModules = Spark.sparkModules
- val sparkClassPath = value(PackageCmd.providedFiles(build, providedModules, logger))
+ val sparkClassPath: Seq[os.Path] = value(PackageCmd.providedFiles(
+ builds,
+ providedModules,
+ logger
+ ))
scratchDirOpt.foreach(os.makeDir.all(_))
- val library = Library.libraryJar(build)
+ val library = Library.libraryJar(builds)
val finalMainClass = "org.apache.spark.deploy.SparkSubmit"
- val depCp = build.dependencyClassPath.filterNot(sparkClassPath.toSet)
- val javaHomeInfo = build.options.javaHome().value
- val javaOpts = build.options.javaOptions.javaOpts.toSeq.map(_.value.value)
- val jarsArgs =
- if (depCp.isEmpty) Nil
- else Seq("--jars", depCp.mkString(","))
+ val depCp = builds.flatMap(_.dependencyClassPath).distinct.filterNot(sparkClassPath.toSet)
+ val javaHomeInfo = builds.head.options.javaHome().value
+ val javaOpts = builds.head.options.javaOptions.javaOpts.toSeq.map(_.value.value)
+ val jarsArgs = if depCp.isEmpty then Nil else Seq("--jars", depCp.mkString(","))
val finalArgs =
Seq("--class", mainClass) ++
jarsArgs ++
@@ -103,19 +104,19 @@ object RunSpark {
Seq(library.toString) ++
args
val envUpdates = javaHomeInfo.envUpdates(sys.env)
- if (showCommand) {
- val command = Runner.jvmCommand(
- javaHomeInfo.javaCommand,
- javaOpts,
- sparkClassPath,
- finalMainClass,
- finalArgs,
- extraEnv = envUpdates,
- useManifest = build.options.notForBloopOptions.runWithManifest,
- scratchDirOpt = scratchDirOpt
- )
- Left(command)
- }
+ if showCommand then
+ Left {
+ Runner.jvmCommand(
+ javaHomeInfo.javaCommand,
+ javaOpts,
+ sparkClassPath,
+ finalMainClass,
+ finalArgs,
+ extraEnv = envUpdates,
+ useManifest = builds.head.options.notForBloopOptions.runWithManifest,
+ scratchDirOpt = scratchDirOpt
+ )
+ }
else {
val proc = Runner.runJvm(
javaHomeInfo.javaCommand,
@@ -126,13 +127,12 @@ object RunSpark {
logger,
allowExecve = allowExecve,
extraEnv = envUpdates,
- useManifest = build.options.notForBloopOptions.runWithManifest,
+ useManifest = builds.head.options.notForBloopOptions.runWithManifest,
scratchDirOpt = scratchDirOpt
)
Right((
proc,
- if (scratchDirOpt.isEmpty) Some(() => os.remove(library, checkExists = true))
- else None
+ if scratchDirOpt.isEmpty then Some(() => os.remove(library, checkExists = true)) else None
))
}
}
diff --git a/modules/cli/src/main/scala/scala/cli/internal/CachedBinary.scala b/modules/cli/src/main/scala/scala/cli/internal/CachedBinary.scala
index 3fcdc157e1..05ebfdd46d 100644
--- a/modules/cli/src/main/scala/scala/cli/internal/CachedBinary.scala
+++ b/modules/cli/src/main/scala/scala/cli/internal/CachedBinary.scala
@@ -58,13 +58,13 @@ object CachedBinary {
.map(_.getBytes(StandardCharsets.UTF_8))
}
- private def projectSha(build: Build.Successful, config: List[String]) = {
+ private def projectSha(builds: Seq[Build.Successful], config: List[String]): String = {
val md = MessageDigest.getInstance("SHA-1")
val charset = StandardCharsets.UTF_8
- md.update(build.inputs.sourceHash().getBytes(charset))
+ md.update(builds.map(_.inputs.sourceHash()).reduce(_ + _).getBytes(charset))
md.update("".getBytes())
// Resource changes for SN require relinking, so they should also be hashed
- hashResources(build).foreach(md.update)
+ builds.foreach(build => hashResources(build).foreach(md.update))
md.update("".getBytes())
md.update(0: Byte)
md.update("".getBytes(charset))
@@ -75,7 +75,7 @@ object CachedBinary {
md.update("".getBytes(charset))
md.update(Constants.version.getBytes)
md.update(0: Byte)
- for (h <- build.options.hash) {
+ for (h <- builds.map(_.options).reduce(_ orElse _).hash) {
md.update(h.getBytes(charset))
md.update(0: Byte)
}
@@ -99,7 +99,7 @@ object CachedBinary {
}
def getCacheData(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
config: List[String],
dest: os.Path,
workDir: os.Path
@@ -107,11 +107,12 @@ object CachedBinary {
val projectShaPath = resolveProjectShaPath(workDir)
val outputShaPath = resolveOutputShaPath(workDir)
- val currentProjectSha = projectSha(build, config)
- val currentOutputSha = if (os.exists(dest)) Some(fileSha(dest)) else None
+ val currentProjectSha = projectSha(builds, config)
+ val currentOutputSha = if os.exists(dest) then Some(fileSha(dest)) else None
- val previousProjectSha = if (os.exists(projectShaPath)) Some(os.read(projectShaPath)) else None
- val previousOutputSha = if (os.exists(outputShaPath)) Some(os.read(outputShaPath)) else None
+ val previousProjectSha =
+ if os.exists(projectShaPath) then Some(os.read(projectShaPath)) else None
+ val previousOutputSha = if os.exists(outputShaPath) then Some(os.read(outputShaPath)) else None
val changed =
!previousProjectSha.contains(currentProjectSha) ||
diff --git a/modules/cli/src/main/scala/scala/cli/packaging/Library.scala b/modules/cli/src/main/scala/scala/cli/packaging/Library.scala
index 9ba415b6b0..ca69e3bf4a 100644
--- a/modules/cli/src/main/scala/scala/cli/packaging/Library.scala
+++ b/modules/cli/src/main/scala/scala/cli/packaging/Library.scala
@@ -10,23 +10,21 @@ import scala.build.Build
import scala.cli.internal.CachedBinary
object Library {
-
def libraryJar(
- build: Build.Successful,
+ builds: Seq[Build.Successful],
mainClassOpt: Option[String] = None
): os.Path = {
-
- val workDir = build.inputs.libraryJarWorkDir
+ val workDir = builds.head.inputs.libraryJarWorkDir
val dest = workDir / "library.jar"
val cacheData =
CachedBinary.getCacheData(
- build,
+ builds,
mainClassOpt.toList.flatMap(c => List("--main-class", c)),
dest,
workDir
)
- if (cacheData.changed) {
+ if cacheData.changed then {
var outputStream: OutputStream = null
try {
outputStream = os.write.outputStream(
@@ -36,13 +34,12 @@ object Library {
)
writeLibraryJarTo(
outputStream,
- build,
+ builds,
mainClassOpt
)
}
finally
- if (outputStream != null)
- outputStream.close()
+ if outputStream != null then outputStream.close()
CachedBinary.updateProjectAndOutputSha(dest, workDir, cacheData.projectSha)
}
@@ -52,7 +49,7 @@ object Library {
def writeLibraryJarTo(
outputStream: OutputStream,
- build: Build.Successful,
+ builds: Seq[Build.Successful],
mainClassOpt: Option[String] = None,
hasActualManifest: Boolean = true,
contentDirOverride: Option[os.Path] = None
@@ -61,16 +58,21 @@ object Library {
val manifest = new java.util.jar.Manifest
manifest.getMainAttributes.put(JarAttributes.Name.MANIFEST_VERSION, "1.0")
- if (hasActualManifest)
- for (mainClass <- mainClassOpt.orElse(build.sources.defaultMainClass) if mainClass.nonEmpty)
- manifest.getMainAttributes.put(JarAttributes.Name.MAIN_CLASS, mainClass)
+ if hasActualManifest then
+ for {
+ mainClass <- mainClassOpt.orElse(builds.flatMap(_.sources.defaultMainClass).headOption)
+ if mainClass.nonEmpty
+ } manifest.getMainAttributes.put(JarAttributes.Name.MAIN_CLASS, mainClass)
var zos: ZipOutputStream = null
- val contentDir = contentDirOverride.getOrElse(build.output)
+ val contentDirs = builds.map(b => contentDirOverride.getOrElse(b.output))
try {
zos = new JarOutputStream(outputStream, manifest)
- for (path <- os.walk(contentDir) if os.isFile(path)) {
+ for {
+ contentDir <- contentDirs
+ path <- os.walk(contentDir) if os.isFile(path)
+ } {
val name = path.relativeTo(contentDir).toString
val lastModified = os.mtime(path)
val ent = new ZipEntry(name)
@@ -88,10 +90,10 @@ object Library {
}
extension (build: Build.Successful) {
- def fullClassPathAsJar: Seq[os.Path] =
- Seq(libraryJar(build)) ++ build.dependencyClassPath
+ private def fullClassPathAsJar: Seq[os.Path] =
+ Seq(libraryJar(Seq(build))) ++ build.dependencyClassPath
def fullClassPathMaybeAsJar(asJar: Boolean): Seq[os.Path] =
- if (asJar) fullClassPathAsJar else build.fullClassPath
+ if asJar then fullClassPathAsJar else build.fullClassPath
}
}
diff --git a/modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala b/modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala
index 1f2b7572b2..8ce5c8dc10 100644
--- a/modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala
+++ b/modules/cli/src/main/scala/scala/cli/packaging/NativeImage.scala
@@ -190,14 +190,14 @@ object NativeImage {
options.notForBloopOptions.packageOptions.nativeImageOptions.graalvmArgs.map(_.value)
val cacheData = CachedBinary.getCacheData(
- build,
+ Seq(build),
s"--java-home=${javaHome.javaHome.toString}" :: "--" :: extraOptions.toList ++ nativeImageArgs,
dest,
nativeImageWorkDir
)
if (cacheData.changed) {
- val mainJar = Library.libraryJar(build)
+ val mainJar = Library.libraryJar(Seq(build))
val originalClassPath = mainJar +: build.dependencyClassPath
ManifestJar.maybeWithManifestClassPath(
diff --git a/modules/cli/src/test/scala/cli/tests/CachedBinaryTests.scala b/modules/cli/src/test/scala/cli/tests/CachedBinaryTests.scala
index 2a758e8fa8..096025b207 100644
--- a/modules/cli/src/test/scala/cli/tests/CachedBinaryTests.scala
+++ b/modules/cli/src/test/scala/cli/tests/CachedBinaryTests.scala
@@ -1,22 +1,24 @@
package scala.cli.tests
-import com.eed3si9n.expecty.Expecty.{assert => expect}
+import bloop.rifle.BloopRifleConfig
+import com.eed3si9n.expecty.Expecty.assert as expect
+import os.Path
import scala.build.options.{BuildOptions, InternalOptions}
import scala.build.tests.util.BloopServer
import scala.build.tests.{TestInputs, TestLogger}
-import scala.build.{BuildThreads, Directories, LocalRepo}
+import scala.build.{Build, BuildThreads, Directories, LocalRepo}
import scala.cli.internal.CachedBinary
import scala.util.{Properties, Random}
class CachedBinaryTests extends munit.FunSuite {
- val buildThreads = BuildThreads.create()
- def bloopConfig = BloopServer.bloopConfig
+ val buildThreads: BuildThreads = BuildThreads.create()
+ def bloopConfig: BloopRifleConfig = BloopServer.bloopConfig
val helloFileName = "Hello.scala"
- val inputs = TestInputs(
+ val inputs: TestInputs = TestInputs(
os.rel / helloFileName ->
s"""object Hello extends App {
| println("Hello")
@@ -29,21 +31,55 @@ class CachedBinaryTests extends munit.FunSuite {
|""".stripMargin
)
- val extraRepoTmpDir = os.temp.dir(prefix = "scala-cli-tests-extra-repo-")
- val directories = Directories.under(extraRepoTmpDir)
+ val extraRepoTmpDir: Path = os.temp.dir(prefix = "scala-cli-tests-extra-repo-")
+ val directories: Directories = Directories.under(extraRepoTmpDir)
- val defaultOptions = BuildOptions(
+ val defaultOptions: BuildOptions = BuildOptions(
internal = InternalOptions(
localRepository = LocalRepo.localRepo(directories.localRepoDir, TestLogger())
)
)
- private def helperTests(fromDirectory: Boolean) = {
- val additionalMessage =
- if (fromDirectory) "built from a directory" else "built from a set of files"
+ for {
+ fromDirectory <- List(false, true)
+ additionalMessage = if (fromDirectory) "built from a directory" else "built from a set of files"
+ } {
+ test(s"should build native app with added test scope at first time ($additionalMessage)") {
+ TestInputs(
+ os.rel / "main" / "Main.scala" ->
+ s"""object Main extends App {
+ | println("Hello")
+ |}
+ |""".stripMargin,
+ os.rel / "test" / "TestScope.scala" ->
+ s"""object TestScope extends App {
+ | println("Hello from the test scope")
+ |}
+ |""".stripMargin
+ ).withLoadedBuilds(
+ defaultOptions,
+ buildThreads,
+ Some(bloopConfig),
+ fromDirectory
+ ) {
+ (_, _, builds) =>
+ expect(builds.builds.forall(_.success))
+
+ val config =
+ builds.main.options.scalaNativeOptions.configCliOptions(resourcesExist = false)
+ val nativeWorkDir = builds.main.inputs.nativeWorkDir
+ val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
+ // generate dummy output
+ os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true)
- test(s"should build native app at first time ($additionalMessage)") {
+ val successfulBuilds = builds.builds.map { case s: Build.Successful => s }
+ val cacheData =
+ CachedBinary.getCacheData(successfulBuilds, config, destPath, nativeWorkDir)
+ expect(cacheData.changed)
+ }
+ }
+ test(s"should build native app at first time ($additionalMessage)") {
inputs.withLoadedBuild(defaultOptions, buildThreads, Some(bloopConfig), fromDirectory) {
(_, _, maybeBuild) =>
val build = maybeBuild.successfulOpt.get
@@ -55,7 +91,7 @@ class CachedBinaryTests extends munit.FunSuite {
os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true)
val cacheData =
- CachedBinary.getCacheData(build, config, destPath, nativeWorkDir)
+ CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir)
expect(cacheData.changed)
}
}
@@ -72,7 +108,7 @@ class CachedBinaryTests extends munit.FunSuite {
os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true)
val cacheData =
- CachedBinary.getCacheData(build, config, destPath, nativeWorkDir)
+ CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir)
CachedBinary.updateProjectAndOutputSha(
destPath,
nativeWorkDir,
@@ -81,7 +117,7 @@ class CachedBinaryTests extends munit.FunSuite {
expect(cacheData.changed)
val sameBuildCache =
- CachedBinary.getCacheData(build, config, destPath, nativeWorkDir)
+ CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir)
expect(!sameBuildCache.changed)
}
}
@@ -98,7 +134,7 @@ class CachedBinaryTests extends munit.FunSuite {
os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true)
val cacheData =
- CachedBinary.getCacheData(build, config, destPath, nativeWorkDir)
+ CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir)
CachedBinary.updateProjectAndOutputSha(
destPath,
nativeWorkDir,
@@ -108,7 +144,7 @@ class CachedBinaryTests extends munit.FunSuite {
os.remove(destPath)
val afterDeleteCache =
- CachedBinary.getCacheData(build, config, destPath, nativeWorkDir)
+ CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir)
expect(afterDeleteCache.changed)
}
}
@@ -125,7 +161,7 @@ class CachedBinaryTests extends munit.FunSuite {
os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true)
val cacheData =
- CachedBinary.getCacheData(build, config, destPath, nativeWorkDir)
+ CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir)
CachedBinary.updateProjectAndOutputSha(
destPath,
nativeWorkDir,
@@ -135,7 +171,7 @@ class CachedBinaryTests extends munit.FunSuite {
os.write.over(destPath, Random.alphanumeric.take(10).mkString(""))
val cacheAfterFileUpdate =
- CachedBinary.getCacheData(build, config, destPath, nativeWorkDir)
+ CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir)
expect(cacheAfterFileUpdate.changed)
}
}
@@ -151,7 +187,7 @@ class CachedBinaryTests extends munit.FunSuite {
os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true)
val cacheData =
- CachedBinary.getCacheData(build, config, destPath, nativeWorkDir)
+ CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir)
CachedBinary.updateProjectAndOutputSha(
destPath,
nativeWorkDir,
@@ -161,7 +197,7 @@ class CachedBinaryTests extends munit.FunSuite {
os.write.append(root / helloFileName, Random.alphanumeric.take(10).mkString(""))
val cacheAfterFileUpdate =
- CachedBinary.getCacheData(build, config, destPath, nativeWorkDir)
+ CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir)
expect(cacheAfterFileUpdate.changed)
}
}
@@ -177,7 +213,7 @@ class CachedBinaryTests extends munit.FunSuite {
os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true)
val cacheData =
- CachedBinary.getCacheData(build, config, destPath, nativeWorkDir)
+ CachedBinary.getCacheData(Seq(build), config, destPath, nativeWorkDir)
CachedBinary.updateProjectAndOutputSha(
destPath,
nativeWorkDir,
@@ -197,7 +233,7 @@ class CachedBinaryTests extends munit.FunSuite {
val cacheAfterConfigUpdate =
CachedBinary.getCacheData(
- updatedBuild,
+ Seq(updatedBuild),
updatedConfig,
destPath,
nativeWorkDir
@@ -206,8 +242,4 @@ class CachedBinaryTests extends munit.FunSuite {
}
}
}
-
- helperTests(fromDirectory = false)
- helperTests(fromDirectory = true)
-
}
diff --git a/modules/cli/src/test/scala/cli/tests/PackageTests.scala b/modules/cli/src/test/scala/cli/tests/PackageTests.scala
index 1cd79a315f..254f48ceda 100644
--- a/modules/cli/src/test/scala/cli/tests/PackageTests.scala
+++ b/modules/cli/src/test/scala/cli/tests/PackageTests.scala
@@ -44,7 +44,7 @@ class PackageTests extends munit.FunSuite {
inputs.withBuild(defaultOptions, buildThreads, Some(bloopConfig)) {
(_, _, maybeFirstBuild) =>
val firstBuild = maybeFirstBuild.orThrow.successfulOpt.get
- val firstLibraryJar = Library.libraryJar(firstBuild)
+ val firstLibraryJar = Library.libraryJar(Seq(firstBuild))
expect(os.exists(firstLibraryJar)) // should create library jar
// change Hello.scala and recompile
@@ -66,7 +66,7 @@ class PackageTests extends munit.FunSuite {
) {
(_, _, maybeSecondBuild) =>
val secondBuild = maybeSecondBuild.orThrow.successfulOpt.get
- val libraryJar = Library.libraryJar(secondBuild)
+ val libraryJar = Library.libraryJar(Seq(secondBuild))
val fs = // should not throw "invalid CEN header (bad signature)" ZipException
FileSystems.newFileSystem(libraryJar.toNIO, null: ClassLoader)
expect(fs.isOpen)
diff --git a/modules/integration/src/test/scala/scala/cli/integration/HadoopTests.scala b/modules/integration/src/test/scala/scala/cli/integration/HadoopTests.scala
index 28dcb0b256..8583513de5 100644
--- a/modules/integration/src/test/scala/scala/cli/integration/HadoopTests.scala
+++ b/modules/integration/src/test/scala/scala/cli/integration/HadoopTests.scala
@@ -5,99 +5,108 @@ import com.eed3si9n.expecty.Expecty.expect
class HadoopTests extends munit.FunSuite {
protected lazy val extraOptions: Seq[String] = TestUtil.extraOptions
- test("simple map-reduce") {
- TestUtil.retryOnCi() {
- val inputs = TestInputs(
- os.rel / "WordCount.java" ->
- """//> using dep org.apache.hadoop:hadoop-client-api:3.3.3
- |
- |// from https://hadoop.apache.org/docs/r3.3.3/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html
- |
- |package foo;
- |
- |import java.io.IOException;
- |import java.util.StringTokenizer;
- |
- |import org.apache.hadoop.conf.Configuration;
- |import org.apache.hadoop.fs.Path;
- |import org.apache.hadoop.io.IntWritable;
- |import org.apache.hadoop.io.Text;
- |import org.apache.hadoop.mapreduce.Job;
- |import org.apache.hadoop.mapreduce.Mapper;
- |import org.apache.hadoop.mapreduce.Reducer;
- |import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
- |import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
- |
- |public class WordCount {
- |
- | public static class TokenizerMapper
- | extends Mapper