From 8db8c79d337f5c52a8c1dcec876e011cd9468b29 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 9 Mar 2025 13:46:37 -0400 Subject: [PATCH] Implement client-side run **Problem** `run` task has been emulated via function call inside of a sandboxed classloader, and blocking the command processing of sbt server loop. This poses isolation and availability issues. **Solution** This implements client-side run where the server creates a sandbox environment, and sends the information to the client, and the client forks a new JVM to perform the run. The client-side behavior has been implemented in sbtn side already. --- main-command/src/main/scala/sbt/State.scala | 7 +- main/src/main/scala/sbt/Defaults.scala | 189 +---------- main/src/main/scala/sbt/Keys.scala | 9 +- .../src/main/scala/sbt/internal/RunUtil.scala | 302 ++++++++++++++++++ .../scala/sbt/internal/server/ClientJob.scala | 95 ------ .../src/sbt-test/actions/add-alias/build.sbt | 4 +- .../sbt-test/actions/input-task-dyn/build.sbt | 2 +- .../sbt-test/classloader-cache/jni/build.sbt | 2 +- .../dependency-management/aar-packaging/test | 2 +- .../exclude-dependencies2/test | 2 +- .../dependency-management/profiles/test | 2 +- .../stdlib-unfreeze-warn/test | 2 +- .../stdlib-unfreeze/test | 4 +- .../dependency-management/url-no-head/test | 2 +- .../src/sbt-test/lm-coursier/classifiers/test | 2 +- .../sbt-test/lm-coursier/from-no-head/test | 2 +- .../sbt-test/lm-coursier/from-wrong-url/test | 2 +- sbt-app/src/sbt-test/lm-coursier/from/test | 2 +- .../lm-coursier/maven-compatible/test | 2 +- .../lm-coursier/per-config-resolution/test | 2 +- .../src/sbt-test/lm-coursier/scala-jars/test | 2 +- .../scala-sources-javadoc-jars/test | 2 +- sbt-app/src/sbt-test/lm-coursier/simple/test | 2 +- .../src/sbt-test/lm-coursier/zookeeper/test | 2 +- sbt-app/src/sbt-test/package/resources/test | 4 +- .../source-dependencies/constants/test | 8 +- .../source-dependencies/implicit-search/test | 4 +- .../source-dependencies/java-basic/test | 4 +- .../source-dependencies/linearization/test | 4 +- .../sbt-test/source-dependencies/named/test | 4 +- .../source-dependencies/specialized/test | 4 +- .../src/test/scala/testpkg/ClientTest.scala | 8 + 32 files changed, 364 insertions(+), 320 deletions(-) create mode 100644 main/src/main/scala/sbt/internal/RunUtil.scala delete mode 100644 main/src/main/scala/sbt/internal/server/ClientJob.scala diff --git a/main-command/src/main/scala/sbt/State.scala b/main-command/src/main/scala/sbt/State.scala index eb0a3a106..c26b241a2 100644 --- a/main-command/src/main/scala/sbt/State.scala +++ b/main-command/src/main/scala/sbt/State.scala @@ -61,10 +61,13 @@ final case class State( } def source: Option[CommandSource] = - currentCommand match { + currentCommand match case Some(x) => x.source case _ => None - } + def isNetworkCommand: Boolean = + source match + case Some(s) => s.channelName.startsWith("network") + case _ => false } /** diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index a8ad44213..287c894dd 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -52,7 +52,6 @@ import sbt.internal.server.{ BspCompileTask, BuildServerProtocol, BuildServerReporter, - ClientJob, Definition, LanguageServerProtocol, ServerHandler, @@ -141,6 +140,7 @@ import xsbti.compile.{ TransactionalManagerType } import sbt.internal.IncrementalTest +import sbt.internal.RunUtil object Defaults extends BuildCommon { final val CacheDirectoryName = "cache" @@ -241,7 +241,7 @@ object Defaults extends BuildCommon { getRootPaths(out, app) + ("CSR_CACHE" -> coursierCache) }, fileConverter := MappedFileConverter(rootPaths.value, allowMachinePath.value) - ) ++ BuildServerProtocol.globalSettings ++ ClientJob.globalSettings + ) ++ BuildServerProtocol.globalSettings private[sbt] def getRootPaths(out: NioPath, app: AppConfiguration): ListMap[String, NioPath] = val base = app.baseDirectory.getCanonicalFile.toPath @@ -391,6 +391,7 @@ object Defaults extends BuildCommon { aggregate :== true, maxErrors :== 100, fork :== false, + clientSide :== true, initialize :== {}, templateResolverInfos :== Nil, templateDescriptions :== TemplateCommandUtil.defaultTemplateDescriptions, @@ -1035,27 +1036,12 @@ object Defaults extends BuildCommon { }) pickMainClassOrWarn(discoveredMainClasses.value, streams.value.log, logWarning) }, - runMain := foregroundRunMainTask.evaluated, - run := foregroundRunTask.evaluated, - runBlock := { - val r = run.evaluated - val service = bgJobService.value - service.waitForTry(r.handle).get - () - }, - runMainBlock := { - val r = runMain.evaluated - val service = bgJobService.value - service.waitForTry(r.handle).get - () - }, fgRun := runTask(fullClasspath, (run / mainClass), (run / runner)).evaluated, fgRunMain := runMainTask(fullClasspath, (run / runner)).evaluated, copyResources := copyResourcesTask.value, - // note that we use the same runner and mainClass as plain run - mainBgRunMainTaskForConfig(This), - mainBgRunTaskForConfig(This) - ) ++ inTask(run)(runnerSettings ++ newRunnerSettings) ++ compileIncrementalTaskSettings + ) ++ RunUtil.configTasks(This) ++ inTask(run)( + runnerSettings ++ newRunnerSettings + ) ++ compileIncrementalTaskSettings private lazy val configGlobal = globalDefaults( Seq( @@ -1869,122 +1855,6 @@ object Defaults extends BuildCommon { /** Implements `cleanFiles` task. */ private[sbt] def cleanFilesTask: Initialize[Task[Vector[File]]] = Def.task { Vector.empty[File] } - private def termWrapper(canonical: Boolean, echo: Boolean): (() => Unit) => (() => Unit) = - (f: () => Unit) => - () => { - val term = ITerminal.get - if (!canonical) { - term.enterRawMode() - if (echo) term.setEchoEnabled(echo) - } else if (!echo) term.setEchoEnabled(false) - try f() - finally { - if (!canonical) term.exitRawMode() - if (!echo) term.setEchoEnabled(true) - } - } - def bgRunMainTask( - products: Initialize[Task[Classpath]], - classpath: Initialize[Task[Classpath]], - copyClasspath: Initialize[Boolean], - scalaRun: Initialize[Task[ScalaRun]] - ): Initialize[InputTask[JobHandle]] = { - val parser = Defaults.loadForParser(discoveredMainClasses)((s, names) => - Defaults.runMainParser(s, names getOrElse Nil) - ) - Def.inputTask { - val service = bgJobService.value - val (mainClass, args) = parser.parsed - val hashClasspath = (bgRunMain / bgHashClasspath).value - val wrapper = termWrapper(canonicalInput.value, echoInput.value) - val converter = fileConverter.value - service.runInBackgroundWithLoader(resolvedScoped.value, state.value) { (logger, workingDir) => - val cp = - if copyClasspath.value then - service.copyClasspath( - products.value, - classpath.value, - workingDir, - hashClasspath, - converter, - ) - else classpath.value - given FileConverter = fileConverter.value - scalaRun.value match - case r: Run => - val loader = r.newLoader(cp.files) - ( - Some(loader), - wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get) - ) - case sr => - (None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get)) - } - } - } - - def bgRunTask( - products: Initialize[Task[Classpath]], - classpath: Initialize[Task[Classpath]], - mainClassTask: Initialize[Task[Option[String]]], - copyClasspath: Initialize[Boolean], - scalaRun: Initialize[Task[ScalaRun]] - ): Initialize[InputTask[JobHandle]] = - val parser = Def.spaceDelimited() - Def.inputTask { - val args = parser.parsed - val service = bgJobService.value - val mainClass = mainClassTask.value getOrElse sys.error("No main class detected.") - val hashClasspath = (bgRun / bgHashClasspath).value - val wrapper = termWrapper(canonicalInput.value, echoInput.value) - val converter = fileConverter.value - service.runInBackgroundWithLoader(resolvedScoped.value, state.value) { (logger, workingDir) => - val cp = - if copyClasspath.value then - service.copyClasspath( - products.value, - classpath.value, - workingDir, - hashClasspath, - converter - ) - else classpath.value - given FileConverter = converter - scalaRun.value match - case r: Run => - val loader = r.newLoader(cp.files) - ( - Some(loader), - wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get) - ) - case sr => - (None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get)) - } - } - - // `runMain` calls bgRunMain in the background and pauses the current channel - def foregroundRunMainTask: Initialize[InputTask[EmulateForeground]] = - Def.inputTask { - val handle = bgRunMain.evaluated - handle match - case threadJobHandle: AbstractBackgroundJobService#ThreadJobHandle => - threadJobHandle.isAutoCancel = true - case _ => () - EmulateForeground(handle) - } - - // `run` task calls bgRun in the background and pauses the current channel - def foregroundRunTask: Initialize[InputTask[EmulateForeground]] = - Def.inputTask { - val handle = bgRun.evaluated - handle match { - case threadJobHandle: AbstractBackgroundJobService#ThreadJobHandle => - threadJobHandle.isAutoCancel = true - case _ => - } - EmulateForeground(handle) - } - def runMainTask( classpath: Initialize[Task[Classpath]], scalaRun: Initialize[Task[ScalaRun]] @@ -2005,15 +1875,7 @@ object Defaults extends BuildCommon { classpath: Initialize[Task[Classpath]], mainClassTask: Initialize[Task[Option[String]]], scalaRun: Initialize[Task[ScalaRun]] - ): Initialize[InputTask[Unit]] = - val parser = Def.spaceDelimited() - Def.inputTask { - val in = parser.parsed - val mainClass = mainClassTask.value getOrElse sys.error("No main class detected.") - val cp = classpath.value - given FileConverter = fileConverter.value - scalaRun.value.run(mainClass, cp.files, in, streams.value.log).get - } + ): Initialize[InputTask[Unit]] = RunUtil.serverSideRunTask(classpath, mainClassTask, scalaRun) def runnerTask: Setting[Task[ScalaRun]] = runner := runnerInit.value @@ -2173,26 +2035,6 @@ object Defaults extends BuildCommon { ) ) - def mainBgRunTask = mainBgRunTaskForConfig(Select(Runtime)) - def mainBgRunMainTask = mainBgRunMainTaskForConfig(Select(Runtime)) - - private def mainBgRunTaskForConfig(c: ScopeAxis[ConfigKey]) = - bgRun := bgRunTask( - exportedProductJars, - This / c / This / fullClasspathAsJars, - run / mainClass, - bgRun / bgCopyClasspath, - run / runner - ).evaluated - - private def mainBgRunMainTaskForConfig(c: ScopeAxis[ConfigKey]) = - bgRunMain := bgRunMainTask( - exportedProductJars, - This / c / This / fullClasspathAsJars, - bgRunMain / bgCopyClasspath, - run / runner - ).evaluated - def discoverMainClasses(analysis: CompileAnalysis): Seq[String] = analysis match { case analysis: Analysis => analysis.infos.allInfos.values.map(_.getMainClasses).flatten.toSeq.sorted @@ -2649,10 +2491,10 @@ object Defaults extends BuildCommon { lazy val configSettings: Seq[Setting[?]] = Classpaths.configSettings ++ configTasks ++ configPaths ++ packageConfig ++ Classpaths.compilerPluginConfig ++ deprecationSettings ++ - BuildServerProtocol.configSettings ++ ClientJob.configSettings + BuildServerProtocol.configSettings lazy val compileSettings: Seq[Setting[?]] = - configSettings ++ (mainBgRunMainTask +: mainBgRunTask) ++ Classpaths.addUnmanagedLibrary + configSettings ++ RunUtil.configTasks(Select(Runtime)) ++ Classpaths.addUnmanagedLibrary lazy val testSettings: Seq[Setting[?]] = configSettings ++ testTasks @@ -4685,19 +4527,6 @@ trait BuildExtra extends BuildCommon with DefExtra { r.run(mainClass, cp.files, baseArguments ++ args, streams.value.log).get } - def runTask( - config: Configuration, - mainClass: String, - arguments: String* - ): Initialize[Task[Unit]] = - Def.task { - given FileConverter = fileConverter.value - val cp = (config / fullClasspath).value - val r = (config / run / runner).value - val s = streams.value - r.run(mainClass, cp.files, arguments, s.log).get - } - // public API /** Returns a vector of settings that create custom run input task. */ @nowarn diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index d22b8bce6..ad901b33d 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -316,10 +316,8 @@ object Keys { // Run Keys val selectMainClass = taskKey[Option[String]]("Selects the main class to run.").withRank(BMinusTask) val mainClass = taskKey[Option[String]]("Defines the main class for packaging or running.").withRank(BPlusTask) - val run = inputKey[EmulateForeground]("Runs a main class, passing along arguments provided on the command line.").withRank(APlusTask) - val runBlock = inputKey[Unit]("Runs a main class, and blocks until it's done.").withRank(DTask) - val runMain = inputKey[EmulateForeground]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(ATask) - val runMainBlock = inputKey[Unit]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(DTask) + val run = inputKey[Unit | ClientJobParams]("Runs a main class, passing along arguments provided on the command line.").withRank(APlusTask) + val runMain = inputKey[Unit | ClientJobParams]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(ATask) val discoveredMainClasses = taskKey[Seq[String]]("Auto-detects main classes.").withRank(BMinusTask) val runner = taskKey[ScalaRun]("Implementation used to run a main class.").withRank(DTask) val trapExit = settingKey[Boolean]("If true, enables exit trapping and thread management for 'run'-like tasks. This was removed in sbt 1.6.0 due to JDK 17 deprecating Security Manager.").withRank(CSetting) @@ -332,6 +330,7 @@ object Keys { val discoveredJavaHomes = settingKey[Map[String, File]]("Discovered Java home directories") val javaHomes = settingKey[Map[String, File]]("The user-defined additional Java home directories") val fullJavaHomes = settingKey[Map[String, File]]("Combines discoveredJavaHomes and custom javaHomes.").withRank(CTask) + val clientSide = settingKey[Boolean]("If true, takes the action on the client-side") val javaOptions = taskKey[Seq[String]]("Options passed to a new JVM when forking.").withRank(BPlusTask) val envVars = taskKey[Map[String, String]]("Environment variables used when forking a new JVM").withRank(BTask) @@ -484,8 +483,6 @@ object Keys { @cacheLevel(include = Array.empty) val bspReporter = taskKey[BuildServerReporter]("").withRank(DTask) - val clientJob = inputKey[ClientJobParams]("Translates a task into a job specification").withRank(Invisible) - val clientJobRunInfo = inputKey[ClientJobParams]("Translates the run task into a job specification").withRank(Invisible) val csrCacheDirectory = settingKey[File]("Coursier cache directory. Uses -Dsbt.coursier.home or Coursier's default.").withRank(CSetting) val csrMavenProfiles = settingKey[Set[String]]("").withRank(CSetting) diff --git a/main/src/main/scala/sbt/internal/RunUtil.scala b/main/src/main/scala/sbt/internal/RunUtil.scala new file mode 100644 index 000000000..e5448c935 --- /dev/null +++ b/main/src/main/scala/sbt/internal/RunUtil.scala @@ -0,0 +1,302 @@ +package sbt +package internal + +import java.io.File +import sbt.BuildExtra.* +import sbt.Def.* +import sbt.Keys.* +import sbt.ScopeAxis.This +import sbt.SlashSyntax0.* +import sbt.internal.util.{ Terminal as ITerminal } +import sbt.internal.worker.{ ClientJobParams, FilePath, JvmRunInfo, RunInfo } +import sbt.io.IO +import sbt.protocol.Serialization +import sbt.util.CacheImplicits.given +import xsbti.FileConverter + +object RunUtil: + /** + * Conventional server-side run implementation. + */ + def serverSideRunTask( + classpath: Initialize[Task[Classpath]], + mainClassTask: Initialize[Task[Option[String]]], + scalaRun: Initialize[Task[ScalaRun]] + ): Initialize[InputTask[Unit]] = + val parser = Def.spaceDelimited() + Def.inputTask { + val in = parser.parsed + val mainClass = getMainClass(mainClassTask.value) + val cp = classpath.value + given FileConverter = fileConverter.value + scalaRun.value.run(mainClass, cp.files, in, streams.value.log).get + } + + def configTasks(c: ScopeAxis[ConfigKey]): Seq[Setting[?]] = Seq( + bgRunMain := bgRunMainTask( + exportedProductJars, + This / c / This / fullClasspathAsJars, + bgRunMain / bgCopyClasspath, + run / runner + ).evaluated, + // note that we use the same runner and mainClass as plain run + bgRun := bgRunTask( + exportedProductJars, + This / c / This / fullClasspathAsJars, + run / mainClass, + bgRun / bgCopyClasspath, + run / runner + ).evaluated, + runMain := defaultRunMainTask( + exportedProductJars, + This / c / This / fullClasspathAsJars, + run / runner, + runMain / clientSide + ).evaluated, + run := defaultRunTask( + exportedProductJars, + This / c / This / fullClasspathAsJars, + run / mainClass, + run / runner, + run / clientSide + ).evaluated, + run / connectInput := true, + ) + + private def termWrapper(canonical: Boolean, echo: Boolean): (() => Unit) => (() => Unit) = + (f: () => Unit) => + () => { + val term = ITerminal.get + if (!canonical) { + term.enterRawMode() + if (echo) term.setEchoEnabled(echo) + } else if (!echo) term.setEchoEnabled(false) + try f() + finally { + if (!canonical) term.exitRawMode() + if (!echo) term.setEchoEnabled(true) + } + } + + private def getMainClass(value: Option[String]): String = + value.getOrElse(sys.error("no main class detected")) + + private def mkRunInfo( + args: Vector[String], + mainClass: String, + cp: Classpath, + fo: ForkOptions, + conv: FileConverter + ): RunInfo = + val strategy = fo.outputStrategy.map(_.getClass().getSimpleName().filter(_ != '$')) + // sbtn doesn't set java.home, so we need to do the fallback here + val javaHome = + fo.javaHome.map(IO.toURI).orElse(sys.props.get("java.home").map(x => IO.toURI(new File(x)))) + val jvmRunInfo = JvmRunInfo( + args = args, + classpath = cp.map(x => IO.toURI(conv.toPath(x.data).toFile)).map(FilePath(_, "")).toVector, + mainClass = mainClass, + connectInput = fo.connectInput, + javaHome = javaHome, + outputStrategy = strategy, + workingDirectory = fo.workingDirectory.map(IO.toURI), + jvmOptions = fo.runJVMOptions, + environmentVariables = fo.envVars.toMap, + ) + RunInfo( + jvm = true, + jvmRunInfo = jvmRunInfo, + ) + + def defaultRunMainTask( + products: Initialize[Task[Classpath]], + classpath: Initialize[Task[Classpath]], + scalaRun: Initialize[Task[ScalaRun]], + clientRun: Initialize[Boolean], + ): Initialize[InputTask[Unit | ClientJobParams]] = + val parser = Defaults.loadForParser(discoveredMainClasses)((s, names) => + Defaults.runMainParser(s, names getOrElse Nil) + ) + Def.inputTask { + val conv = fileConverter.value + given FileConverter = conv + val service = bgJobService.value + val (mainClass, args) = parser.parsed + val hashClasspath = (bgRunMain / bgHashClasspath).value + val fo = (run / forkOptions).value + val state = Keys.state.value + if clientRun.value && state.isNetworkCommand then + val workingDir = service.createWorkingDirectory + val cp = service.copyClasspath( + products.value, + classpath.value, + workingDir, + conv, + ) + val info = mkRunInfo(args.toVector, mainClass, cp, fo, conv) + val result = ClientJobParams( + runInfo = info + ) + import sbt.internal.worker.codec.JsonProtocol.* + state.notifyEvent(Serialization.clientJob, result) + result + else + val wrapper = termWrapper(canonicalInput.value, echoInput.value) + val handle = service.runInBackgroundWithLoader(Keys.resolvedScoped.value, state): + (logger, workingDir) => + val cp = service.copyClasspath( + products.value, + classpath.value, + workingDir, + hashClasspath, + conv, + ) + scalaRun.value match + case r: Run => + val loader = r.newLoader(cp.files) + ( + Some(loader), + wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get) + ) + case sr => + (None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get)) + service.waitForTry(handle).get + () + } + + def defaultRunTask( + products: Initialize[Task[Classpath]], + classpath: Initialize[Task[Classpath]], + mainClassTask: Initialize[Task[Option[String]]], + scalaRun: Initialize[Task[ScalaRun]], + clientRun: Initialize[Boolean] + ): Initialize[InputTask[Unit | ClientJobParams]] = + val parser = Def.spaceDelimited() + Def.inputTask { + val conv = fileConverter.value + given FileConverter = conv + val args = parser.parsed + val service = bgJobService.value + val mainClass = getMainClass(mainClassTask.value) + val hashClasspath = (bgRun / bgHashClasspath).value + val fo = (run / forkOptions).value + val state = Keys.state.value + if clientRun.value && state.isNetworkCommand then + val workingDir = service.createWorkingDirectory + val cp = service.copyClasspath( + products.value, + classpath.value, + workingDir, + conv, + ) + val info = mkRunInfo(args.toVector, mainClass, cp, fo, conv) + val result = ClientJobParams( + runInfo = info + ) + import sbt.internal.worker.codec.JsonProtocol.* + state.notifyEvent(Serialization.clientJob, result) + result + else + val wrapper = termWrapper(canonicalInput.value, echoInput.value) + val handle = service.runInBackgroundWithLoader(Keys.resolvedScoped.value, state): + (logger, workingDir) => + val cp = service.copyClasspath( + products.value, + classpath.value, + workingDir, + hashClasspath, + conv + ) + scalaRun.value match + case r: Run => + val loader = r.newLoader(cp.files) + ( + Some(loader), + wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get) + ) + case sr => + (None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get)) + service.waitForTry(handle).get + () + } + + def bgRunMainTask( + products: Initialize[Task[Classpath]], + classpath: Initialize[Task[Classpath]], + copyClasspath: Initialize[Boolean], + scalaRun: Initialize[Task[ScalaRun]] + ): Initialize[InputTask[JobHandle]] = + val parser = Defaults.loadForParser(discoveredMainClasses)((s, names) => + Defaults.runMainParser(s, names getOrElse Nil) + ) + Def.inputTask { + val service = bgJobService.value + val (mainClass, args) = parser.parsed + val hashClasspath = (bgRunMain / bgHashClasspath).value + val wrapper = termWrapper(canonicalInput.value, echoInput.value) + val converter = fileConverter.value + service.runInBackgroundWithLoader(Keys.resolvedScoped.value, state.value) { + (logger, workingDir) => + val cp = + if copyClasspath.value then + service.copyClasspath( + products.value, + classpath.value, + workingDir, + hashClasspath, + converter, + ) + else classpath.value + given FileConverter = fileConverter.value + scalaRun.value match + case r: Run => + val loader = r.newLoader(cp.files) + ( + Some(loader), + wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get) + ) + case sr => + (None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get)) + } + } + + def bgRunTask( + products: Initialize[Task[Classpath]], + classpath: Initialize[Task[Classpath]], + mainClassTask: Initialize[Task[Option[String]]], + copyClasspath: Initialize[Boolean], + scalaRun: Initialize[Task[ScalaRun]] + ): Initialize[InputTask[JobHandle]] = + val parser = Def.spaceDelimited() + Def.inputTask { + val args = parser.parsed + val service = bgJobService.value + val mainClass = getMainClass(mainClassTask.value) + val hashClasspath = (bgRun / bgHashClasspath).value + val wrapper = termWrapper(canonicalInput.value, echoInput.value) + val converter = fileConverter.value + service.runInBackgroundWithLoader(Keys.resolvedScoped.value, state.value) { + (logger, workingDir) => + val cp = + if copyClasspath.value then + service.copyClasspath( + products.value, + classpath.value, + workingDir, + hashClasspath, + converter + ) + else classpath.value + given FileConverter = converter + scalaRun.value match + case r: Run => + val loader = r.newLoader(cp.files) + ( + Some(loader), + wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get) + ) + case sr => + (None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get)) + } + } +end RunUtil diff --git a/main/src/main/scala/sbt/internal/server/ClientJob.scala b/main/src/main/scala/sbt/internal/server/ClientJob.scala deleted file mode 100644 index 1433fde93..000000000 --- a/main/src/main/scala/sbt/internal/server/ClientJob.scala +++ /dev/null @@ -1,95 +0,0 @@ -/* - * sbt - * Copyright 2023, Scala center - * Copyright 2011 - 2022, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt -package internal -package server - -import java.io.File -import sbt.Def.* -import sbt.Keys.* -import sbt.UpperStateOps.* -import sbt.internal.util.complete.Parser -import sbt.internal.worker.{ ClientJobParams, FilePath, JvmRunInfo, RunInfo } -import sbt.io.IO -import sbt.protocol.Serialization -import sbt.Keys.fileConverter - -/** - * A ClientJob represents a unit of work that sbt server process - * can outsourse back to the client. Initially intended for sbtn client-side run. - */ -object ClientJob { - lazy val globalSettings: Seq[Def.Setting[?]] = Seq( - clientJob := clientJobTask.evaluated, - clientJob / aggregate := false, - ) - - private def clientJobTask: Def.Initialize[InputTask[ClientJobParams]] = Def.inputTaskDyn { - val tokens = spaceDelimited().parsed - val state = Keys.state.value - val p = Act.aggregatedKeyParser(state) - if (tokens.isEmpty) { - sys.error("expected an argument, for example foo/run") - } - val scopedKey = Parser.parse(tokens.head, p) match { - case Right(x :: Nil) => x - case Right(xs) => sys.error("too many keys") - case Left(err) => sys.error(err) - } - if (scopedKey.key == run.key) - clientJobRunInfo.rescope(scopedKey.scope).toTask(" " + tokens.tail.mkString(" ")) - else sys.error(s"unsupported task for clientJob $scopedKey") - } - - // This will be scoped to Compile, Test, etc - lazy val configSettings: Seq[Def.Setting[?]] = Seq( - clientJobRunInfo := clientJobRunInfoTask.evaluated, - ) - - private def clientJobRunInfoTask: Def.Initialize[InputTask[ClientJobParams]] = Def.inputTask { - val state = Keys.state.value - val args = spaceDelimited().parsed - val mainClass = (Keys.run / Keys.mainClass).value - val service = bgJobService.value - val fo = (Keys.run / Keys.forkOptions).value - val workingDir = service.createWorkingDirectory - val conv = fileConverter.value - val cp = service.copyClasspath( - exportedProductJars.value, - fullClasspathAsJars.value, - workingDir, - conv, - ) - val strategy = fo.outputStrategy.map(_.getClass().getSimpleName().filter(_ != '$')) - // sbtn doesn't set java.home, so we need to do the fallback here - val javaHome = - fo.javaHome.map(IO.toURI).orElse(sys.props.get("java.home").map(x => IO.toURI(new File(x)))) - val jvmRunInfo = JvmRunInfo( - args = args.toVector, - classpath = cp.map(x => IO.toURI(conv.toPath(x.data).toFile)).map(FilePath(_, "")).toVector, - mainClass = mainClass.getOrElse(sys.error("no main class")), - connectInput = fo.connectInput, - javaHome = javaHome, - outputStrategy = strategy, - workingDirectory = fo.workingDirectory.map(IO.toURI), - jvmOptions = fo.runJVMOptions, - environmentVariables = fo.envVars.toMap, - ) - val info = RunInfo( - jvm = true, - jvmRunInfo = jvmRunInfo, - ) - val result = ClientJobParams( - runInfo = info - ) - import sbt.internal.worker.codec.JsonProtocol.* - state.notifyEvent(Serialization.clientJob, result) - result - } -} diff --git a/sbt-app/src/sbt-test/actions/add-alias/build.sbt b/sbt-app/src/sbt-test/actions/add-alias/build.sbt index c8673eaea..089e388c1 100644 --- a/sbt-app/src/sbt-test/actions/add-alias/build.sbt +++ b/sbt-app/src/sbt-test/actions/add-alias/build.sbt @@ -1,3 +1,3 @@ -addCommandAlias("demo-success", "runBlock true") -addCommandAlias("demo-failure", "runBlock false") +addCommandAlias("demo-success", "run true") +addCommandAlias("demo-failure", "run false") addCommandAlias("z", "scalaVersion") diff --git a/sbt-app/src/sbt-test/actions/input-task-dyn/build.sbt b/sbt-app/src/sbt-test/actions/input-task-dyn/build.sbt index 3fd7439ec..44542da92 100644 --- a/sbt-app/src/sbt-test/actions/input-task-dyn/build.sbt +++ b/sbt-app/src/sbt-test/actions/input-task-dyn/build.sbt @@ -12,7 +12,7 @@ lazy val root = (project in file(".")). name := "run-test", runFoo := Def.inputTaskDyn { val args = Def.spaceDelimited().parsed - (Compile / runMainBlock).toTask(s" Foo " + args.mkString(" ")) + (Compile / runMain).toTask(s" Foo " + args.mkString(" ")) }.evaluated, check := { val x = runFoo.toTask(" hi ho").value diff --git a/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt b/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt index f901176a9..0856775ce 100644 --- a/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt +++ b/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt @@ -35,6 +35,6 @@ val root = (project in file(".")).settings( val cp = System.getProperty("java.library.path", "").split(":").dropRight(1) System.setProperty("java.library.path", cp.mkString(":")) }, - wrappedRun := wrap(Runtime / runBlock).value, + wrappedRun := wrap(Runtime / run).value, wrappedTest := wrap(Test / testOnly).value ) diff --git a/sbt-app/src/sbt-test/dependency-management/aar-packaging/test b/sbt-app/src/sbt-test/dependency-management/aar-packaging/test index 5efe58689..2182f57b0 100644 --- a/sbt-app/src/sbt-test/dependency-management/aar-packaging/test +++ b/sbt-app/src/sbt-test/dependency-management/aar-packaging/test @@ -1,3 +1,3 @@ $ delete output -> runBlock +> run $ exists output diff --git a/sbt-app/src/sbt-test/dependency-management/exclude-dependencies2/test b/sbt-app/src/sbt-test/dependency-management/exclude-dependencies2/test index 5efe58689..2182f57b0 100644 --- a/sbt-app/src/sbt-test/dependency-management/exclude-dependencies2/test +++ b/sbt-app/src/sbt-test/dependency-management/exclude-dependencies2/test @@ -1,3 +1,3 @@ $ delete output -> runBlock +> run $ exists output diff --git a/sbt-app/src/sbt-test/dependency-management/profiles/test b/sbt-app/src/sbt-test/dependency-management/profiles/test index 5efe58689..2182f57b0 100644 --- a/sbt-app/src/sbt-test/dependency-management/profiles/test +++ b/sbt-app/src/sbt-test/dependency-management/profiles/test @@ -1,3 +1,3 @@ $ delete output -> runBlock +> run $ exists output diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/test b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/test index 27080878d..f347b3534 100644 --- a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/test +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/test @@ -6,6 +6,6 @@ # macro expansion fails -> b/compile -> c/runBlock +> c/run $ exists s2.13.13.txt $ delete s2.13.13.txt diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/test b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/test index dc55696e7..8eb173463 100644 --- a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/test +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/test @@ -1,12 +1,12 @@ > a/checkLibs > b/checkLibs -> b/runBlock +> b/run $ exists s2.13.12.txt $ delete s2.13.12.txt # don't crash when expanding the macro -> b3/runBlock +> b3/run $ exists s2.13.14.txt $ delete s2.13.14.txt diff --git a/sbt-app/src/sbt-test/dependency-management/url-no-head/test b/sbt-app/src/sbt-test/dependency-management/url-no-head/test index 5efe58689..2182f57b0 100644 --- a/sbt-app/src/sbt-test/dependency-management/url-no-head/test +++ b/sbt-app/src/sbt-test/dependency-management/url-no-head/test @@ -1,3 +1,3 @@ $ delete output -> runBlock +> run $ exists output diff --git a/sbt-app/src/sbt-test/lm-coursier/classifiers/test b/sbt-app/src/sbt-test/lm-coursier/classifiers/test index e0f9f8b3c..62ea636c1 100644 --- a/sbt-app/src/sbt-test/lm-coursier/classifiers/test +++ b/sbt-app/src/sbt-test/lm-coursier/classifiers/test @@ -1 +1 @@ -> runBlock +> run diff --git a/sbt-app/src/sbt-test/lm-coursier/from-no-head/test b/sbt-app/src/sbt-test/lm-coursier/from-no-head/test index 5efe58689..2182f57b0 100644 --- a/sbt-app/src/sbt-test/lm-coursier/from-no-head/test +++ b/sbt-app/src/sbt-test/lm-coursier/from-no-head/test @@ -1,3 +1,3 @@ $ delete output -> runBlock +> run $ exists output diff --git a/sbt-app/src/sbt-test/lm-coursier/from-wrong-url/test b/sbt-app/src/sbt-test/lm-coursier/from-wrong-url/test index 5efe58689..2182f57b0 100644 --- a/sbt-app/src/sbt-test/lm-coursier/from-wrong-url/test +++ b/sbt-app/src/sbt-test/lm-coursier/from-wrong-url/test @@ -1,3 +1,3 @@ $ delete output -> runBlock +> run $ exists output diff --git a/sbt-app/src/sbt-test/lm-coursier/from/test b/sbt-app/src/sbt-test/lm-coursier/from/test index ced2d6b19..11e0b3386 100644 --- a/sbt-app/src/sbt-test/lm-coursier/from/test +++ b/sbt-app/src/sbt-test/lm-coursier/from/test @@ -1,4 +1,4 @@ $ delete output -> runBlock +> run $ exists output $ delete shapeless_2.11-2.3.0.jar diff --git a/sbt-app/src/sbt-test/lm-coursier/maven-compatible/test b/sbt-app/src/sbt-test/lm-coursier/maven-compatible/test index 5efe58689..2182f57b0 100644 --- a/sbt-app/src/sbt-test/lm-coursier/maven-compatible/test +++ b/sbt-app/src/sbt-test/lm-coursier/maven-compatible/test @@ -1,3 +1,3 @@ $ delete output -> runBlock +> run $ exists output diff --git a/sbt-app/src/sbt-test/lm-coursier/per-config-resolution/test b/sbt-app/src/sbt-test/lm-coursier/per-config-resolution/test index 4ffe54064..3ee8ba373 100644 --- a/sbt-app/src/sbt-test/lm-coursier/per-config-resolution/test +++ b/sbt-app/src/sbt-test/lm-coursier/per-config-resolution/test @@ -1,2 +1,2 @@ -> runBlock +> run > Test/run diff --git a/sbt-app/src/sbt-test/lm-coursier/scala-jars/test b/sbt-app/src/sbt-test/lm-coursier/scala-jars/test index 5efe58689..2182f57b0 100644 --- a/sbt-app/src/sbt-test/lm-coursier/scala-jars/test +++ b/sbt-app/src/sbt-test/lm-coursier/scala-jars/test @@ -1,3 +1,3 @@ $ delete output -> runBlock +> run $ exists output diff --git a/sbt-app/src/sbt-test/lm-coursier/scala-sources-javadoc-jars/test b/sbt-app/src/sbt-test/lm-coursier/scala-sources-javadoc-jars/test index 39496485a..e82fd5a76 100644 --- a/sbt-app/src/sbt-test/lm-coursier/scala-sources-javadoc-jars/test +++ b/sbt-app/src/sbt-test/lm-coursier/scala-sources-javadoc-jars/test @@ -1,4 +1,4 @@ $ delete output -> runBlock +> run $ exists output > updateClassifiersCheck \ No newline at end of file diff --git a/sbt-app/src/sbt-test/lm-coursier/simple/test b/sbt-app/src/sbt-test/lm-coursier/simple/test index 7180419df..0fa50ca65 100644 --- a/sbt-app/src/sbt-test/lm-coursier/simple/test +++ b/sbt-app/src/sbt-test/lm-coursier/simple/test @@ -1,4 +1,4 @@ $ delete output -> runBlock +> run $ exists output > updateSbtClassifiers diff --git a/sbt-app/src/sbt-test/lm-coursier/zookeeper/test b/sbt-app/src/sbt-test/lm-coursier/zookeeper/test index 5efe58689..2182f57b0 100644 --- a/sbt-app/src/sbt-test/lm-coursier/zookeeper/test +++ b/sbt-app/src/sbt-test/lm-coursier/zookeeper/test @@ -1,3 +1,3 @@ $ delete output -> runBlock +> run $ exists output diff --git a/sbt-app/src/sbt-test/package/resources/test b/sbt-app/src/sbt-test/package/resources/test index 83139bf77..27ee7a65d 100644 --- a/sbt-app/src/sbt-test/package/resources/test +++ b/sbt-app/src/sbt-test/package/resources/test @@ -4,7 +4,7 @@ # This should fail because the Main object is in package jartest and the resource is directly # in src/main/resources --> runBlock +-> run > package @@ -18,7 +18,7 @@ $ copy-file src/main/resources/main_resource_test src/main/resources/jartest/mai $ delete src/main/resources/main_resource_test # This should succeed because sbt should put the resource on the runClasspath -> runBlock +> run # This is necessary because package bases whether or not to run on last modified times, which don't have # high enough resolution to notice the above move of main_resource_test diff --git a/sbt-app/src/sbt-test/source-dependencies/constants/test b/sbt-app/src/sbt-test/source-dependencies/constants/test index 28f6743d5..8f027dbf2 100644 --- a/sbt-app/src/sbt-test/source-dependencies/constants/test +++ b/sbt-app/src/sbt-test/source-dependencies/constants/test @@ -3,14 +3,14 @@ $ copy-file changes/B.scala B.scala $ copy-file changes/A1.scala A.scala -> runBlock 1 +> run 1 $ copy-file changes/A2.scala A.scala -> runBlock 2 +> run 2 > clean > ++2.13.12! $ copy-file changes/A1.scala A.scala -> runBlock 1 +> run 1 $ copy-file changes/A2.scala A.scala -> runBlock 2 +> run 2 diff --git a/sbt-app/src/sbt-test/source-dependencies/implicit-search/test b/sbt-app/src/sbt-test/source-dependencies/implicit-search/test index d7d7190b5..18d69f6b8 100644 --- a/sbt-app/src/sbt-test/source-dependencies/implicit-search/test +++ b/sbt-app/src/sbt-test/source-dependencies/implicit-search/test @@ -2,10 +2,10 @@ $ copy-file changes/A1.scala A.scala $ copy-file changes/B.scala B.scala $ copy-file changes/C.scala C.scala > compile --> runBlock +-> run $ copy-file changes/A2.scala A.scala $ sleep 1000 > compile -> runBlock +> run diff --git a/sbt-app/src/sbt-test/source-dependencies/java-basic/test b/sbt-app/src/sbt-test/source-dependencies/java-basic/test index 04a3d604d..42d594b9c 100644 --- a/sbt-app/src/sbt-test/source-dependencies/java-basic/test +++ b/sbt-app/src/sbt-test/source-dependencies/java-basic/test @@ -36,10 +36,10 @@ $ delete src/main/java/a/A.java # It shouldn't run though, because it doesn't have a main method $ copy-file changes/B1.java src/main/java/a/b/B.java > compile --> runBlock +-> run # Replace B with a new B that has a main method and should therefore run # if the main method was properly detected $ copy-file changes/B3.java src/main/java/a/b/B.java -> runBlock +> run diff --git a/sbt-app/src/sbt-test/source-dependencies/linearization/test b/sbt-app/src/sbt-test/source-dependencies/linearization/test index f6c002d7c..445441bc5 100644 --- a/sbt-app/src/sbt-test/source-dependencies/linearization/test +++ b/sbt-app/src/sbt-test/source-dependencies/linearization/test @@ -1,7 +1,7 @@ > compile # the value of F.x should be 16 -> runBlock 16 +> run 16 # modify D.scala so that the linearization changes $ copy-file changes/D.scala D.scala @@ -12,4 +12,4 @@ $ sleep 1000 # if F is recompiled, the value of x should be 11, otherwise it will still be 16 # and this will fail -> runBlock 11 \ No newline at end of file +> run 11 diff --git a/sbt-app/src/sbt-test/source-dependencies/named/test b/sbt-app/src/sbt-test/source-dependencies/named/test index 626ddc2f4..28f1c58d3 100644 --- a/sbt-app/src/sbt-test/source-dependencies/named/test +++ b/sbt-app/src/sbt-test/source-dependencies/named/test @@ -4,7 +4,7 @@ > compile # result should be 1 -> runBlock 1 +> run 1 # change order of arguments in A.x $ copy-file changes/A.scala A.scala @@ -13,4 +13,4 @@ $ copy-file changes/A.scala A.scala > compile # Should still get 1 and not -1 -> runBlock 1 +> run 1 diff --git a/sbt-app/src/sbt-test/source-dependencies/specialized/test b/sbt-app/src/sbt-test/source-dependencies/specialized/test index 76c2120a8..faabe38ea 100644 --- a/sbt-app/src/sbt-test/source-dependencies/specialized/test +++ b/sbt-app/src/sbt-test/source-dependencies/specialized/test @@ -2,7 +2,7 @@ > compile # verify that erased A.x can be called normally and reflectively -> runBlock false +> run false # make A.x specialized $ copy-file changes/A.scala A.scala @@ -12,4 +12,4 @@ $ copy-file changes/A.scala A.scala # verify that specialized A.x can be called normally and reflectively # NOTE: this test doesn't actually work correctly: have to check the output to see that B.scala was recompiled -> runBlock true \ No newline at end of file +> run true diff --git a/server-test/src/test/scala/testpkg/ClientTest.scala b/server-test/src/test/scala/testpkg/ClientTest.scala index d4177ecfa..2b5ae87aa 100644 --- a/server-test/src/test/scala/testpkg/ClientTest.scala +++ b/server-test/src/test/scala/testpkg/ClientTest.scala @@ -122,6 +122,14 @@ class ClientTest extends AbstractServerTest { test("three commands with middle failure") { assert(client("compile;willFail;willSucceed") == 1) } + test("run") { + val (exitCode, lines) = clientWithStdoutLines("run") + assert(exitCode == 0) + assert( + lines.toList.exists(_.contains("running (fork) hello")), + lines.toList.mkString(",") + ) + } test("compi completions") { val expected = Vector( "compile",