[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 <noreply@anthropic.com>
This commit is contained in:
Dream 2026-03-12 20:51:07 -04:00 committed by GitHub
parent ab503ca51e
commit dc90f160df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 52 additions and 4 deletions

View File

@ -151,6 +151,8 @@ class NetworkClient(
private lazy val noStdErr = arguments.completionArguments.contains("--no-stderr") && private lazy val noStdErr = arguments.completionArguments.contains("--no-stderr") &&
!sys.env.contains("SBTN_AUTO_COMPLETE") && !sys.env.contains("SBTC_AUTO_COMPLETE") !sys.env.contains("SBTN_AUTO_COMPLETE") && !sys.env.contains("SBTC_AUTO_COMPLETE")
private def shutdownOnly = arguments.commandArguments == Seq(Shutdown) 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) private def mkSocket(file: File): (Socket, Option[String]) = ClientSocket.socket(file, useJNI)
@ -189,6 +191,9 @@ class NetworkClient(
if (shutdownOnly) { if (shutdownOnly) {
console.appendLog(Level.Info, "no sbt server is running. ciao") console.appendLog(Level.Info, "no sbt server is running. ciao")
System.exit(0) 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) { } else if (promptCompleteUsers) {
val msg = if (noTab) "" else "No sbt server is running. Press <tab> to start one..." val msg = if (noTab) "" else "No sbt server is running. Press <tab> to start one..."
errorStream.print(s"\n$msg") errorStream.print(s"\n$msg")
@ -1187,7 +1192,7 @@ object NetworkClient {
completionArguments, completionArguments,
sbtScript, sbtScript,
bsp, bsp,
sbtLaunchJar sbtLaunchJar,
) )
} }
private[client] val completions = "--completions" private[client] val completions = "--completions"
@ -1246,8 +1251,6 @@ object NetworkClient {
"--timings", "--timings",
"-traces", "-traces",
"--traces", "--traces",
"-no-server",
"--no-server",
"-no-share", "-no-share",
"--no-share", "--no-share",
"-no-global", "-no-global",
@ -1264,6 +1267,8 @@ object NetworkClient {
"-supershell=", "-supershell=",
"--color=", "--color=",
"-color=", "-color=",
"--autostart=",
"-autostart=",
) )
private[client] def parseArgs(args: Array[String]): Arguments = { private[client] def parseArgs(args: Array[String]): Arguments = {
val defaultSbtScript = if (Properties.isWin) "sbt.bat" else "sbt" val defaultSbtScript = if (Properties.isWin) "sbt.bat" else "sbt"
@ -1302,6 +1307,12 @@ object NetworkClient {
i += 1 i += 1
launchJar = Option(sanitized(i).replace("%20", " ")) launchJar = Option(sanitized(i).replace("%20", " "))
case "-bsp" | "--bsp" => bsp = true 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) => case a if launcherValueFlags.contains(a) =>
if (i + 1 < sanitized.length) i += 1 if (i + 1 < sanitized.length) i += 1
case a if launcherNoValueFlags.contains(a) => () case a if launcherNoValueFlags.contains(a) => ()
@ -1329,7 +1340,7 @@ object NetworkClient {
completionArguments.toSeq, completionArguments.toSeq,
sbtScript.getOrElse(defaultSbtScript).replace("%20", " "), sbtScript.getOrElse(defaultSbtScript).replace("%20", " "),
bsp, bsp,
launchJar launchJar,
) )
} }

View File

@ -111,6 +111,38 @@ object NetworkClientParseArgsTest extends BasicTestSuite:
val result = parse("-bsp") val result = parse("-bsp")
assert(result.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"): test("--sbt-launch-jar is preserved"):
val result = parse("--sbt-launch-jar", "/path/to/sbt-launch.jar", "compile") val result = parse("--sbt-launch-jar", "/path/to/sbt-launch.jar", "compile")
assert(result.sbtLaunchJar.contains("/path/to/sbt-launch.jar")) assert(result.sbtLaunchJar.contains("/path/to/sbt-launch.jar"))

2
sbt
View File

@ -732,6 +732,8 @@ map_args () {
--supershell=*) options=( "${options[@]}" "-Dsbt.supershell=${1:13}" ) && shift ;; --supershell=*) options=( "${options[@]}" "-Dsbt.supershell=${1:13}" ) && shift ;;
-supershell=*) options=( "${options[@]}" "-Dsbt.supershell=${1:12}" ) && shift ;; -supershell=*) options=( "${options[@]}" "-Dsbt.supershell=${1:12}" ) && shift ;;
-no-server|--no-server) options=( "${options[@]}" "-Dsbt.io.virtual=false" "-Dsbt.server.autostart=false" ) && 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:8}" ) && shift ;;
-color=*) options=( "${options[@]}" "-Dsbt.color=${1:7}" ) && shift ;; -color=*) options=( "${options[@]}" "-Dsbt.color=${1:7}" ) && shift ;;
-no-share|--no-share) options=( "${options[@]}" "${noshare_opts[@]}" ) && shift ;; -no-share|--no-share) options=( "${options[@]}" "${noshare_opts[@]}" ) && shift ;;

View File

@ -41,6 +41,7 @@ object ArgParser:
opt[Int]("mem").action((x, c) => c.copy(mem = Some(x))), opt[Int]("mem").action((x, c) => c.copy(mem = Some(x))),
opt[String]("supershell").action((x, c) => c.copy(supershell = 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]("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[Int]("jvm-debug").action((x, c) => c.copy(jvmDebug = Some(x))),
opt[String]("java-home").action((x, c) => c.copy(javaHome = Some(x))), opt[String]("java-home").action((x, c) => c.copy(javaHome = Some(x))),
arg[String]("<arg>") arg[String]("<arg>")

View File

@ -29,6 +29,7 @@ case class LauncherOptions(
mem: Option[Int] = None, mem: Option[Int] = None,
supershell: Option[String] = None, supershell: Option[String] = None,
color: Option[String] = None, color: Option[String] = None,
autostart: Option[String] = None,
jvmDebug: Option[Int] = None, jvmDebug: Option[Int] = None,
javaHome: Option[String] = None, javaHome: Option[String] = None,
server: Boolean = false, server: Boolean = false,

View File

@ -62,6 +62,7 @@ object Runner:
opts.sbtCache.foreach(v => s = s :+ s"-Dsbt.global.localcache=$v") opts.sbtCache.foreach(v => s = s :+ s"-Dsbt.global.localcache=$v")
opts.ivy.foreach(v => s = s :+ s"-Dsbt.ivy.home=$v") opts.ivy.foreach(v => s = s :+ s"-Dsbt.ivy.home=$v")
opts.color.foreach(v => s = s :+ s"-Dsbt.color=$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 if opts.timings then
s = s ++ Seq("-Dsbt.task.timings=true", "-Dsbt.task.timings.on.shutdown=true") s = s ++ Seq("-Dsbt.task.timings=true", "-Dsbt.task.timings.on.shutdown=true")
if opts.traces then s = s :+ "-Dsbt.traces=true" if opts.traces then s = s :+ "-Dsbt.traces=true"