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 d6b376dac..3ae676893 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -330,6 +330,18 @@ class NetworkClient( val cmd = arguments.sbtLaunchJar match { case Some(lj) => + if (log) { + val sbtScript = if (Properties.isWin) "sbt.bat" else "sbt" + console.appendLog(Level.Warn, s"server is started using sbt-launch jar directly") + console.appendLog( + Level.Warn, + "this is not the recommended way: .sbtopts and .jvmopts files are not loaded and SBT_OPTS is ignored" + ) + console.appendLog( + Level.Warn, + s"either upgrade $sbtScript to its latest version or make sure it is accessible from $$PATH, and run 'sbt bspConfig'" + ) + } List("java") ++ arguments.sbtArguments ++ List("-jar", lj, DashDashDetachStdio, DashDashServer) case _ => @@ -1045,7 +1057,8 @@ object NetworkClient { private[client] val noStdErr = "--no-stderr" private[client] val sbtBase = "--sbt-base-directory" private[client] def parseArgs(args: Array[String]): Arguments = { - var sbtScript = if (Properties.isWin) "sbt.bat" else "sbt" + val defaultSbtScript = if (Properties.isWin) "sbt.bat" else "sbt" + var sbtScript = Properties.propOrNone("sbt.script") var launchJar: Option[String] = None var bsp = false val commandArgs = new mutable.ArrayBuffer[String] @@ -1067,11 +1080,10 @@ object NetworkClient { sbtScript = a .split("--sbt-script=") .lastOption - .map(_.replaceAllLiterally("%20", " ")) - .getOrElse(sbtScript) + .orElse(sbtScript) case "--sbt-script" if i + 1 < sanitized.length => i += 1 - sbtScript = sanitized(i).replaceAllLiterally("%20", " ") + sbtScript = Some(sanitized(i)) case a if a.startsWith("--sbt-launch-jar=") => launchJar = a .split("--sbt-launch-jar=") @@ -1091,12 +1103,17 @@ object NetworkClient { } val base = new File("").getCanonicalFile if (!sbtArguments.contains("-Dsbt.io.virtual=true")) sbtArguments += "-Dsbt.io.virtual=true" + if (!sbtArguments.exists(_.startsWith("-Dsbt.script"))) { + sbtScript.foreach { sbtScript => + sbtArguments += s"-Dsbt.script=$sbtScript" + } + } new Arguments( base, sbtArguments.toSeq, commandArgs.toSeq, completionArguments.toSeq, - sbtScript, + sbtScript.getOrElse(defaultSbtScript).replaceAllLiterally("%20", " "), bsp, launchJar ) diff --git a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala index 9b21dd31e..85c2bd3cb 100644 --- a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala +++ b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala @@ -7,12 +7,14 @@ package sbt.internal.bsp -import java.io.File - -import sbt.internal.bsp +import sbt.internal.bsp.codec.JsonProtocol.BspConnectionDetailsFormat import sbt.io.IO import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter } +import java.io.File +import java.nio.file.{ Files, Paths } +import scala.util.Properties + object BuildServerConnection { final val name = "sbt" final val bspVersion = "2.0.0-M5" @@ -21,14 +23,25 @@ object BuildServerConnection { private final val SbtLaunchJar = "sbt-launch(-.*)?\\.jar".r private[sbt] def writeConnectionFile(sbtVersion: String, baseDir: File): Unit = { - import bsp.codec.JsonProtocol._ val bspConnectionFile = new File(baseDir, ".bsp/sbt.json") val javaHome = System.getProperty("java.home") val classPath = System.getProperty("java.class.path") + + val sbtScript = Option(System.getProperty("sbt.script")) + .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 sbtLaunchJar = classPath .split(File.pathSeparator) .find(jar => SbtLaunchJar.findFirstIn(jar).nonEmpty) .map(_.replaceAllLiterally(" ", "%20")) + .map(jar => s"--sbt-launch-jar=$jar") + val argv = Vector( s"$javaHome/bin/java", @@ -38,9 +51,21 @@ object BuildServerConnection { classPath, "xsbt.boot.Boot", "-bsp" - ) ++ sbtLaunchJar.map(jar => s"--sbt-launch-jar=$jar") + ) ++ sbtScript.orElse(sbtLaunchJar) 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] = { + // 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 = Option(System.getenv("PATH")).getOrElse("") + val allPaths = envPath.split(File.pathSeparator).map(Paths.get(_)) + allPaths + .map(_.resolve(fileName)) + .find(file => Files.exists(file) && Files.isExecutable(file)) + .map(_.toString.replaceAllLiterally(" ", "%20")) + } } diff --git a/sbt b/sbt index 8adff2117..7fb865514 100755 --- a/sbt +++ b/sbt @@ -299,6 +299,16 @@ addDefaultMemory() { fi } +addSbtScriptProperty () { + if [[ "${java_args[@]}" == *-Dsbt.script=* ]]; then + : + else + sbt_script=$0 + sbt_script=${sbt_script/ /%20} + addJava "-Dsbt.script=$sbt_script" + fi +} + require_arg () { local type="$1" local opt="$2" @@ -769,6 +779,7 @@ else java_version="$(jdk_version)" vlog "[process_args] java_version = '$java_version'" addDefaultMemory + addSbtScriptProperty set -- "${residual_args[@]}" argumentCount=$# run