From dc90f160dfb563f46fd1a7b97945c381d15e2a6c Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka928@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:51:07 -0400 Subject: [PATCH] [2.x] feat: Support --autostart=false and --no-server in sbtn client (#8895) **Problem** When sbtn (native thin client) can't find a running sbt server, it prompts to start one. There was no way to opt out of server auto-start from the client side, which is needed for CI environments and scripting (sbt/sbt#7079). **Solution** Reuse the existing sbt.server.autostart system property in NetworkClient to control whether sbtn should attempt to start a server. When no server is running and sbt.server.autostart=false, sbtn exits with an error instead of prompting. Support setting this property via: - sbtn --no-server compile (sets sbt.server.autostart=false) - sbtn --autostart=false compile (new flag following sbt conventions) - sbtn -Dsbt.server.autostart=false compile (direct system property) - sbt --autostart=false compile (bash runner and sbtw) This follows sbt's naming conventions for properties/options: positive property names with =false opt-out (like --color=false, --supershell=false). Fixes sbt/sbt#7079 Co-authored-by: Claude Opus 4.6 --- .../sbt/internal/client/NetworkClient.scala | 19 ++++++++--- .../client/NetworkClientParseArgsTest.scala | 32 +++++++++++++++++++ sbt | 2 ++ sbtw/src/main/scala/sbtw/ArgParser.scala | 1 + .../src/main/scala/sbtw/LauncherOptions.scala | 1 + sbtw/src/main/scala/sbtw/Runner.scala | 1 + 6 files changed, 52 insertions(+), 4 deletions(-) diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index 22de59306..1e2f170e5 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -151,6 +151,8 @@ class NetworkClient( private lazy val noStdErr = arguments.completionArguments.contains("--no-stderr") && !sys.env.contains("SBTN_AUTO_COMPLETE") && !sys.env.contains("SBTC_AUTO_COMPLETE") private def shutdownOnly = arguments.commandArguments == Seq(Shutdown) + private lazy val serverAutoStart: Boolean = + sys.props.get("sbt.server.autostart").forall(_.toLowerCase == "true") private def mkSocket(file: File): (Socket, Option[String]) = ClientSocket.socket(file, useJNI) @@ -189,6 +191,9 @@ class NetworkClient( if (shutdownOnly) { console.appendLog(Level.Info, "no sbt server is running. ciao") System.exit(0) + } else if (!serverAutoStart) { + console.appendLog(Level.Error, "no sbt server is running (sbt.server.autostart=false)") + System.exit(1) } else if (promptCompleteUsers) { val msg = if (noTab) "" else "No sbt server is running. Press to start one..." errorStream.print(s"\n$msg") @@ -1187,7 +1192,7 @@ object NetworkClient { completionArguments, sbtScript, bsp, - sbtLaunchJar + sbtLaunchJar, ) } private[client] val completions = "--completions" @@ -1246,8 +1251,6 @@ object NetworkClient { "--timings", "-traces", "--traces", - "-no-server", - "--no-server", "-no-share", "--no-share", "-no-global", @@ -1264,6 +1267,8 @@ object NetworkClient { "-supershell=", "--color=", "-color=", + "--autostart=", + "-autostart=", ) private[client] def parseArgs(args: Array[String]): Arguments = { val defaultSbtScript = if (Properties.isWin) "sbt.bat" else "sbt" @@ -1302,6 +1307,12 @@ object NetworkClient { i += 1 launchJar = Option(sanitized(i).replace("%20", " ")) case "-bsp" | "--bsp" => bsp = true + case "-no-server" | "--no-server" => + System.setProperty("sbt.server.autostart", "false") + case a if a.startsWith("--autostart=") => + System.setProperty("sbt.server.autostart", a.stripPrefix("--autostart=")) + case a if a.startsWith("-autostart=") => + System.setProperty("sbt.server.autostart", a.stripPrefix("-autostart=")) case a if launcherValueFlags.contains(a) => if (i + 1 < sanitized.length) i += 1 case a if launcherNoValueFlags.contains(a) => () @@ -1329,7 +1340,7 @@ object NetworkClient { completionArguments.toSeq, sbtScript.getOrElse(defaultSbtScript).replace("%20", " "), bsp, - launchJar + launchJar, ) } diff --git a/main-command/src/test/scala/sbt/internal/client/NetworkClientParseArgsTest.scala b/main-command/src/test/scala/sbt/internal/client/NetworkClientParseArgsTest.scala index 40f174e21..67ef43217 100644 --- a/main-command/src/test/scala/sbt/internal/client/NetworkClientParseArgsTest.scala +++ b/main-command/src/test/scala/sbt/internal/client/NetworkClientParseArgsTest.scala @@ -111,6 +111,38 @@ object NetworkClientParseArgsTest extends BasicTestSuite: val result = parse("-bsp") assert(result.bsp) + // -- --no-server and --autostart= set sbt.server.autostart -- + + test("--no-server sets sbt.server.autostart=false"): + try + System.clearProperty("sbt.server.autostart") + parse("--no-server", "compile") + assert(System.getProperty("sbt.server.autostart") == "false") + finally System.clearProperty("sbt.server.autostart") + + test("-no-server sets sbt.server.autostart=false"): + try + System.clearProperty("sbt.server.autostart") + parse("-no-server", "compile") + assert(System.getProperty("sbt.server.autostart") == "false") + finally System.clearProperty("sbt.server.autostart") + + test("--autostart=false sets sbt.server.autostart=false"): + try + System.clearProperty("sbt.server.autostart") + val result = parse("--autostart=false", "compile") + assert(System.getProperty("sbt.server.autostart") == "false") + assert(!result.sbtArguments.exists(_.contains("autostart"))) + assert(result.commandArguments.contains("compile")) + finally System.clearProperty("sbt.server.autostart") + + test("--autostart=true sets sbt.server.autostart=true"): + try + System.clearProperty("sbt.server.autostart") + parse("--autostart=true", "compile") + assert(System.getProperty("sbt.server.autostart") == "true") + finally System.clearProperty("sbt.server.autostart") + test("--sbt-launch-jar is preserved"): val result = parse("--sbt-launch-jar", "/path/to/sbt-launch.jar", "compile") assert(result.sbtLaunchJar.contains("/path/to/sbt-launch.jar")) diff --git a/sbt b/sbt index 60a8ddb7b..1bbb0e544 100755 --- a/sbt +++ b/sbt @@ -732,6 +732,8 @@ map_args () { --supershell=*) options=( "${options[@]}" "-Dsbt.supershell=${1:13}" ) && shift ;; -supershell=*) options=( "${options[@]}" "-Dsbt.supershell=${1:12}" ) && shift ;; -no-server|--no-server) options=( "${options[@]}" "-Dsbt.io.virtual=false" "-Dsbt.server.autostart=false" ) && shift ;; + --autostart=*) options=( "${options[@]}" "-Dsbt.server.autostart=${1:13}" ) && shift ;; + -autostart=*) options=( "${options[@]}" "-Dsbt.server.autostart=${1:12}" ) && shift ;; --color=*) options=( "${options[@]}" "-Dsbt.color=${1:8}" ) && shift ;; -color=*) options=( "${options[@]}" "-Dsbt.color=${1:7}" ) && shift ;; -no-share|--no-share) options=( "${options[@]}" "${noshare_opts[@]}" ) && shift ;; diff --git a/sbtw/src/main/scala/sbtw/ArgParser.scala b/sbtw/src/main/scala/sbtw/ArgParser.scala index 719a5b9a4..66c3eb233 100644 --- a/sbtw/src/main/scala/sbtw/ArgParser.scala +++ b/sbtw/src/main/scala/sbtw/ArgParser.scala @@ -41,6 +41,7 @@ object ArgParser: opt[Int]("mem").action((x, c) => c.copy(mem = Some(x))), opt[String]("supershell").action((x, c) => c.copy(supershell = Some(x))), opt[String]("color").action((x, c) => c.copy(color = Some(x))), + opt[String]("autostart").action((x, c) => c.copy(autostart = Some(x))), opt[Int]("jvm-debug").action((x, c) => c.copy(jvmDebug = Some(x))), opt[String]("java-home").action((x, c) => c.copy(javaHome = Some(x))), arg[String]("") diff --git a/sbtw/src/main/scala/sbtw/LauncherOptions.scala b/sbtw/src/main/scala/sbtw/LauncherOptions.scala index bc95e8f55..2c6aafec0 100644 --- a/sbtw/src/main/scala/sbtw/LauncherOptions.scala +++ b/sbtw/src/main/scala/sbtw/LauncherOptions.scala @@ -29,6 +29,7 @@ case class LauncherOptions( mem: Option[Int] = None, supershell: Option[String] = None, color: Option[String] = None, + autostart: Option[String] = None, jvmDebug: Option[Int] = None, javaHome: Option[String] = None, server: Boolean = false, diff --git a/sbtw/src/main/scala/sbtw/Runner.scala b/sbtw/src/main/scala/sbtw/Runner.scala index 48bdc328c..359effdce 100644 --- a/sbtw/src/main/scala/sbtw/Runner.scala +++ b/sbtw/src/main/scala/sbtw/Runner.scala @@ -62,6 +62,7 @@ object Runner: opts.sbtCache.foreach(v => s = s :+ s"-Dsbt.global.localcache=$v") opts.ivy.foreach(v => s = s :+ s"-Dsbt.ivy.home=$v") opts.color.foreach(v => s = s :+ s"-Dsbt.color=$v") + opts.autostart.foreach(v => s = s :+ s"-Dsbt.server.autostart=$v") if opts.timings then s = s ++ Seq("-Dsbt.task.timings=true", "-Dsbt.task.timings.on.shutdown=true") if opts.traces then s = s :+ "-Dsbt.traces=true"