diff --git a/launcher-package/integration-test/src/test/scala/BspConfigTest.scala b/launcher-package/integration-test/src/test/scala/BspConfigTest.scala index 50b5bad0a..da6d8369d 100644 --- a/launcher-package/integration-test/src/test/scala/BspConfigTest.scala +++ b/launcher-package/integration-test/src/test/scala/BspConfigTest.scala @@ -49,8 +49,14 @@ object BspConfigTest extends BasicTestSuite: // Verify argv structure assert(argv.nonEmpty, "argv should not be empty") - assert(argv.head.contains("java"), s"argv should start with java command, got: ${argv.head}") - assert(argv.contains("-bsp"), s"argv should contain -bsp flag, got: $argv") + // When sbt script is available, argv uses the sbt script with "bsp" command. + // When not, argv falls back to direct java invocation with "-bsp" flag. + val usesSbtScript = argv.last == "bsp" && !argv.head.contains("java") + val usesJavaDirect = argv.head.contains("java") && argv.contains("-bsp") + assert( + usesSbtScript || usesJavaDirect, + s"argv should either use sbt script with 'bsp' command or java with '-bsp' flag, got: $argv" + ) // Test execution of the generated argv // Run the BSP command with a very short timeout to verify it starts correctly diff --git a/launcher-package/src/universal/bin/sbt.bat b/launcher-package/src/universal/bin/sbt.bat index e9be0c68c..89c1d00e7 100755 --- a/launcher-package/src/universal/bin/sbt.bat +++ b/launcher-package/src/universal/bin/sbt.bat @@ -442,6 +442,11 @@ if "%~0" == "shutdownall" ( goto args_loop ) +if "%~0" == "bsp" ( + set sbt_args_client=0 + goto args_loop +) + if "%~0" == "--script-version" ( set sbt_args_print_sbt_script_version=1 goto args_loop 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 1e2f170e5..a08d29f10 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -1306,7 +1306,7 @@ object NetworkClient { case "--sbt-launch-jar" if i + 1 < sanitized.length => i += 1 launchJar = Option(sanitized(i).replace("%20", " ")) - case "-bsp" | "--bsp" => bsp = true + case "-bsp" | "--bsp" | "bsp" => bsp = true case "-no-server" | "--no-server" => System.setProperty("sbt.server.autostart", "false") case a if a.startsWith("--autostart=") => diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 0bbc13f88..f7bbb12b9 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -75,7 +75,7 @@ private[sbt] object xMain: .map(_.trim) .filterNot(_ == DashDashServer) val isClient: String => Boolean = cmd => (cmd == JavaClient) || (cmd == DashDashClient) - val isBsp: String => Boolean = cmd => (cmd == "-bsp") || (cmd == "--bsp") + val isBsp: String => Boolean = cmd => (cmd == "-bsp") || (cmd == "--bsp") || (cmd == "bsp") val isNew: String => Boolean = cmd => (cmd == "new") || (cmd == "init") lazy val isServer = !userCommands.exists(c => isBsp(c) || isClient(c)) // keep this lazy to prevent project directory created prematurely diff --git a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala index 340daad06..f45b4e65e 100644 --- a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala +++ b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala @@ -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] = diff --git a/protocol/src/test/scala/sbt/internal/bsp/BuildServerConnectionSpec.scala b/protocol/src/test/scala/sbt/internal/bsp/BuildServerConnectionSpec.scala index 2c153c9e1..df86b7216 100644 --- a/protocol/src/test/scala/sbt/internal/bsp/BuildServerConnectionSpec.scala +++ b/protocol/src/test/scala/sbt/internal/bsp/BuildServerConnectionSpec.scala @@ -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") diff --git a/sbt b/sbt index 1bbb0e544..5ab69d315 100755 --- a/sbt +++ b/sbt @@ -826,6 +826,7 @@ process_args () { -Dsbt.color=never|-Dsbt.log.noformat=true) addJava "$1" && use_colors=0 && shift ;; "-D*"|-D*) addJava "$1" && shift ;; -J*) addJava "${1:2}" && shift ;; + bsp) use_sbtn=0 && addResidual "-bsp" && shift ;; *) addResidual "$1" && shift ;; esac done diff --git a/sbtw/src/main/scala/sbtw/Main.scala b/sbtw/src/main/scala/sbtw/Main.scala index 227ed2333..fe6db0421 100644 --- a/sbtw/src/main/scala/sbtw/Main.scala +++ b/sbtw/src/main/scala/sbtw/Main.scala @@ -51,8 +51,11 @@ object Main: else System.err.println("[error] sbt requires at least JDK 8+, you have " + javaVer) return 1 + val bspMode = opts.residual.exists(a => a == "bsp" || a == "-bsp" || a == "--bsp") val clientOpt = opts.client || sys.env.get("SBT_NATIVE_CLIENT").contains("true") - val useNativeClient = shouldRunNativeClient(opts.copy(client = clientOpt), buildPropsVersion) + val useNativeClient = + if bspMode then false + else shouldRunNativeClient(opts.copy(client = clientOpt), buildPropsVersion) if useNativeClient then val scriptPath = sbtBinDir.getAbsolutePath.replace("\\", "/") + "/sbt.bat"