[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") &&
!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 <tab> 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,
)
}

View File

@ -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"))

2
sbt
View File

@ -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 ;;

View File

@ -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]("<arg>")

View File

@ -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,

View File

@ -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"