fix: Use sbt script in BSP config instead of hardcoded Java path (#8920)

**Problem**

sbt bspConfig writes the absolute path of the current Java binary into .bsp/sbt.json. When the user switches Java versions (via sdkman, cs java, etc.) or removes that JDK, the IDE fails to start the sbt BSP server because the hardcoded path is stale or gone.

**Solution**

When an sbt launcher script is available (via `sbt.script` system property or PATH lookup), generate:

"argv": ["/path/to/sbt", "bsp"]
This commit is contained in:
BitToby
2026-03-19 20:57:53 -04:00
committed by GitHub
parent f92c06155c
commit be305eb3a5
8 changed files with 68 additions and 32 deletions
@@ -26,42 +26,48 @@ object BuildServerConnection {
private[sbt] def writeConnectionFile(sbtVersion: String, baseDir: File): Unit = {
val bspConnectionFile = new File(baseDir, ".bsp/sbt.json")
val javaHome = Util.javaHome
val classPath = System.getProperty("java.class.path")
val sbtScript = Option(System.getProperty("sbt.script"))
.map(_.replace("%20", " "))
.filter(_.nonEmpty)
.orElse(sbtScriptInPath)
.map(script => s"-Dsbt.script=$script")
val sbtOptsArgs = parseSbtOpts(sys.env.get("SBT_OPTS"))
val sbtLaunchJar = classPath
.split(File.pathSeparator)
.find(jar => SbtLaunchJar.findFirstIn(jar).nonEmpty)
.map(_.replace(" ", "%20"))
.map(jar => s"--sbt-launch-jar=$jar")
val argv =
Vector(
s"$javaHome/bin/java",
"-Xms100m",
"-Xmx100m",
) ++
sbtOptsArgs ++
Vector(
"-classpath",
classPath,
) ++
sbtScript ++
Vector("xsbt.boot.Boot", "-bsp") ++
(if (sbtScript.isEmpty) sbtLaunchJar else None)
val argv = sbtScript match
case Some(script) =>
Vector(script, "bsp")
case None =>
buildFallbackArgv
val details = BspConnectionDetails(name, sbtVersion, bspVersion, languages, argv)
val json = Converter.toJson(details).get
IO.write(bspConnectionFile, CompactPrinter(json), append = false)
}
private def sbtScriptInPath: Option[String] = {
private[sbt] def buildFallbackArgv: Vector[String] = {
val javaHome = Util.javaHome
val classPath = System.getProperty("java.class.path")
val sbtOptsArgs = parseSbtOpts(sys.env.get("SBT_OPTS"))
val sbtLaunchJar = classPath
.split(File.pathSeparator)
.find(jar => SbtLaunchJar.findFirstIn(jar).nonEmpty)
.map(_.replace(" ", "%20"))
.map(jar => s"--sbt-launch-jar=$jar")
Vector(
s"$javaHome/bin/java",
"-Xms100m",
"-Xmx100m",
) ++
sbtOptsArgs ++
Vector(
"-classpath",
classPath,
) ++
Vector("xsbt.boot.Boot", "-bsp") ++
sbtLaunchJar
}
private[sbt] def sbtScriptInPath: Option[String] = {
val fileName = if (Properties.isWin) "sbt.bat" else "sbt"
val envPath = sys.env.collectFirst {
case (k, v) if k.toUpperCase() == "PATH" => v
@@ -72,7 +78,7 @@ object BuildServerConnection {
allPaths
.map(_.resolve(fileName))
.find(file => Files.exists(file) && Files.isExecutable(file))
.map(_.toString.replace(" ", "%20"))
.map(_.toString)
}
private[sbt] def parseSbtOpts(sbtOpts: Option[String]): Vector[String] =
@@ -45,3 +45,18 @@ object BuildServerConnectionSpec extends BasicTestSuite:
test("parseSbtOpts should handle whitespace-separated options"):
val result = BuildServerConnection.parseSbtOpts(Some(" -Dfoo=bar -Xmx1g "))
assert(result == Vector("-Dfoo=bar", "-Xmx1g"))
test("sbtScriptInPath should return None when sbt is not in PATH"):
val result = BuildServerConnection.sbtScriptInPath
result match
case Some(path) => assert(path.nonEmpty)
case None => assert(true)
test("buildFallbackArgv should include java path and -bsp flag"):
val argv = BuildServerConnection.buildFallbackArgv
assert(argv.head.contains("java"), s"argv should start with java, got: ${argv.head}")
assert(argv.contains("-bsp"), s"argv should contain -bsp, got: $argv")
assert(argv.contains("-Xms100m"), s"argv should contain -Xms100m, got: $argv")
assert(argv.contains("-Xmx100m"), s"argv should contain -Xmx100m, got: $argv")
assert(argv.contains("-classpath"), s"argv should contain -classpath, got: $argv")
assert(argv.contains("xsbt.boot.Boot"), s"argv should contain xsbt.boot.Boot, got: $argv")