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{ - | - | private final static IntWritable one = new IntWritable(1); - | private Text word = new Text(); - | - | public void map(Object key, Text value, Context context - | ) throws IOException, InterruptedException { - | StringTokenizer itr = new StringTokenizer(value.toString()); - | while (itr.hasMoreTokens()) { - | word.set(itr.nextToken()); - | context.write(word, one); - | } - | } - | } - | - | public static class IntSumReducer - | extends Reducer { - | private IntWritable result = new IntWritable(); - | - | public void reduce(Text key, Iterable values, - | Context context - | ) throws IOException, InterruptedException { - | int sum = 0; - | for (IntWritable val : values) { - | sum += val.get(); - | } - | result.set(sum); - | context.write(key, result); - | } - | } - | - | public static void main(String[] args) throws Exception { - | Configuration conf = new Configuration(); - | Job job = Job.getInstance(conf, "word count"); - | job.setJarByClass(WordCount.class); - | job.setMapperClass(TokenizerMapper.class); - | job.setCombinerClass(IntSumReducer.class); - | job.setReducerClass(IntSumReducer.class); - | job.setOutputKeyClass(Text.class); - | job.setOutputValueClass(IntWritable.class); - | FileInputFormat.addInputPath(job, new Path(args[0])); - | FileOutputFormat.setOutputPath(job, new Path(args[1])); - | System.exit(job.waitForCompletion(true) ? 0 : 1); - | } - |} - |""".stripMargin - ) - inputs.fromRoot { root => - val res = os.proc( - TestUtil.cli, - "--power", - "run", - TestUtil.extraOptions, - ".", - "--hadoop", - "--command", - "--scratch-dir", - "tmp", - "--", - "foo" + for { + withTestScope <- Seq(true, false) + scopeDescription = if (withTestScope) "test scope" else "main scope" + inputPath = + if (withTestScope) os.rel / "test" / "WordCount.java" else os.rel / "main" / "WordCount.java" + directiveKey = if (withTestScope) "test.dep" else "dep" + scopeOptions = if (withTestScope) Seq("--test") else Nil + } + test(s"simple map-reduce ($scopeDescription)") { + TestUtil.retryOnCi() { + val inputs = TestInputs( + inputPath -> + s"""//> using $directiveKey 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{ + | + | private final static IntWritable one = new IntWritable(1); + | private Text word = new Text(); + | + | public void map(Object key, Text value, Context context + | ) throws IOException, InterruptedException { + | StringTokenizer itr = new StringTokenizer(value.toString()); + | while (itr.hasMoreTokens()) { + | word.set(itr.nextToken()); + | context.write(word, one); + | } + | } + | } + | + | public static class IntSumReducer + | extends Reducer { + | private IntWritable result = new IntWritable(); + | + | public void reduce(Text key, Iterable values, + | Context context + | ) throws IOException, InterruptedException { + | int sum = 0; + | for (IntWritable val : values) { + | sum += val.get(); + | } + | result.set(sum); + | context.write(key, result); + | } + | } + | + | public static void main(String[] args) throws Exception { + | Configuration conf = new Configuration(); + | Job job = Job.getInstance(conf, "word count"); + | job.setJarByClass(WordCount.class); + | job.setMapperClass(TokenizerMapper.class); + | job.setCombinerClass(IntSumReducer.class); + | job.setReducerClass(IntSumReducer.class); + | job.setOutputKeyClass(Text.class); + | job.setOutputValueClass(IntWritable.class); + | FileInputFormat.addInputPath(job, new Path(args[0])); + | FileOutputFormat.setOutputPath(job, new Path(args[1])); + | System.exit(job.waitForCompletion(true) ? 0 : 1); + | } + |} + |""".stripMargin ) - .call(cwd = root) - val command = res.out.lines() - pprint.err.log(command) - expect(command.take(2) == Seq("hadoop", "jar")) - expect(command.takeRight(2) == Seq("foo.WordCount", "foo")) + inputs.fromRoot { root => + val res = os.proc( + TestUtil.cli, + "--power", + "run", + TestUtil.extraOptions, + ".", + "--hadoop", + "--command", + "--scratch-dir", + "tmp", + scopeOptions, + "--", + "foo" + ) + .call(cwd = root) + val command = res.out.lines() + pprint.err.log(command) + expect(command.take(2) == Seq("hadoop", "jar")) + expect(command.takeRight(2) == Seq("foo.WordCount", "foo")) + } } } - } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index b59c5e9174..0ccfe39612 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -2362,4 +2362,42 @@ abstract class RunTestDefinitions } } + + for ( + (platformDescription, platformOpts) <- Seq( + "JVM" -> Nil, + "JS" -> Seq("--js"), + "Native" -> Seq("--native") + ) + ) + test(s"run a main method from the test scope ($platformDescription)") { + val expectedMessage = "Hello from the test scope" + TestInputs( + os.rel / "example.test.scala" -> s"""object Main extends App { println("$expectedMessage") }""" + ).fromRoot { root => + val res = os.proc(TestUtil.cli, "run", ".", "--test", extraOptions, platformOpts) + .call(cwd = root) + expect(res.out.trim().contains(expectedMessage)) + } + } + + test(s"--list-main-classes includes test scope main methods when --test is enabled") { + val expectedMains @ Seq(expectedMain1, expectedMain2) = Seq("Main", "AnotherMain") + val expectedOutput = expectedMains.mkString(" ") + TestInputs( + os.rel / "example.scala" -> s"""object $expectedMain1 extends App { println("Hello from the main scope") }""", + os.rel / "example.test.scala" -> s"""object $expectedMain2 extends App { println("Hello from the test scope") }""" + ).fromRoot { root => + val res = os.proc( + TestUtil.cli, + "run", + ".", + "--test", + "--list-main-classes", + extraOptions + ) + .call(cwd = root) + expect(res.out.trim() == expectedOutput) + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala index 2de0246228..901f7a8a73 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala @@ -89,6 +89,52 @@ trait RunWithWatchTestDefinitions { _: RunTestDefinitions => } } + for { + (platformDescription, platformOpts) <- Seq( + "JVM" -> Nil, + "JS" -> Seq("--js"), + "Native" -> Seq("--native") + ) + // TODO make this pass reliably on Mac CI https://github.com/VirtusLab/scala-cli/issues/2517 + if !Properties.isMac || !TestUtil.isCI + } + test(s"--watch --test ($platformDescription)") { + TestUtil.retryOnCi() { + val expectedMessage1 = "Hello from the test scope 1" + val expectedMessage2 = "Hello from the test scope 2" + val inputPath = os.rel / "example.test.scala" + + def code(expectedMessage: String) = + s"""object Main extends App { println("$expectedMessage") }""" + + TestInputs( + inputPath -> code(expectedMessage1) + ).fromRoot { root => + TestUtil.withProcessWatching( + proc = + os.proc( + TestUtil.cli, + "run", + inputPath.toString(), + "--watch", + "--test", + extraOptions, + platformOpts + ) + .spawn(cwd = root, stderr = os.Pipe), + timeout = 300.seconds + ) { (proc, timeout, ec) => + val output1 = TestUtil.readLine(proc.stdout, ec, timeout) + expect(output1 == expectedMessage1) + proc.printStderrUntilRerun(timeout)(ec) + os.write.over(root / inputPath, code(expectedMessage2)) + val output2 = TestUtil.readLine(proc.stdout, ec, timeout) + expect(output2 == expectedMessage2) + } + } + } + } + test("watch with interactive, with multiple main classes") { val fileName = "watch.scala" TestInputs( diff --git a/modules/integration/src/test/scala/scala/cli/integration/SparkTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/SparkTestDefinitions.scala index 86e12505b4..40753931a9 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/SparkTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/SparkTestDefinitions.scala @@ -58,8 +58,8 @@ abstract class SparkTestDefinitions extends ScalaCliSuite with TestScalaVersionA protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions protected def defaultMaster = "local[4]" - protected def simpleJobInputs(spark: Spark) = TestInputs( - os.rel / "SparkJob.scala" -> + protected def simpleJobInputs(spark: Spark, withTestScope: Boolean): TestInputs = TestInputs( + os.rel / (if (withTestScope) "SparkJob.test.scala" else "SparkJob.scala") -> s"""//> using dep org.apache.spark::spark-sql:${spark.sparkVersion} |//> using dep com.chuusai::shapeless:2.3.10 |//> using dep com.lihaoyi::pprint:0.7.3 @@ -128,13 +128,23 @@ abstract class SparkTestDefinitions extends ScalaCliSuite with TestScalaVersionA def simpleRunStandaloneSparkJobTest( scalaVersion: String, sparkVersion: String, - needsWinUtils: Boolean = false + needsWinUtils: Boolean = false, + withTestScope: Boolean ): Unit = - simpleJobInputs(new Spark(sparkVersion, scalaVersion)).fromRoot { root => + simpleJobInputs(new Spark(sparkVersion, scalaVersion), withTestScope).fromRoot { root => val extraEnv = if (needsWinUtils) maybeHadoopHomeForWinutils(root / "hadoop-home") else Map.empty[String, String] - val res = os.proc(TestUtil.cli, "--power", "run", extraOptions, "--spark-standalone", ".") + val scopeOptions = if (withTestScope) Seq("--test") else Nil + val res = os.proc( + TestUtil.cli, + "--power", + "run", + extraOptions, + "--spark-standalone", + ".", + scopeOptions + ) .call(cwd = root, env = extraEnv) val expectedOutput = "Result: 55" @@ -144,8 +154,16 @@ abstract class SparkTestDefinitions extends ScalaCliSuite with TestScalaVersionA expect(output.contains(expectedOutput)) } - test("run spark 3.3 standalone") { - simpleRunStandaloneSparkJobTest(actualScalaVersion, "3.3.0", needsWinUtils = true) + for { + withTestScope <- Seq(true, false) + scopeDescription = if (withTestScope) "test scope" else "main scope" + } test(s"run spark 3.3 standalone ($scopeDescription)") { + simpleRunStandaloneSparkJobTest( + actualScalaVersion, + "3.3.0", + needsWinUtils = true, + withTestScope = withTestScope + ) } test("run spark spark-submit args") { diff --git a/modules/integration/src/test/scala/scala/cli/integration/SparkTests212.scala b/modules/integration/src/test/scala/scala/cli/integration/SparkTests212.scala index 3b38af3b92..809ef2b05d 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/SparkTests212.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/SparkTests212.scala @@ -29,7 +29,7 @@ class SparkTests212 extends SparkTestDefinitions with Test212 { } def simplePackageSparkJobTest(spark: Spark): Unit = - simpleJobInputs(spark).fromRoot { root => + simpleJobInputs(spark, withTestScope = false).fromRoot { root => val dest = os.rel / "SparkJob.jar" os.proc( TestUtil.cli, @@ -80,12 +80,27 @@ class SparkTests212 extends SparkTestDefinitions with Test212 { Map(key -> s"$dir${File.pathSeparator}$currentValue") } - def simpleRunSparkJobTest(spark: Spark, usePath: Boolean = false): Unit = - simpleJobInputs(spark).fromRoot { root => + def simpleRunSparkJobTest( + spark: Spark, + usePath: Boolean = false, + withTestScope: Boolean = false + ): Unit = + simpleJobInputs(spark, withTestScope).fromRoot { root => val env = if (usePath) addToPath(spark.sparkHome / "bin") else Map("SPARK_HOME" -> spark.sparkHome.toString) - val res = os.proc(TestUtil.cli, "--power", "run", extraOptions, "--spark", "--jvm", "8", ".") + val scopeOptions = if (withTestScope) Seq("--test") else Nil + val res = os.proc( + TestUtil.cli, + "--power", + "run", + extraOptions, + "--spark", + "--jvm", + "8", + ".", + scopeOptions + ) .call(cwd = root, env = env) val expectedOutput = "Result: 55" @@ -103,24 +118,37 @@ class SparkTests212 extends SparkTestDefinitions with Test212 { simplePackageSparkJobTest(spark30) } - test("run spark 2.4") { - simpleRunSparkJobTest(spark24) - } - - test("run spark 3.0") { - simpleRunSparkJobTest(spark30) - } + for { + withTestScope <- Seq(true, false) + scopeDescription = if (withTestScope) "test scope" else "main scope" + } { + test(s"run spark 2.4 ($scopeDescription)") { + simpleRunSparkJobTest(spark24, withTestScope = withTestScope) + } - test("run spark 3.0 via PATH") { - simpleRunSparkJobTest(spark30, usePath = true) - } + test(s"run spark 2.4 standalone ($scopeDescription)") { + simpleRunStandaloneSparkJobTest( + spark24.scalaVersion, + spark24.sparkVersion, + withTestScope = withTestScope + ) + } - test("run spark 2.4 standalone") { - simpleRunStandaloneSparkJobTest(spark24.scalaVersion, spark24.sparkVersion) - } + test(s"run spark 3.0 standalone ($scopeDescription)") { + simpleRunStandaloneSparkJobTest( + spark30.scalaVersion, + spark30.sparkVersion, + withTestScope = withTestScope + ) + } - test("run spark 3.0 standalone") { - simpleRunStandaloneSparkJobTest(spark30.scalaVersion, spark30.sparkVersion) + for { + usePath <- Seq(false, true) + pathDescription = if (usePath) "via PATH " else "" + } + test(s"run spark 3.0 $pathDescription($scopeDescription)") { + simpleRunSparkJobTest(spark30, usePath = usePath, withTestScope = withTestScope) + } } } diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index bf2a22f2a4..baec3df207 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -737,7 +737,7 @@ Specify which main class to run ### `--main-class-ls` -Aliases: `--list-main-class`, `--list-main-classes`, `--main-class-list` +Aliases: `--list-main-class`, `--list-main-classes`, `--list-main-method`, `--list-main-methods`, `--main-class-list`, `--main-method-list`, `--main-method-ls` List main classes available in the current context @@ -1534,7 +1534,7 @@ Run scalafix rule(s) explicitly, overriding the configuration file default. Available in commands: -[`compile`](./commands.md#compile), [`repl` , `console`](./commands.md#repl) +[`compile`](./commands.md#compile), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`shebang`](./commands.md#shebang) diff --git a/website/docs/reference/commands.md b/website/docs/reference/commands.md index 5e26d87d5c..bc4737270d 100644 --- a/website/docs/reference/commands.md +++ b/website/docs/reference/commands.md @@ -306,7 +306,7 @@ To pass arguments to the actual application, just add them after `--`, like: For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/run -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-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), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-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), [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), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-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), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-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), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## github secret create @@ -384,7 +384,7 @@ Using this, it is possible to conveniently set up Unix shebang scripts. For exam For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/shebang -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-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), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-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), [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), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-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), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-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), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## test diff --git a/website/docs/reference/scala-command/cli-options.md b/website/docs/reference/scala-command/cli-options.md index 24f5273595..70560196d5 100644 --- a/website/docs/reference/scala-command/cli-options.md +++ b/website/docs/reference/scala-command/cli-options.md @@ -658,7 +658,7 @@ Specify which main class to run ### `--main-class-ls` -Aliases: `--list-main-class`, `--list-main-classes`, `--main-class-list` +Aliases: `--list-main-class`, `--list-main-classes`, `--list-main-method`, `--list-main-methods`, `--main-class-list`, `--main-method-list`, `--main-method-ls` `SHOULD have` per Scala Runner specification @@ -978,7 +978,7 @@ Turn verbosity on for scalac. This is an alias for --scalac-option -verbose Available in commands: -[`compile`](./commands.md#compile), [`repl` , `console`](./commands.md#repl) +[`compile`](./commands.md#compile), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`shebang`](./commands.md#shebang) diff --git a/website/docs/reference/scala-command/commands.md b/website/docs/reference/scala-command/commands.md index e9839dbd0b..638a9785bb 100644 --- a/website/docs/reference/scala-command/commands.md +++ b/website/docs/reference/scala-command/commands.md @@ -135,7 +135,7 @@ To pass arguments to the actual application, just add them after `--`, like: For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/run -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-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), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-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), [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), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-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), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-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), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ### shebang @@ -166,7 +166,7 @@ Using this, it is possible to conveniently set up Unix shebang scripts. For exam For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/shebang -Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-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), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-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), [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), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) +Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-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), [java](./cli-options.md#java-options), [java prop](./cli-options.md#java-prop-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [power](./cli-options.md#power-options), [python](./cli-options.md#python-options), [run](./cli-options.md#run-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), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options) ## SHOULD have commands: diff --git a/website/docs/reference/scala-command/runner-specification.md b/website/docs/reference/scala-command/runner-specification.md index 91151ea271..3cad603ac2 100644 --- a/website/docs/reference/scala-command/runner-specification.md +++ b/website/docs/reference/scala-command/runner-specification.md @@ -2320,12 +2320,18 @@ Aliases: `--revolver` List main classes available in the current context -Aliases: `--main-class-list` ,`--list-main-class` ,`--list-main-classes` +Aliases: `--main-class-list` ,`--list-main-class` ,`--list-main-classes` ,`--list-main-methods` ,`--list-main-method` ,`--main-method-list` ,`--main-method-ls` **--command** Print the command that would have been run (one argument per line), rather than running it +**--test** + +Include test scope + +Aliases: `--test-scope` ,`--with-test-scope` ,`--with-test` +
### Implementantation specific options @@ -2954,12 +2960,18 @@ Aliases: `--revolver` List main classes available in the current context -Aliases: `--main-class-list` ,`--list-main-class` ,`--list-main-classes` +Aliases: `--main-class-list` ,`--list-main-class` ,`--list-main-classes` ,`--list-main-methods` ,`--list-main-method` ,`--main-method-list` ,`--main-method-ls` **--command** Print the command that would have been run (one argument per line), rather than running it +**--test** + +Include test scope + +Aliases: `--test-scope` ,`--with-test-scope` ,`--with-test` +
### Implementantation specific options