From 0239f63bdd9df3e91f2fb57ed036c30ea40f5c13 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Tue, 29 Jul 2025 11:10:12 +0200 Subject: [PATCH 1/3] Run all cross builds when `--cross` is passed --- .../src/main/scala/scala/build/Build.scala | 8 +- .../scala/cli/commands/publish/Publish.scala | 8 - .../scala/scala/cli/commands/run/Run.scala | 522 +++++++++--------- .../commands/util/BuildCommandHelpers.scala | 10 + .../cli/integration/RunTestsDefault.scala | 13 +- 5 files changed, 293 insertions(+), 268 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index ee04f917c8..2246c6d923 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -451,10 +451,10 @@ object Build { (Nil, Nil, Nil, Nil) Builds( - Seq(nonCrossBuilds.main) ++ nonCrossBuilds.testOpt.toSeq, - Seq(extraMainBuilds, extraTestBuilds), - nonCrossBuilds.docOpt.toSeq ++ nonCrossBuilds.testDocOpt.toSeq, - Seq(extraDocBuilds, extraDocTestBuilds) + builds = Seq(nonCrossBuilds.main) ++ nonCrossBuilds.testOpt.toSeq, + crossBuilds = Seq(extraMainBuilds, extraTestBuilds), + docBuilds = nonCrossBuilds.docOpt.toSeq ++ nonCrossBuilds.testDocOpt.toSeq, + docCrossBuilds = Seq(extraDocBuilds, extraDocTestBuilds) ) } 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 cab1ededc1..8dd6c0140b 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 @@ -661,14 +661,6 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { dummy: Boolean ): Either[BuildException, Unit] = either { assert(docBuilds.isEmpty || docBuilds.length == builds.length) - - extension (b: Seq[Build.Successful]) { - private def groupedByCrossParams = - b.groupBy(b => - b.options.scalaOptions.scalaVersion.map(_.asString).toString -> - b.options.platform.toString - ) - } val groupedBuilds = builds.groupedByCrossParams val groupedDocBuilds = docBuilds.groupedByCrossParams val it: Iterator[(Seq[Build.Successful], Seq[Build.Successful])] = 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 a9972305aa..26c09b0462 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 @@ -159,16 +159,16 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { runMode: RunMode, showCommand: Boolean, scratchDirOpt: Option[os.Path] - ): Either[BuildException, Option[(Process, CompletableFuture[?])]] = either { + ): Either[BuildException, Seq[(Process, CompletableFuture[?])]] = either { val potentialMainClasses = builds.flatMap(_.foundMainClasses()).distinct if (options.sharedRun.mainClass.mainClassLs.contains(true)) value { options.sharedRun.mainClass .maybePrintMainClasses(potentialMainClasses, shouldExit = allowTerminate) - .map(_ => None) + .map(_ => Seq.empty) } else { - val processOrCommand = value { + val processOrCommand: Either[Seq[Seq[String]], Seq[(Process, Option[() => Unit])]] = value { maybeRunOnce( builds, programArgs, @@ -184,34 +184,36 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { } processOrCommand match { - case Right((process, onExitOpt)) => - val onExitProcess = process.onExit().thenApply { p1 => - val retCode = p1.exitValue() - onExitOpt.foreach(_()) - (retCode, allowTerminate) match { - case (0, true) => - case (0, false) => - val gray = ScalaCliConsole.GRAY - val reset = Console.RESET - System.err.println(s"${gray}Program exited with return code $retCode.$reset") - case (_, true) => - sys.exit(retCode) - case (_, false) => - val red = Console.RED - val lightRed = "\u001b[91m" - val reset = Console.RESET - System.err.println( - s"${red}Program exited with return code $lightRed$retCode$red.$reset" - ) + case Right(processes) => + processes.map { case (process, onExitOpt) => + val onExitProcess = process.onExit().thenApply { p1 => + val retCode = p1.exitValue() + onExitOpt.foreach(_()) + (retCode, allowTerminate) match { + case (0, true) => + case (0, false) => + val gray = ScalaCliConsole.GRAY + val reset = Console.RESET + System.err.println(s"${gray}Program exited with return code $retCode.$reset") + case (_, true) => + sys.exit(retCode) + case (_, false) => + val red = Console.RED + val lightRed = "\u001b[91m" + val reset = Console.RESET + System.err.println( + s"${red}Program exited with return code $lightRed$retCode$red.$reset" + ) + } } + (process, onExitProcess) } - - Some((process, onExitProcess)) - - case Left(command) => - for (arg <- command) - println(arg) - None + case Left(commands) => + for { + command <- commands + arg <- command + } println(arg) + Seq.empty } } } @@ -236,10 +238,10 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { val shouldBuildTestScope = options.shared.scope.test.getOrElse(false) if options.sharedRun.watch.watchMode then { - /** A handle to the Runner process, used to kill the process if it's still alive when a change - * occured and restarts are allowed or to wait for it if restarts are not allowed + /** A handle to the Runner processes, used to kill the process if it's still alive when a + * change occured and restarts are allowed or to wait for it if restarts are not allowed */ - val processOpt = AtomicReference(Option.empty[(Process, CompletableFuture[_])]) + val processesRef = AtomicReference(Seq.empty[(Process, CompletableFuture[?])]) /** shouldReadInput controls whether [[WatchUtil.waitForCtrlC]](that's keeping the main thread * alive) should try to read StdIn or just call wait() @@ -263,22 +265,23 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { partial = None, actionableDiagnostics = actionableDiagnostics, postAction = () => - if processOpt.get().exists(_._1.isAlive()) then WatchUtil.printWatchWhileRunningMessage() + if processesRef.get().exists(_._1.isAlive()) then + WatchUtil.printWatchWhileRunningMessage() else WatchUtil.printWatchMessage() ) { res => - for ((process, onExitProcess) <- processOpt.get()) { + for ((process, onExitProcess) <- processesRef.get()) { onExitProcess.cancel(true) ProcUtil.interruptProcess(process, logger) } 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) + for ((proc, _) <- processesRef.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( + val maybeProcesses = maybeRun( successfulBuilds, allowTerminate = false, runMode = runMode(options), @@ -286,6 +289,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { scratchDirOpt = scratchDirOpt(options) ) .orReport(logger) + .toSeq .flatten .map { case (proc, onExit) => @@ -297,9 +301,9 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { (proc, onExit) } successfulBuilds.foreach(_.copyOutput(options.shared)) - if options.sharedRun.watch.restart then processOpt.set(maybeProcess) + if options.sharedRun.watch.restart then processesRef.set(maybeProcesses) else { - for ((proc, onExit) <- maybeProcess) + for ((proc, onExit) <- maybeProcesses) ProcUtil.waitForProcess(proc, onExit) shouldReadInput.set(true) mainThreadOpt.get().foreach(_.interrupt()) @@ -326,22 +330,22 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { } else Build.build( - inputs, - initialBuildOptions, - compilerMaker, - None, - logger, + inputs = inputs, + options = initialBuildOptions, + compilerMaker = compilerMaker, + docCompilerMakerOpt = None, + logger = logger, crossBuilds = cross, buildTests = shouldBuildTestScope, partial = None, actionableDiagnostics = actionableDiagnostics ) .orExit(logger) - .builds match { + .all match { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } successfulBuilds.foreach(_.copyOutput(options.shared)) - val res = maybeRun( + val results = maybeRun( successfulBuilds, allowTerminate = true, runMode = runMode(options), @@ -349,7 +353,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { scratchDirOpt = scratchDirOpt(options) ) .orExit(logger) - for ((process, onExit) <- res) + for ((process, onExit) <- results) ProcUtil.waitForProcess(process, onExit) case b if b.exists(bb => !bb.success && !bb.cancelled) => System.err.println("Compilation failed") @@ -369,8 +373,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { showCommand: Boolean, scratchDirOpt: Option[os.Path], asJar: Boolean - ): Either[BuildException, Either[Seq[String], (Process, Option[() => Unit])]] = either { - + ): Either[BuildException, Either[Seq[Seq[String]], Seq[(Process, Option[() => Unit])]]] = either { val mainClassOpt = builds.head.options.mainClass.filter(_.nonEmpty) // trim it too? .orElse { if builds.head.options.jmhOptions.enableJmh.contains( @@ -441,7 +444,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { } private def runOnce( - builds: Seq[Build.Successful], + allBuilds: Seq[Build.Successful], mainClass: String, args: Seq[String], logger: Logger, @@ -450,215 +453,232 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { showCommand: Boolean, scratchDirOpt: Option[os.Path], asJar: Boolean - ): Either[BuildException, Either[Seq[String], (Process, Option[() => Unit])]] = either { - builds.head.options.platform.value match { - case Platform.JS => - val esModule = - builds.head.options.scalaJsOptions.moduleKindStr.exists(m => m == "es" || m == "esmodule") - - val linkerConfig = builds.head.options.scalaJsOptions.linkerConfig(logger) - val jsDest = { - val delete = scratchDirOpt.isEmpty - scratchDirOpt.foreach(os.makeDir.all(_)) - os.temp( - dir = scratchDirOpt.orNull, - prefix = "main", - suffix = if (esModule) ".mjs" else ".js", - deleteOnExit = delete - ) - } - val res = - Package.linkJs( - builds, - jsDest, - Some(mainClass), - addTestInitializer = false, - linkerConfig, - value(builds.head.options.scalaJsOptions.fullOpt), - builds.head.options.scalaJsOptions.noOpt.getOrElse(false), - logger, - scratchDirOpt - ).map { outputPath => - val jsDom = builds.head.options.scalaJsOptions.dom.getOrElse(false) - if (showCommand) - Left(Runner.jsCommand(outputPath.toIO, args, jsDom = jsDom)) - else { - val process = value { - Runner.runJs( - outputPath.toIO, - args, - logger, - allowExecve = allowExecve, - jsDom = jsDom, - sourceMap = builds.head.options.scalaJsOptions.emitSourceMaps, - esModule = esModule + ): Either[BuildException, Either[Seq[Seq[String]], Seq[(Process, Option[() => Unit])]]] = { + allBuilds + .groupedByCrossParams.toSeq + .map { (crossBuildParams, builds) => + logger.debug( + s"Running build for Scala '${crossBuildParams.scalaVersion}' and platform '${crossBuildParams.platform}'" + ) + val build = builds.head + either { + build.options.platform.value match { + case Platform.JS => + val esModule = + build.options.scalaJsOptions.moduleKindStr.exists(m => m == "es" || m == "esmodule") + + val linkerConfig = builds.head.options.scalaJsOptions.linkerConfig(logger) + val jsDest = { + val delete = scratchDirOpt.isEmpty + scratchDirOpt.foreach(os.makeDir.all(_)) + os.temp( + dir = scratchDirOpt.orNull, + prefix = "main", + suffix = if (esModule) ".mjs" else ".js", + deleteOnExit = delete ) } - process.onExit().thenApply(_ => if (os.exists(jsDest)) os.remove(jsDest)) - Right((process, None)) - } - } - value(res) - case Platform.Native => - val setupPython = builds.head.options.notForBloopOptions.doSetupPython.getOrElse(false) - val (pythonExecutable, pythonLibraryPaths, pythonExtraEnv) = - if (setupPython) { - val (exec, libPaths) = value { - val python = Python() - val pythonPropertiesOrError = for { - paths <- python.nativeLibraryPaths - executable <- python.executable - } yield (Some(executable), paths) - logger.debug(s"Python executable and native library paths: $pythonPropertiesOrError") - pythonPropertiesOrError.orPythonDetectionError - } - // Putting the workspace in PYTHONPATH, see - // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 - // for context. - (exec, libPaths, pythonPathEnv(builds.head.inputs.workspace)) - } - else - (None, Nil, Map()) - // seems conda doesn't add the lib directory to LD_LIBRARY_PATH (see conda/conda#308), - // which prevents apps from finding libpython for example, so we update it manually here - val libraryPathsEnv = - if (pythonLibraryPaths.isEmpty) Map.empty - else { - val prependTo = - if (Properties.isWin) EnvVar.Misc.path.name - else if (Properties.isMac) EnvVar.Misc.dyldLibraryPath.name - else EnvVar.Misc.ldLibraryPath.name - val currentOpt = Option(System.getenv(prependTo)) - val currentEntries = currentOpt - .map(_.split(File.pathSeparator).toSet) - .getOrElse(Set.empty) - val additionalEntries = pythonLibraryPaths.filter(!currentEntries.contains(_)) - if (additionalEntries.isEmpty) - Map.empty - else { - val newValue = - (additionalEntries.iterator ++ currentOpt.iterator).mkString(File.pathSeparator) - Map(prependTo -> newValue) - } - } - val programNameEnv = - pythonExecutable.fold(Map.empty)(py => Map("SCALAPY_PYTHON_PROGRAMNAME" -> py)) - val extraEnv = libraryPathsEnv ++ programNameEnv ++ pythonExtraEnv - val maybeResult = withNativeLauncher( - builds, - mainClass, - logger - ) { launcher => - if (showCommand) - Left( - extraEnv.toVector.sorted.map { case (k, v) => s"$k=$v" } ++ - Seq(launcher.toString) ++ - args - ) - else { - val proc = Runner.runNative( - launcher.toIO, - args, - logger, - allowExecve = allowExecve, - extraEnv = extraEnv - ) - Right((proc, None)) - } - } - value(maybeResult) - case Platform.JVM => - runMode match { - case RunMode.Default => - 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 { - val python = Python() - val propsOrError = python.scalapyProperties - logger.debug(s"Python Java properties: $propsOrError") - propsOrError.orPythonDetectionError + val res = + Package.linkJs( + builds, + jsDest, + Some(mainClass), + addTestInitializer = false, + linkerConfig, + value(build.options.scalaJsOptions.fullOpt), + build.options.scalaJsOptions.noOpt.getOrElse(false), + logger, + scratchDirOpt + ).map { outputPath => + val jsDom = build.options.scalaJsOptions.dom.getOrElse(false) + if (showCommand) + Left(Runner.jsCommand(outputPath.toIO, args, jsDom = jsDom)) + else { + val process = value { + Runner.runJs( + outputPath.toIO, + args, + logger, + allowExecve = allowExecve, + jsDom = jsDom, + sourceMap = build.options.scalaJsOptions.emitSourceMaps, + esModule = esModule + ) + } + process.onExit().thenApply(_ => if (os.exists(jsDest)) os.remove(jsDest)) + Right((process, None)) + } } - val props = scalapyProps.toVector.sorted.map { - case (k, v) => s"-D$k=$v" + value(res) + case Platform.Native => + val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false) + val (pythonExecutable, pythonLibraryPaths, pythonExtraEnv) = + if (setupPython) { + val (exec, libPaths) = value { + val python = Python() + val pythonPropertiesOrError = for { + paths <- python.nativeLibraryPaths + executable <- python.executable + } yield (Some(executable), paths) + logger.debug( + s"Python executable and native library paths: $pythonPropertiesOrError" + ) + pythonPropertiesOrError.orPythonDetectionError + } + // Putting the workspace in PYTHONPATH, see + // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 + // for context. + (exec, libPaths, pythonPathEnv(builds.head.inputs.workspace)) } - // Putting the workspace in PYTHONPATH, see - // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 - // for context. - (props, pythonPathEnv(builds.head.inputs.workspace)) - } - else - (Nil, Map.empty[String, String]) - val allJavaOpts = pythonJavaProps ++ baseJavaProps - 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( - builds.head.options.javaHome().value.javaCommand, - allJavaOpts, - builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, - mainClass, - args, - logger, - allowExecve = allowExecve, - extraEnv = pythonExtraEnv, - useManifest = builds.head.options.notForBloopOptions.runWithManifest, - scratchDirOpt = scratchDirOpt - ) - Right((proc, None)) - } - case mode: RunMode.SparkSubmit => - value { - RunSpark.run( - builds, - mainClass, - args, - mode.submitArgs, - logger, - allowExecve, - showCommand, - scratchDirOpt - ) - } - case mode: RunMode.StandaloneSparkSubmit => - value { - RunSpark.runStandalone( - builds, - mainClass, - args, - mode.submitArgs, - logger, - allowExecve, - showCommand, - scratchDirOpt - ) - } - case RunMode.HadoopJar => - value { - RunHadoop.run( + else + (None, Nil, Map()) + // seems conda doesn't add the lib directory to LD_LIBRARY_PATH (see conda/conda#308), + // which prevents apps from finding libpython for example, so we update it manually here + val libraryPathsEnv = + if (pythonLibraryPaths.isEmpty) Map.empty + else { + val prependTo = + if (Properties.isWin) EnvVar.Misc.path.name + else if (Properties.isMac) EnvVar.Misc.dyldLibraryPath.name + else EnvVar.Misc.ldLibraryPath.name + val currentOpt = Option(System.getenv(prependTo)) + val currentEntries = currentOpt + .map(_.split(File.pathSeparator).toSet) + .getOrElse(Set.empty) + val additionalEntries = pythonLibraryPaths.filter(!currentEntries.contains(_)) + if (additionalEntries.isEmpty) + Map.empty + else { + val newValue = + (additionalEntries.iterator ++ currentOpt.iterator).mkString( + File.pathSeparator + ) + Map(prependTo -> newValue) + } + } + val programNameEnv = + pythonExecutable.fold(Map.empty)(py => Map("SCALAPY_PYTHON_PROGRAMNAME" -> py)) + val extraEnv = libraryPathsEnv ++ programNameEnv ++ pythonExtraEnv + val maybeResult = withNativeLauncher( builds, mainClass, - args, - logger, - allowExecve, - showCommand, - scratchDirOpt - ) - } + logger + ) { launcher => + if (showCommand) + Left( + extraEnv.toVector.sorted.map { case (k, v) => s"$k=$v" } ++ + Seq(launcher.toString) ++ + args + ) + else { + val proc = Runner.runNative( + launcher.toIO, + args, + logger, + allowExecve = allowExecve, + extraEnv = extraEnv + ) + Right((proc, None)) + } + } + value(maybeResult) + 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 (pythonJavaProps, pythonExtraEnv) = + if (setupPython) { + val scalapyProps = value { + val python = Python() + val propsOrError = python.scalapyProperties + logger.debug(s"Python Java properties: $propsOrError") + propsOrError.orPythonDetectionError + } + val props = scalapyProps.toVector.sorted.map { + case (k, v) => s"-D$k=$v" + } + // Putting the workspace in PYTHONPATH, see + // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174 + // for context. + (props, pythonPathEnv(build.inputs.workspace)) + } + else + (Nil, Map.empty[String, String]) + val allJavaOpts = pythonJavaProps ++ baseJavaProps + if showCommand then + Left { + Runner.jvmCommand( + build.options.javaHome().value.javaCommand, + allJavaOpts, + builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, + mainClass, + args, + extraEnv = pythonExtraEnv, + useManifest = build.options.notForBloopOptions.runWithManifest, + scratchDirOpt = scratchDirOpt + ) + } + else { + val proc = Runner.runJvm( + build.options.javaHome().value.javaCommand, + allJavaOpts, + builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, + mainClass, + args, + logger, + allowExecve = allowExecve, + extraEnv = pythonExtraEnv, + useManifest = build.options.notForBloopOptions.runWithManifest, + scratchDirOpt = scratchDirOpt + ) + Right((proc, None)) + } + case mode: RunMode.SparkSubmit => + value { + RunSpark.run( + builds, + mainClass, + args, + mode.submitArgs, + logger, + allowExecve, + showCommand, + scratchDirOpt + ) + } + case mode: RunMode.StandaloneSparkSubmit => + value { + RunSpark.runStandalone( + builds, + mainClass, + args, + mode.submitArgs, + logger, + allowExecve, + showCommand, + scratchDirOpt + ) + } + case RunMode.HadoopJar => + value { + RunHadoop.run( + builds, + mainClass, + args, + logger, + allowExecve, + showCommand, + scratchDirOpt + ) + } + } + } } - } + } + .sequence + .left.map(CompositeBuildException(_)) + .right.map(_.sequence.left.map(_.toSeq)) } def withLinkedJs[T]( diff --git a/modules/cli/src/main/scala/scala/cli/commands/util/BuildCommandHelpers.scala b/modules/cli/src/main/scala/scala/cli/commands/util/BuildCommandHelpers.scala index 2b8935d203..b17be7bb1e 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/util/BuildCommandHelpers.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/util/BuildCommandHelpers.scala @@ -7,6 +7,16 @@ import scala.cli.commands.shared.SharedOptions import scala.cli.commands.util.ScalacOptionsUtil.* trait BuildCommandHelpers { self: ScalaCommand[?] => + case class CrossBuildParams(scalaVersion: String, platform: String) + extension (b: Seq[Build.Successful]) { + def groupedByCrossParams: Map[CrossBuildParams, Seq[Build.Successful]] = + b.groupBy { b => + CrossBuildParams( + b.options.scalaOptions.scalaVersion.map(_.asString).toString, + b.options.platform.toString + ) + } + } extension (successfulBuild: Build.Successful) { def retainedMainClass( logger: Logger, diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala index 50a38d3edc..39ee534fe6 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala @@ -163,14 +163,14 @@ class RunTestsDefault extends RunTestDefinitions s"run --cross $platformDesc $actualScalaVersion, ${Constants.scala213} and ${Constants.scala212} ($scopeDesc scope)" ) { TestUtil.retryOnCi() { - TestInputs { + TestInputs( + os.rel / "project.scala" -> s"//> using scala $actualScalaVersion ${Constants.scala213} ${Constants.scala212}", os.rel / fileName -> - s"""//> using scala $actualScalaVersion ${Constants.scala213} ${Constants.scala212} - |object Main extends App { + s"""object Main extends App { | println("$expectedMessage") |} |""".stripMargin - }.fromRoot { root => + ).fromRoot { root => val r = os.proc( TestUtil.cli, @@ -183,7 +183,10 @@ class RunTestsDefault extends RunTestDefinitions platformOptions ) .call(cwd = root) - expect(r.out.trim() == expectedMessage) + expect(r.out.trim() == + s"""$expectedMessage + |$expectedMessage + |$expectedMessage""".stripMargin) } } } From ac753950890456eeead1fa9dfeef120fb5b92751 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Tue, 29 Jul 2025 13:21:54 +0200 Subject: [PATCH 2/3] Log cross builds information when --cross is not enabled (and some other things, too) --- .../src/main/scala/scala/build/Build.scala | 36 ++-- .../scala/scala/build/CrossBuildParams.scala | 17 ++ .../scala/scala/cli/commands/run/Run.scala | 167 ++++++++++-------- .../commands/util/BuildCommandHelpers.scala | 10 +- .../cli/integration/RunTestsDefault.scala | 58 ++++-- 5 files changed, 177 insertions(+), 111 deletions(-) create mode 100644 modules/build/src/main/scala/scala/build/CrossBuildParams.scala diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index 2246c6d923..e77893b391 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -447,8 +447,19 @@ object Build { extraBuilds.flatMap(_.testDocOpt) ) } - else + else { + if crossOptions.nonEmpty then { + val crossBuildParams: Seq[CrossBuildParams] = crossOptions.map(CrossBuildParams(_)) + logger.message( + s"""Cross-building is disabled, ignoring ${crossOptions.length} builds: + | ${crossBuildParams.map(_.asString).mkString("\n ")} + |Cross builds are only available when the --cross option is passed. + |Defaulting to ${CrossBuildParams(options).asString} + |""".stripMargin + ) + } (Nil, Nil, Nil, Nil) + } Builds( builds = Seq(nonCrossBuilds.main) ++ nonCrossBuilds.testOpt.toSeq, @@ -610,7 +621,7 @@ object Build { actionableDiagnostics: Option[Boolean] )(using ScalaCliInvokeData): Either[BuildException, Builds] = either { val buildClient = BloopBuildClient.create( - logger, + logger = logger, keepDiagnostics = options.internal.keepDiagnostics ) val classesDir0 = classesRootDir(inputs.workspace, inputs.projectName) @@ -628,14 +639,15 @@ object Build { ) value { compilerMaker.withCompiler( - inputs0.workspace / Constants.workspaceDirName, - classesDir0, - buildClient, - logger, - buildOptions + workspace = inputs0.workspace / Constants.workspaceDirName, + classesDir = classesDir0, + buildClient = buildClient, + logger = logger, + buildOptions = buildOptions ) { compiler => docCompilerMakerOpt match { case None => + logger.debug("No doc compiler provided, skipping") build( inputs = inputs0, crossSources = crossSources, @@ -651,11 +663,11 @@ object Build { ) case Some(docCompilerMaker) => docCompilerMaker.withCompiler( - inputs0.workspace / Constants.workspaceDirName, - classesDir0, // ??? - buildClient, - logger, - buildOptions + workspace = inputs0.workspace / Constants.workspaceDirName, + classesDir = classesDir0, // ??? + buildClient = buildClient, + logger = logger, + buildOptions = buildOptions ) { docCompiler => build( inputs = inputs0, diff --git a/modules/build/src/main/scala/scala/build/CrossBuildParams.scala b/modules/build/src/main/scala/scala/build/CrossBuildParams.scala new file mode 100644 index 0000000000..066ed54b46 --- /dev/null +++ b/modules/build/src/main/scala/scala/build/CrossBuildParams.scala @@ -0,0 +1,17 @@ +package scala.build + +import scala.build.internal.Constants +import scala.build.options.BuildOptions + +case class CrossBuildParams(scalaVersion: String, platform: String) { + def asString: String = s"Scala $scalaVersion, $platform" +} + +object CrossBuildParams { + def apply(buildOptions: BuildOptions) = new CrossBuildParams( + scalaVersion = buildOptions.scalaOptions.scalaVersion + .map(_.asString) + .getOrElse(Constants.defaultScalaVersion), + platform = buildOptions.platform.value.repr + ) +} 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 26c09b0462..c096daf50f 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 @@ -84,7 +84,11 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { jvmIdOpt = baseOptions.javaOptions.jvmIdOpt.orElse { runMode(options) match { case _: RunMode.Spark | RunMode.HadoopJar => - Some(Positioned.none("8")) + val sparkOrHadoopDefaultJvm = "8" + logger.message( + s"Defaulting the JVM to $sparkOrHadoopDefaultJvm for Spark/Hadoop runs." + ) + Some(Positioned.none(sparkOrHadoopDefaultJvm)) case RunMode.Default => None } } @@ -122,11 +126,14 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { val shouldDefaultServerFalse = inputArgs.isEmpty && options0.shared.compilationServer.server.isEmpty && !options0.shared.hasSnippets - val options = if (shouldDefaultServerFalse) options0.copy(shared = - options0.shared.copy(compilationServer = - options0.shared.compilationServer.copy(server = Some(false)) + val options = if (shouldDefaultServerFalse) { + logger.debug("No inputs provided, skipping the build server.") + options0.copy(shared = + options0.shared.copy(compilationServer = + options0.shared.compilationServer.copy(server = Some(false)) + ) ) - ) + } else options0 val initialBuildOptions = { val buildOptions = buildOptionsOrExit(options) @@ -170,15 +177,15 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { else { val processOrCommand: Either[Seq[Seq[String]], Seq[(Process, Option[() => Unit])]] = value { maybeRunOnce( - builds, - programArgs, - logger, + builds = builds, + args = programArgs, + logger = logger, allowExecve = allowTerminate, jvmRunner = builds.exists(_.artifacts.hasJvmRunner), - potentialMainClasses, - runMode, - showCommand, - scratchDirOpt, + potentialMainClasses = potentialMainClasses, + runMode = runMode, + showCommand = showCommand, + scratchDirOpt = scratchDirOpt, asJar = options.shared.asJar ) } @@ -219,13 +226,17 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { } val cross = options.sharedRun.compileCross.cross.getOrElse(false) + if cross then + logger.log( + "Cross builds enabled, preparing all builds for all Scala versions and platforms..." + ) SetupIde.runSafe( - options.shared, - inputs, - logger, - initialBuildOptions, - Some(name), - inputArgs + options = options.shared, + inputs = inputs, + logger = logger, + buildOptions = initialBuildOptions, + previousCommandName = Some(name), + args = inputArgs ) if CommandUtils.shouldCheckUpdate then Update.checkUpdateSafe(logger) @@ -236,6 +247,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { ) val shouldBuildTestScope = options.shared.scope.test.getOrElse(false) + if shouldBuildTestScope then + logger.log("Test scope enabled, including test scope inputs on the classpath...") if options.sharedRun.watch.watchMode then { /** A handle to the Runner processes, used to kill the process if it's still alive when a @@ -403,11 +416,13 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { .orElse(retainedMainClassesByScope.get(Scope.Test)) .get } + logger.debug(s"Retained main class: $mainClass") val verbosity = builds.head.options.internal.verbosity.getOrElse(0).toString val (finalMainClass, finalArgs) = if (jvmRunner) (Constants.runnerMainClass, mainClass +: verbosity +: args) else (mainClass, args) + logger.debug(s"Final main class: $finalMainClass") val res = runOnce( builds, finalMainClass, @@ -424,8 +439,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { def pythonPathEnv(dirs: os.Path*): Map[String, String] = { val onlySafePaths = sys.env.exists { - case (k, v) => - k.toLowerCase(Locale.ROOT) == "pythonsafepath" && v.nonEmpty + case (k, v) => k.toLowerCase(Locale.ROOT) == "pythonsafepath" && v.nonEmpty } // Don't add unsafe directories to PYTHONPATH if PYTHONSAFEPATH is set, // see https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSAFEPATH @@ -454,12 +468,15 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { scratchDirOpt: Option[os.Path], asJar: Boolean ): Either[BuildException, Either[Seq[Seq[String]], Seq[(Process, Option[() => Unit])]]] = { - allBuilds - .groupedByCrossParams.toSeq + val crossBuilds = allBuilds.groupedByCrossParams.toSeq + val shouldLogCrossInfo = crossBuilds.size > 1 + if shouldLogCrossInfo then + logger.log( + s"Running ${crossBuilds.size} cross builds, one for each Scala version and platform combination." + ) + crossBuilds .map { (crossBuildParams, builds) => - logger.debug( - s"Running build for Scala '${crossBuildParams.scalaVersion}' and platform '${crossBuildParams.platform}'" - ) + if shouldLogCrossInfo then logger.debug(s"Running build for ${crossBuildParams.asString}") val build = builds.head either { build.options.platform.value match { @@ -480,15 +497,15 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { } val res = Package.linkJs( - builds, - jsDest, - Some(mainClass), + builds = builds, + dest = jsDest, + mainClassOpt = Some(mainClass), addTestInitializer = false, - linkerConfig, - value(build.options.scalaJsOptions.fullOpt), - build.options.scalaJsOptions.noOpt.getOrElse(false), - logger, - scratchDirOpt + config = linkerConfig, + fullOpt = value(build.options.scalaJsOptions.fullOpt), + noOpt = build.options.scalaJsOptions.noOpt.getOrElse(false), + logger = logger, + scratchDirOpt = scratchDirOpt ).map { outputPath => val jsDom = build.options.scalaJsOptions.dom.getOrElse(false) if (showCommand) @@ -572,9 +589,9 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { ) else { val proc = Runner.runNative( - launcher.toIO, - args, - logger, + launcher = launcher.toIO, + args = args, + logger = logger, allowExecve = allowExecve, extraEnv = extraEnv ) @@ -621,12 +638,12 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { } else { val proc = Runner.runJvm( - build.options.javaHome().value.javaCommand, - allJavaOpts, - builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, - mainClass, - args, - logger, + javaCommand = build.options.javaHome().value.javaCommand, + javaArgs = allJavaOpts, + classPath = builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct, + mainClass = mainClass, + args = args, + logger = logger, allowExecve = allowExecve, extraEnv = pythonExtraEnv, useManifest = build.options.notForBloopOptions.runWithManifest, @@ -637,39 +654,39 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { case mode: RunMode.SparkSubmit => value { RunSpark.run( - builds, - mainClass, - args, - mode.submitArgs, - logger, - allowExecve, - showCommand, - scratchDirOpt + builds = builds, + mainClass = mainClass, + args = args, + submitArgs = mode.submitArgs, + logger = logger, + allowExecve = allowExecve, + showCommand = showCommand, + scratchDirOpt = scratchDirOpt ) } case mode: RunMode.StandaloneSparkSubmit => value { RunSpark.runStandalone( - builds, - mainClass, - args, - mode.submitArgs, - logger, - allowExecve, - showCommand, - scratchDirOpt + builds = builds, + mainClass = mainClass, + args = args, + submitArgs = mode.submitArgs, + logger = logger, + allowExecve = allowExecve, + showCommand = showCommand, + scratchDirOpt = scratchDirOpt ) } case RunMode.HadoopJar => value { RunHadoop.run( - builds, - mainClass, - args, - logger, - allowExecve, - showCommand, - scratchDirOpt + builds = builds, + mainClass = mainClass, + args = args, + logger = logger, + allowExecve = allowExecve, + showCommand = showCommand, + scratchDirOpt = scratchDirOpt ) } } @@ -693,17 +710,15 @@ 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( - builds, - dest, - mainClassOpt, - addTestInitializer, - config, - fullOpt, - noOpt, - logger - ).map { outputPath => - f(outputPath) - } + builds = builds, + dest = dest, + mainClassOpt = mainClassOpt, + addTestInitializer = addTestInitializer, + config = config, + fullOpt = fullOpt, + noOpt = noOpt, + logger = logger + ).map(outputPath => f(outputPath)) finally if (os.exists(dest)) os.remove(dest) } diff --git a/modules/cli/src/main/scala/scala/cli/commands/util/BuildCommandHelpers.scala b/modules/cli/src/main/scala/scala/cli/commands/util/BuildCommandHelpers.scala index b17be7bb1e..a72d95e5e8 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/util/BuildCommandHelpers.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/util/BuildCommandHelpers.scala @@ -1,21 +1,15 @@ package scala.cli.commands.util import scala.build.errors.BuildException -import scala.build.{Build, Builds, Logger, Os} +import scala.build.{Build, Builds, CrossBuildParams, Logger, Os} import scala.cli.commands.ScalaCommand import scala.cli.commands.shared.SharedOptions import scala.cli.commands.util.ScalacOptionsUtil.* trait BuildCommandHelpers { self: ScalaCommand[?] => - case class CrossBuildParams(scalaVersion: String, platform: String) extension (b: Seq[Build.Successful]) { def groupedByCrossParams: Map[CrossBuildParams, Seq[Build.Successful]] = - b.groupBy { b => - CrossBuildParams( - b.options.scalaOptions.scalaVersion.map(_.asString).toString, - b.options.platform.toString - ) - } + b.groupBy(bb => CrossBuildParams(bb.options)) } extension (successfulBuild: Build.Successful) { def retainedMainClass( diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala index 39ee534fe6..6caa39d5c0 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala @@ -157,20 +157,27 @@ class RunTestsDefault extends RunTestDefinitions scopeDesc = if (useTestScope) "test" else "main" expectedMessage = "Hello, World!" platformOptions <- Seq(Seq("--native"), Seq("--js"), Nil) - platformDesc = platformOptions.headOption.getOrElse("JVM").stripPrefix("--") - } + platformDesc = platformOptions.headOption.map { + case "--native" => "Native" + case "--js" => "JS" + case other => other + }.getOrElse("JVM") + crossScalaVersions = Seq(actualScalaVersion, Constants.scala213, Constants.scala212) + numberOfBuilds = crossScalaVersions.size + testInputs = TestInputs( + os.rel / "project.scala" -> s"//> using scala ${crossScalaVersions.mkString(" ")}", + os.rel / fileName -> + s"""object Main extends App { + | println("$expectedMessage") + |} + |""".stripMargin + ) + } { test( - s"run --cross $platformDesc $actualScalaVersion, ${Constants.scala213} and ${Constants.scala212} ($scopeDesc scope)" + s"run --cross platform $platformDesc Scala ${crossScalaVersions.mkString(" ")} ($scopeDesc scope)" ) { TestUtil.retryOnCi() { - TestInputs( - os.rel / "project.scala" -> s"//> using scala $actualScalaVersion ${Constants.scala213} ${Constants.scala212}", - os.rel / fileName -> - s"""object Main extends App { - | println("$expectedMessage") - |} - |""".stripMargin - ).fromRoot { root => + testInputs.fromRoot { root => val r = os.proc( TestUtil.cli, @@ -183,13 +190,34 @@ class RunTestsDefault extends RunTestDefinitions platformOptions ) .call(cwd = root) - expect(r.out.trim() == - s"""$expectedMessage - |$expectedMessage - |$expectedMessage""".stripMargin) + expect(r.out.trim().linesIterator.count(_.trim() == expectedMessage) == numberOfBuilds) + } + } + } + + test( + s"run without --cross $platformDesc ${crossScalaVersions.mkString(" ")} ($scopeDesc scope)" + ) { + TestUtil.retryOnCi() { + testInputs.fromRoot { root => + val r = + os.proc( + TestUtil.cli, + "run", + ".", + extraOptions, + scopeOptions, + platformOptions + ) + .call(cwd = root, stderr = os.Pipe) + println(r.err.trim()) + expect(r.err.trim().contains(s"Defaulting to Scala $actualScalaVersion, $platformDesc")) + expect(r.err.trim().contains(s"ignoring ${numberOfBuilds - 1} builds")) + expect(r.out.trim() == expectedMessage) } } } + } for { scalaVersion <- TestUtil.legacyScalaVersionsOnePerMinor From 2f077e236e8d3ceff1079ece1fbd1e34bae19468 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Tue, 29 Jul 2025 17:13:24 +0200 Subject: [PATCH 3/3] NIT Misc refactor around running/building --- .../src/main/scala/scala/build/Build.scala | 313 +++++++++--------- .../scala/scala/cli/commands/run/Run.scala | 109 +++--- 2 files changed, 199 insertions(+), 223 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index e77893b391..22d93789c6 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -98,9 +98,9 @@ object Build { .map(decodedToEncoded(_)) // encode back the name of the chosen class .toRight { SeveralMainClassesFoundError( - ::(mainClasses.head, mainClasses.tail.toList), - commandString, - Nil + mainClasses = ::(mainClasses.head, mainClasses.tail.toList), + commandString = commandString, + positions = Nil ) } } @@ -261,9 +261,9 @@ object Build { CrossSources.forInputs( inputs, Sources.defaultPreprocessors( - options.archiveCache, - options.internal.javaClassNameVersionOpt, - () => options.javaHome().value.javaCommand + archiveCache = options.archiveCache, + javaClassNameVersionOpt = options.internal.javaClassNameVersionOpt, + javaCommand = () => options.javaHome().value.javaCommand ), logger, options.suppressWarningOptions, @@ -291,11 +291,11 @@ object Build { case build: Build.Successful => for (sv <- build.project.scalaCompiler.map(_.scalaVersion)) postProcess( - build.generatedSources, - inputs.generatedSrcRoot(scope), - build.output, - logger, - inputs.workspace, + generatedSources = build.generatedSources, + generatedSrcRoot = inputs.generatedSrcRoot(scope), + classesDir = build.output, + logger = logger, + workspace = inputs.workspace, updateSemanticDbs = true, scalaVersion = sv, buildOptions = build.options @@ -341,17 +341,17 @@ object Build { val generatedSources = sources0.generateSources(inputs0.generatedSrcRoot(scope)) val res = build( - inputs0, - sources0, - generatedSources, - options, - scope, - logger, - buildClient, - actualCompiler, - buildTests, - partial, - actionableDiagnostics + inputs = inputs0, + sources = sources0, + generatedSources = generatedSources, + options = options, + scope = scope, + logger = logger, + buildClient = buildClient, + compiler = actualCompiler, + buildTests = buildTests, + partial = partial, + actionableDiagnostics = actionableDiagnostics ) value(res) @@ -362,15 +362,15 @@ object Build { case None => None case Some(docCompiler) => Some(value(doBuildScope( - mainOptions, - mainSources, - Scope.Main, + options = mainOptions, + sources = mainSources, + scope = Scope.Main, actualCompiler = docCompiler ))) } def testBuildOpt(doc: Boolean = false): Either[BuildException, Option[Build]] = either { - if (buildTests) { + if buildTests then { val actualCompilerOpt = if doc then docCompilerOpt else Some(compiler) actualCompilerOpt match { case None => None @@ -397,9 +397,9 @@ object Build { testSources.withExtraSources(mainSources) else testSources doBuildScope( - testOptions0, - finalSources, - Scope.Test, + options = testOptions0, + sources = finalSources, + scope = Scope.Test, actualCompiler = actualCompiler ) case _ => @@ -432,10 +432,9 @@ object Build { val nonCrossBuilds = value(doBuild(BuildOptions())) val (extraMainBuilds, extraTestBuilds, extraDocBuilds, extraDocTestBuilds) = - if (crossBuilds) { + if crossBuilds then { val extraBuilds = value { val maybeBuilds = crossOptions.map(doBuild) - maybeBuilds .sequence .left.map(CompositeBuildException(_)) @@ -454,8 +453,7 @@ object Build { s"""Cross-building is disabled, ignoring ${crossOptions.length} builds: | ${crossBuildParams.map(_.asString).mkString("\n ")} |Cross builds are only available when the --cross option is passed. - |Defaulting to ${CrossBuildParams(options).asString} - |""".stripMargin + |Defaulting to ${CrossBuildParams(options).asString}""".stripMargin ) } (Nil, Nil, Nil, Nil) @@ -475,7 +473,7 @@ object Build { for (testBuild <- builds.get(Scope.Test)) ResourceMapper.copyResourceToClassesDir(testBuild) - if (actionableDiagnostics.getOrElse(true)) { + if actionableDiagnostics.getOrElse(true) then { val projectOptions = builds.get(Scope.Test).getOrElse(builds.main).options projectOptions.logActionableDiagnostics(logger) } @@ -499,30 +497,30 @@ object Build { val build0 = value { buildOnce( - inputs, - sources, - generatedSources, - options, - scope, - logger, - buildClient, - compiler, - partial + inputs = inputs, + sources = sources, + generatedSources = generatedSources, + options = options, + scope = scope, + logger = logger, + buildClient = buildClient, + compiler = compiler, + partialOpt = partial ) } build0 match { case successful: Successful => - if (options.jmhOptions.canRunJmh && scope == Scope.Main) + if options.jmhOptions.canRunJmh && scope == Scope.Main then value { val res = jmhBuild( - inputs, - successful, - logger, + inputs = inputs, + build = successful, + logger = logger, successful.options.javaHome().value.javaCommand, - buildClient, - compiler, - buildTests, + buildClient = buildClient, + compiler = compiler, + buildTests = buildTests, actionableDiagnostics = actionableDiagnostics ) res.flatMap { @@ -568,10 +566,9 @@ object Build { ) ) def warnIncompatibleNativeOptions(numeralVersion: SNNumeralVersion) = - if ( - numeralVersion < SNNumeralVersion(0, 4, 4) + if numeralVersion < SNNumeralVersion(0, 4, 4) && options.scalaNativeOptions.embedResources.isDefined - ) + then logger.diagnostic( "This Scala Version cannot embed resources, regardless of the options used." ) @@ -579,22 +576,19 @@ object Build { val numeralOrError: Either[ScalaNativeCompatibilityError, SNNumeralVersion] = nativeVersionMaybe match { case Some(snNumeralVer) => - if (snNumeralVer < SNNumeralVersion(0, 4, 1) && Properties.isWin) - snCompatError - else if (scalaVersion.startsWith("3.0")) - snCompatError - else if (scalaVersion.startsWith("3")) - if (snNumeralVer >= SNNumeralVersion(0, 4, 3)) Right(snNumeralVer) + if snNumeralVer < SNNumeralVersion(0, 4, 1) && Properties.isWin then snCompatError + else if scalaVersion.startsWith("3.0") then snCompatError + else if scalaVersion.startsWith("3") then + if snNumeralVer >= SNNumeralVersion(0, 4, 3) + then Right(snNumeralVer) else snCompatError - else if (scalaVersion.startsWith("2.13")) - Right(snNumeralVer) - else if (scalaVersion.startsWith("2.12")) - if ( - inputs.sourceFiles().forall { + else if scalaVersion.startsWith("2.13") then Right(snNumeralVer) + else if scalaVersion.startsWith("2.12") then + if inputs.sourceFiles().forall { case _: AnyScript => snNumeralVer >= SNNumeralVersion(0, 4, 3) case _ => true } - ) Right(snNumeralVer) + then Right(snNumeralVer) else snCompatError else snCompatError case None => snCompatError @@ -725,19 +719,19 @@ object Build { val sharedOptions = crossSources.sharedOptions(options) val compiler = value { compilerMaker.create( - inputs0.workspace / Constants.workspaceDirName, - classesDir0, - buildClient, - logger, - sharedOptions + workspace = inputs0.workspace / Constants.workspaceDirName, + classesDir = classesDir0, + buildClient = buildClient, + logger = logger, + buildOptions = sharedOptions ) } val docCompilerOpt = docCompilerMakerOpt.map(_.create( - inputs0.workspace / Constants.workspaceDirName, - classesDir0, - buildClient, - logger, - sharedOptions + workspace = inputs0.workspace / Constants.workspaceDirName, + classesDir = classesDir0, + buildClient = buildClient, + logger = logger, + buildOptions = sharedOptions )).map(value) compiler -> docCompilerOpt } @@ -867,7 +861,7 @@ object Build { logger: Logger ): Option[Int] = { lazy val javaHome = options.javaHome() - if (compilerJvmVersionOpt.exists(javaHome.value.version > _.value)) { + if compilerJvmVersionOpt.exists(javaHome.value.version > _.value) then { logger.log(List(Diagnostic( Diagnostic.Messages.bloopTooOld, Severity.Warning, @@ -875,19 +869,14 @@ object Build { ))) None } - else if (compilerJvmVersionOpt.exists(_.value == 8)) - None - else if ( - options.scalaOptions.scalacOptions.values.exists(opt => + else if compilerJvmVersionOpt.exists(_.value == 8) then None + else if options.scalaOptions.scalacOptions.values.exists(opt => opt.headOption.exists(_.value.value.startsWith("-release")) || opt.headOption.exists(_.value.value.startsWith("-java-output-version")) ) - ) - None - else if (compilerJvmVersionOpt.isEmpty && javaHome.value.version == 8) - None - else - Some(javaHome.value.version) + then None + else if compilerJvmVersionOpt.isEmpty && javaHome.value.version == 8 then None + else Some(javaHome.value.version) } /** Builds a Bloop project. @@ -970,20 +959,19 @@ object Build { else Nil val sourceRootScalacOptions = - if (params.scalaVersion.startsWith("2.")) Nil + if params.scalaVersion.startsWith("2.") + then Nil else Seq("-sourceroot", inputs.workspace.toString).map(ScalacOpt(_)) val scalaJsScalacOptions = - if (options.platform.value == Platform.JS && !params.scalaVersion.startsWith("2.")) - Seq(ScalacOpt("-scalajs")) + if options.platform.value == Platform.JS && !params.scalaVersion.startsWith("2.") + then Seq(ScalacOpt("-scalajs")) else Nil val scalapyOptions = - if ( - params.scalaVersion.startsWith("2.13.") && + if params.scalaVersion.startsWith("2.13.") && options.notForBloopOptions.python.getOrElse(false) - ) - Seq(ScalacOpt("-Yimports:java.lang,scala,scala.Predef,me.shadaj.scalapy")) + then Seq(ScalacOpt("-Yimports:java.lang,scala,scala.Predef,me.shadaj.scalapy")) else Nil val scalacOptions = @@ -1011,7 +999,7 @@ object Build { val semanticDbJavacOptions = // FIXME Should this be in scalaOptions, now that we use it for javac stuff too? - if (generateSemanticDbs) { + if generateSemanticDbs then { // from https://github.com/scalameta/metals/blob/04405c0401121b372ea1971c361e05108fb36193/metals/src/main/scala/scala/meta/internal/metals/JavaInteractiveSemanticdb.scala#L137-L146 val compilerPackages = Seq( "com.sun.tools.javac.api", @@ -1038,8 +1026,8 @@ object Build { // `test` scope should contains class path to main scope val mainClassesPath = - if (scope == Scope.Test) - List(classesDir(inputs.workspace, inputs.projectName, Scope.Main)) + if scope == Scope.Test + then List(classesDir(inputs.workspace, inputs.projectName, Scope.Main)) else Nil value(validate(logger, options)) @@ -1058,12 +1046,12 @@ object Build { scaladocDir = scaladocDir, scalaCompiler = scalaCompilerParamsOpt, scalaJsOptions = - if (options.platform.value == Platform.JS) - Some(value(options.scalaJsOptions.config(logger))) + if options.platform.value == Platform.JS + then Some(value(options.scalaJsOptions.config(logger))) else None, scalaNativeOptions = - if (options.platform.value == Platform.Native) - Some(options.scalaNativeOptions.bloopConfig()) + if options.platform.value == Platform.Native + then Some(options.scalaNativeOptions.bloopConfig()) else None, projectName = inputs.scopeProjectName(scope), classPath = fullClassPath, @@ -1093,7 +1081,8 @@ object Build { val options0 = // FIXME: don't add Scala to pure Java test builds (need to add pure Java test runner) - if (sources.hasJava && !sources.hasScala && scope != Scope.Test) + if sources.hasJava && !sources.hasScala && scope != Scope.Test + then options.copy( scalaOptions = options.scalaOptions.copy( scalaVersion = options.scalaOptions.scalaVersion.orElse { @@ -1101,13 +1090,10 @@ object Build { } ) ) - else - options + else options val params = value(options0.scalaParams) - val scopeParams = - if (scope == Scope.Main) Nil - else Seq(scope.name) + val scopeParams = if scope == Scope.Main then Nil else Seq(scope.name) buildClient.setProjectParams(scopeParams ++ value(options0.projectParams)) @@ -1119,22 +1105,22 @@ object Build { val project = value { buildProject( - inputs, - sources, - generatedSources, - options0, - compilerJvmVersionOpt, - scope, - logger, - artifacts, - maybeRecoverOnError + inputs = inputs, + sources = sources, + generatedSources = generatedSources, + options = options0, + compilerJvmVersionOpt = compilerJvmVersionOpt, + scope = scope, + logger = logger, + artifacts = artifacts, + maybeRecoverOnError = maybeRecoverOnError ) } val projectChanged = compiler.prepareProject(project, logger) - if (projectChanged) { - if (compiler.usesClassDir && os.isDir(classesDir0)) { + if projectChanged then { + if compiler.usesClassDir && os.isDir(classesDir0) then { logger.debug(s"Clearing $classesDir0") os.list(classesDir0).foreach { p => logger.debug(s"Removing $p") @@ -1145,7 +1131,7 @@ object Build { } } } - if (os.exists(project.argsFilePath)) { + if os.exists(project.argsFilePath) then { logger.debug(s"Removing ${project.argsFilePath}") try os.remove(project.argsFilePath) catch { @@ -1170,7 +1156,7 @@ object Build { partialOpt: Option[Boolean] ): Either[BuildException, Build] = either { - if (options.platform.value == Platform.Native) + if options.platform.value == Platform.Native then value(scalaNativeSupported(options, inputs, logger)) match { case None => case Some(error) => value(Left(error)) @@ -1178,15 +1164,15 @@ object Build { val (classesDir0, scalaParams, artifacts, project, projectChanged) = value { prepareBuild( - inputs, - sources, - generatedSources, - options, - compiler.jvmVersion, - scope, - compiler, - logger, - buildClient + inputs = inputs, + sources = sources, + generatedSources = generatedSources, + options = options, + compilerJvmVersionOpt = compiler.jvmVersion, + scope = scope, + compiler = compiler, + logger = logger, + buildClient = buildClient ) } @@ -1199,30 +1185,30 @@ object Build { val success = partial || compiler.compile(project, logger) - if (success) + if success then Successful( - inputs, - options, + inputs = inputs, + options = options, scalaParams, - scope, - sources, - artifacts, - project, - classesDir0, - buildClient.diagnostics, - generatedSources, - partial, - logger + scope = scope, + sources = sources, + artifacts = artifacts, + project = project, + output = classesDir0, + diagnostics = buildClient.diagnostics, + generatedSources = generatedSources, + isPartial = partial, + logger = logger ) else Failed( - inputs, - options, - scope, - sources, - artifacts, - project, - buildClient.diagnostics + inputs = inputs, + options = options, + scope = scope, + sources = sources, + artifacts = artifacts, + project = project, + diagnostics = buildClient.diagnostics ) } @@ -1236,7 +1222,7 @@ object Build { scalaVersion: String, buildOptions: BuildOptions ): Either[Seq[String], Unit] = - if (os.exists(classesDir)) { + if os.exists(classesDir) then { // TODO Write classes to a separate directory during post-processing logger.debug("Post-processing class files of pre-processed sources") @@ -1250,22 +1236,22 @@ object Build { val postProcessors = Seq(ByteCodePostProcessor) ++ - (if (updateSemanticDbs) Seq(SemanticDbPostProcessor) else Nil) ++ + (if updateSemanticDbs then Seq(SemanticDbPostProcessor) else Nil) ++ Seq(TastyPostProcessor) val failures = postProcessors.flatMap( _.postProcess( - generatedSources, - mappings, - workspace, - classesDir, - logger, - scalaVersion, - buildOptions + generatedSources = generatedSources, + mappings = mappings, + workspace = workspace, + output = classesDir, + logger = logger, + scalaVersion = scalaVersion, + buildOptions = buildOptions ) .fold(e => Seq(e), _ => Nil) ) - if (failures.isEmpty) Right(()) else Left(failures) + if failures.isEmpty then Right(()) else Left(failures) } else Right(()) @@ -1277,7 +1263,7 @@ object Build { System.err.println("got error:") @tailrec def printEx(t: Throwable): Unit = - if (t != null) { + if t != null then { System.err.println(t) System.err.println( t.getStackTrace.iterator.map(" " + _ + System.lineSeparator()).mkString @@ -1318,15 +1304,14 @@ object Build { onChange // FIXME Log exceptions } def schedule(): Unit = - if (f == null) + if f == null then lock.synchronized { - if (f == null) - f = scheduler.schedule(runnable, waitFor.length, waitFor.unit) + if f == null then f = scheduler.schedule(runnable, waitFor.length, waitFor.unit) } } private def printable(path: os.Path): String = - if (path.startsWith(os.pwd)) path.relativeTo(os.pwd).toString + if path.startsWith(os.pwd) then path.relativeTo(os.pwd).toString else path.toString private def jmhBuild( @@ -1352,7 +1337,7 @@ object Build { Seq(printable(build.output), printable(jmhSourceDir), printable(jmhResourceDir), "default"), logger ) - if (retCode != 0) { + if retCode != 0 then { val red = Console.RED val lightRed = "\u001b[91m" val reset = Console.RESET @@ -1361,7 +1346,7 @@ object Build { ) } - if (retCode == 0) { + if retCode == 0 then { val jmhInputs = inputs.copy( baseProjectName = jmhProjectName, // hash of the underlying project if needed is already in jmhProjectName 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 c096daf50f..136ca97fd2 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 @@ -43,17 +43,14 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { override def sharedOptions(options: RunOptions): Option[SharedOptions] = Some(options.shared) private def runMode(options: RunOptions): RunMode = - if ( - options.sharedRun.standaloneSpark.getOrElse(false) && + if options.sharedRun.standaloneSpark.getOrElse(false) && !options.sharedRun.sparkSubmit.contains(false) - ) - RunMode.StandaloneSparkSubmit(options.sharedRun.submitArgument) - else if (options.sharedRun.sparkSubmit.getOrElse(false)) - RunMode.SparkSubmit(options.sharedRun.submitArgument) - else if (options.sharedRun.hadoopJar) - RunMode.HadoopJar - else - RunMode.Default + then RunMode.StandaloneSparkSubmit(options.sharedRun.submitArgument) + else if options.sharedRun.sparkSubmit.getOrElse(false) + then RunMode.SparkSubmit(options.sharedRun.submitArgument) + else if options.sharedRun.hadoopJar + then RunMode.HadoopJar + else RunMode.Default private def scratchDirOpt(options: RunOptions): Option[os.Path] = options.sharedRun.scratchDir @@ -62,12 +59,12 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { override def runCommand(options: RunOptions, args: RemainingArgs, logger: Logger): Unit = runCommand( - options, - args.remaining, - args.unparsed, - () => Inputs.default(), - logger, - invokeData + options0 = options, + inputArgs = args.remaining, + programArgs = args.unparsed, + defaultInputs = () => Inputs.default(), + logger = logger, + invokeData = invokeData ) override def buildOptions(options: RunOptions): Some[BuildOptions] = Some { @@ -126,7 +123,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { val shouldDefaultServerFalse = inputArgs.isEmpty && options0.shared.compilationServer.server.isEmpty && !options0.shared.hasSnippets - val options = if (shouldDefaultServerFalse) { + val options = if shouldDefaultServerFalse then { logger.debug("No inputs provided, skipping the build server.") options0.copy(shared = options0.shared.copy(compilationServer = @@ -137,7 +134,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { else options0 val initialBuildOptions = { val buildOptions = buildOptionsOrExit(options) - if (invokeData.subCommand == SubCommand.Shebang) { + if invokeData.subCommand == SubCommand.Shebang then { val suppressDepUpdateOptions = buildOptions.suppressWarningOptions.copy( suppressOutdatedDependencyWarning = Some(true) ) @@ -149,12 +146,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { else buildOptions } - val inputs = options.shared.inputs( - inputArgs, - defaultInputs - )( - using invokeData - ).orExit(logger) + val inputs = options.shared.inputs(inputArgs, defaultInputs)(using invokeData).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) val threads = BuildThreads.create() @@ -168,7 +160,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { scratchDirOpt: Option[os.Path] ): Either[BuildException, Seq[(Process, CompletableFuture[?])]] = either { val potentialMainClasses = builds.flatMap(_.foundMainClasses()).distinct - if (options.sharedRun.mainClass.mainClassLs.contains(true)) + if options.sharedRun.mainClass.mainClassLs.contains(true) then value { options.sharedRun.mainClass .maybePrintMainClasses(potentialMainClasses, shouldExit = allowTerminate) @@ -295,7 +287,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { shouldReadInput.set(false) mainThreadOpt.get().foreach(_.interrupt()) val maybeProcesses = maybeRun( - successfulBuilds, + builds = successfulBuilds, allowTerminate = false, runMode = runMode(options), showCommand = options.sharedRun.command, @@ -306,7 +298,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { .flatten .map { case (proc, onExit) => - if (options.sharedRun.watch.restart) + if options.sharedRun.watch.restart then onExit.thenApply { _ => shouldReadInput.set(true) mainThreadOpt.get().foreach(_.interrupt()) @@ -314,7 +306,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { (proc, onExit) } successfulBuilds.foreach(_.copyOutput(options.shared)) - if options.sharedRun.watch.restart then processesRef.set(maybeProcesses) + if options.sharedRun.watch.restart + then processesRef.set(maybeProcesses) else { for ((proc, onExit) <- maybeProcesses) ProcUtil.waitForProcess(proc, onExit) @@ -330,11 +323,11 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { try WatchUtil.waitForCtrlC( - { () => + onPressEnter = { () => watcher.schedule() shouldReadInput.set(false) }, - () => shouldReadInput.get() + shouldReadInput = () => shouldReadInput.get() ) finally { mainThreadOpt.set(None) @@ -359,7 +352,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { val successfulBuilds = b.collect { case s: Build.Successful => s } successfulBuilds.foreach(_.copyOutput(options.shared)) val results = maybeRun( - successfulBuilds, + builds = successfulBuilds, allowTerminate = true, runMode = runMode(options), showCommand = options.sharedRun.command, @@ -389,9 +382,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { ): Either[BuildException, Either[Seq[Seq[String]], Seq[(Process, Option[() => Unit])]]] = either { val mainClassOpt = builds.head.options.mainClass.filter(_.nonEmpty) // trim it too? .orElse { - if builds.head.options.jmhOptions.enableJmh.contains( - true - ) && !builds.head.options.jmhOptions.canRunJmh + if builds.head.options.jmhOptions.enableJmh + .contains(true) && !builds.head.options.jmhOptions.canRunJmh then Some("org.openjdk.jmh.Main") else None } @@ -420,19 +412,20 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { val verbosity = builds.head.options.internal.verbosity.getOrElse(0).toString val (finalMainClass, finalArgs) = - if (jvmRunner) (Constants.runnerMainClass, mainClass +: verbosity +: args) + if jvmRunner + then (Constants.runnerMainClass, mainClass +: verbosity +: args) else (mainClass, args) logger.debug(s"Final main class: $finalMainClass") val res = runOnce( - builds, - finalMainClass, - finalArgs, - logger, - allowExecve, - runMode, - showCommand, - scratchDirOpt, - asJar + allBuilds = builds, + mainClass = finalMainClass, + args = finalArgs, + logger = logger, + allowExecve = allowExecve, + runMode = runMode, + showCommand = showCommand, + scratchDirOpt = scratchDirOpt, + asJar = asJar ) value(res) } @@ -445,7 +438,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { // see https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSAFEPATH // and https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1336017760 // for more details. - if (onlySafePaths) Map.empty[String, String] + if onlySafePaths then Map.empty[String, String] else { val (pythonPathEnvVarName, currentPythonPath) = sys.env .find(_._1.toLowerCase(Locale.ROOT) == "pythonpath") @@ -491,7 +484,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { os.temp( dir = scratchDirOpt.orNull, prefix = "main", - suffix = if (esModule) ".mjs" else ".js", + suffix = if esModule then ".mjs" else ".js", deleteOnExit = delete ) } @@ -508,8 +501,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { scratchDirOpt = scratchDirOpt ).map { outputPath => val jsDom = build.options.scalaJsOptions.dom.getOrElse(false) - if (showCommand) - Left(Runner.jsCommand(outputPath.toIO, args, jsDom = jsDom)) + if showCommand then Left(Runner.jsCommand(outputPath.toIO, args, jsDom = jsDom)) else { val process = value { Runner.runJs( @@ -522,7 +514,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { esModule = esModule ) } - process.onExit().thenApply(_ => if (os.exists(jsDest)) os.remove(jsDest)) + process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest)) Right((process, None)) } } @@ -530,7 +522,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { case Platform.Native => val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false) val (pythonExecutable, pythonLibraryPaths, pythonExtraEnv) = - if (setupPython) { + if setupPython then { val (exec, libPaths) = value { val python = Python() val pythonPropertiesOrError = for { @@ -552,19 +544,18 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { // seems conda doesn't add the lib directory to LD_LIBRARY_PATH (see conda/conda#308), // which prevents apps from finding libpython for example, so we update it manually here val libraryPathsEnv = - if (pythonLibraryPaths.isEmpty) Map.empty + if pythonLibraryPaths.isEmpty then Map.empty else { val prependTo = - if (Properties.isWin) EnvVar.Misc.path.name - else if (Properties.isMac) EnvVar.Misc.dyldLibraryPath.name + if Properties.isWin then EnvVar.Misc.path.name + else if Properties.isMac then EnvVar.Misc.dyldLibraryPath.name else EnvVar.Misc.ldLibraryPath.name val currentOpt = Option(System.getenv(prependTo)) val currentEntries = currentOpt .map(_.split(File.pathSeparator).toSet) .getOrElse(Set.empty) val additionalEntries = pythonLibraryPaths.filter(!currentEntries.contains(_)) - if (additionalEntries.isEmpty) - Map.empty + if additionalEntries.isEmpty then Map.empty else { val newValue = (additionalEntries.iterator ++ currentOpt.iterator).mkString( @@ -581,7 +572,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { mainClass, logger ) { launcher => - if (showCommand) + if showCommand then Left( extraEnv.toVector.sorted.map { case (k, v) => s"$k=$v" } ++ Seq(launcher.toString) ++ @@ -605,7 +596,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { val baseJavaProps = build.options.javaOptions.javaOpts.toSeq.map(_.value.value) val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false) val (pythonJavaProps, pythonExtraEnv) = - if (setupPython) { + if setupPython then { val scalapyProps = value { val python = Python() val propsOrError = python.scalapyProperties @@ -708,7 +699,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { logger: Logger, esModule: Boolean )(f: os.Path => T): Either[BuildException, T] = { - val dest = os.temp(prefix = "main", suffix = if (esModule) ".mjs" else ".js") + val dest = os.temp(prefix = "main", suffix = if esModule then ".mjs" else ".js") try Package.linkJs( builds = builds, dest = dest, @@ -719,7 +710,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { noOpt = noOpt, logger = logger ).map(outputPath => f(outputPath)) - finally if (os.exists(dest)) os.remove(dest) + finally if os.exists(dest) then os.remove(dest) } def withNativeLauncher[T](