From 1ed08f00344211b01a4987048739c3eb41ef988f Mon Sep 17 00:00:00 2001 From: MkDev11 Date: Wed, 14 Jan 2026 14:20:30 -0500 Subject: [PATCH] [2.x] fix: Propagate SBT_OPTS to BSP config (#8531) Fixes #7469 When running 'sbt bspConfig', the generated .bsp/sbt.json now includes JVM options from the SBT_OPTS environment variable. This ensures that options like -Dsbt.boot.directory are propagated to the BSP server. The parseSbtOpts method extracts -D, -X, and -J prefixed options from SBT_OPTS and includes them in the BSP connection argv. --- .../internal/bsp/BuildServerConnection.scala | 26 ++++++---- .../bsp/BuildServerConnectionSpec.scala | 47 +++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 protocol/src/test/scala/sbt/internal/bsp/BuildServerConnectionSpec.scala diff --git a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala index 306e17d92..340daad06 100644 --- a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala +++ b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala @@ -33,11 +33,8 @@ object BuildServerConnection { .orElse(sbtScriptInPath) .map(script => s"-Dsbt.script=$script") - // IntelliJ can start sbt even if the sbt script is not accessible from $PATH. - // To do so it uses its own bundled sbt-launch.jar. - // In that case, we must pass the path of the sbt-launch.jar to the BSP connection - // so that the server can be started. - // A known problem in that situation is that the .sbtopts and .jvmopts are not loaded. + val sbtOptsArgs = parseSbtOpts(sys.env.get("SBT_OPTS")) + val sbtLaunchJar = classPath .split(File.pathSeparator) .find(jar => SbtLaunchJar.findFirstIn(jar).nonEmpty) @@ -49,9 +46,12 @@ object BuildServerConnection { s"$javaHome/bin/java", "-Xms100m", "-Xmx100m", - "-classpath", - classPath, ) ++ + sbtOptsArgs ++ + Vector( + "-classpath", + classPath, + ) ++ sbtScript ++ Vector("xsbt.boot.Boot", "-bsp") ++ (if (sbtScript.isEmpty) sbtLaunchJar else None) @@ -62,8 +62,6 @@ object BuildServerConnection { } private def sbtScriptInPath: Option[String] = { - // For those who use an old sbt script, the -Dsbt.script is not set - // As a fallback we try to find the sbt script in $PATH val fileName = if (Properties.isWin) "sbt.bat" else "sbt" val envPath = sys.env.collectFirst { case (k, v) if k.toUpperCase() == "PATH" => v @@ -76,4 +74,14 @@ object BuildServerConnection { .find(file => Files.exists(file) && Files.isExecutable(file)) .map(_.toString.replace(" ", "%20")) } + + private[sbt] def parseSbtOpts(sbtOpts: Option[String]): Vector[String] = + sbtOpts match + case Some(opts) if opts.nonEmpty => + opts + .split("\\s+") + .filter(arg => arg.startsWith("-D") || arg.startsWith("-X") || arg.startsWith("-J")) + .map(arg => if (arg.startsWith("-J")) arg.stripPrefix("-J") else arg) + .toVector + case _ => Vector.empty } diff --git a/protocol/src/test/scala/sbt/internal/bsp/BuildServerConnectionSpec.scala b/protocol/src/test/scala/sbt/internal/bsp/BuildServerConnectionSpec.scala new file mode 100644 index 000000000..2c153c9e1 --- /dev/null +++ b/protocol/src/test/scala/sbt/internal/bsp/BuildServerConnectionSpec.scala @@ -0,0 +1,47 @@ +/* + * 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.internal.bsp + +import verify.BasicTestSuite + +object BuildServerConnectionSpec extends BasicTestSuite: + + test("parseSbtOpts should return empty vector for None"): + val result = BuildServerConnection.parseSbtOpts(None) + assert(result.isEmpty) + + test("parseSbtOpts should return empty vector for empty string"): + val result = BuildServerConnection.parseSbtOpts(Some("")) + assert(result.isEmpty) + + test("parseSbtOpts should parse -D system properties"): + val result = BuildServerConnection.parseSbtOpts(Some("-Dsbt.boot.directory=/custom/path")) + assert(result == Vector("-Dsbt.boot.directory=/custom/path")) + + test("parseSbtOpts should parse -X JVM options"): + val result = BuildServerConnection.parseSbtOpts(Some("-Xmx2g -Xms512m")) + assert(result == Vector("-Xmx2g", "-Xms512m")) + + test("parseSbtOpts should parse -J prefixed options and strip the prefix"): + val result = BuildServerConnection.parseSbtOpts(Some("-J-Xmx4g")) + assert(result == Vector("-Xmx4g")) + + test("parseSbtOpts should parse multiple mixed options"): + val result = BuildServerConnection.parseSbtOpts( + Some("-Dsbt.boot.directory=/path -Xmx2g -J-XX:+UseG1GC") + ) + assert(result == Vector("-Dsbt.boot.directory=/path", "-Xmx2g", "-XX:+UseG1GC")) + + test("parseSbtOpts should filter out non-JVM options"): + val result = BuildServerConnection.parseSbtOpts(Some("-Dfoo=bar --some-flag -Xmx1g")) + assert(result == Vector("-Dfoo=bar", "-Xmx1g")) + + test("parseSbtOpts should handle whitespace-separated options"): + val result = BuildServerConnection.parseSbtOpts(Some(" -Dfoo=bar -Xmx1g ")) + assert(result == Vector("-Dfoo=bar", "-Xmx1g"))