diff --git a/main-command/src/main/scala/sbt/internal/server/Server.scala b/main-command/src/main/scala/sbt/internal/server/Server.scala index 72646dabd..a90a5cb75 100644 --- a/main-command/src/main/scala/sbt/internal/server/Server.scala +++ b/main-command/src/main/scala/sbt/internal/server/Server.scala @@ -10,22 +10,24 @@ package internal package server import java.io.{ File, IOException } -import java.net.{ SocketTimeoutException, InetAddress, ServerSocket, Socket } +import java.net.{ InetAddress, ServerSocket, Socket, SocketTimeoutException } import java.util.concurrent.atomic.AtomicBoolean -import java.nio.file.attribute.{ UserPrincipal, AclEntry, AclEntryPermission, AclEntryType } +import java.nio.file.attribute.{ AclEntry, AclEntryPermission, AclEntryType, UserPrincipal } import java.security.SecureRandom import java.math.BigInteger + import scala.concurrent.{ Future, Promise } -import scala.util.{ Try, Success, Failure } +import scala.util.{ Failure, Success, Try } import sbt.internal.protocol.{ PortFile, TokenFile } import sbt.util.Logger import sbt.io.IO import sbt.io.syntax._ -import sjsonnew.support.scalajson.unsafe.{ Converter, CompactPrinter } +import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter } import sbt.internal.protocol.codec._ import sbt.internal.util.ErrorHandling import sbt.internal.util.Util.isWindows import org.scalasbt.ipcsocket._ +import sbt.internal.bsp.BuildServerConnection private[sbt] sealed trait ServerInstance { def shutdown(): Unit @@ -85,6 +87,7 @@ private[sbt] object Server { serverSocketOpt = Option(serverSocket) log.info(s"sbt server started at ${connection.shortName}") writePortfile() + writeBspConnectionDetails() running.set(true) p.success(()) while (running.get()) { @@ -194,6 +197,13 @@ private[sbt] object Server { IO.write(portfile, CompactPrinter(json)) } + private[this] def writeBspConnectionDetails(): Unit = { + import bsp.codec.JsonProtocol._ + val details = BuildServerConnection.details(sbtVersion) + val json = Converter.toJson(details).get + IO.write(bspConnectionFile, CompactPrinter(json), append = false) + } + private[sbt] def prepareSocketfile(): Unit = { if (socketfile.exists) { IO.delete(socketfile) @@ -211,7 +221,9 @@ private[sbt] case class ServerConnection( portfile: File, tokenfile: File, socketfile: File, - pipeName: String + pipeName: String, + bspConnectionFile: File, + sbtVersion: String ) { def shortName: String = { connectionType match { diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index d62b94665..4de361078 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -118,6 +118,7 @@ private[sbt] final class CommandExchange { private[sbt] def runServer(s: State): State = { lazy val port = s.get(serverPort).getOrElse(5001) lazy val host = s.get(serverHost).getOrElse("127.0.0.1") + lazy val sbtVersion = s.configuration.provider.id.version lazy val auth: Set[ServerAuthentication] = s.get(serverAuthentication).getOrElse(Set(ServerAuthentication.Token)) lazy val connectionType = s.get(serverConnectionType).getOrElse(ConnectionType.Tcp) @@ -147,6 +148,7 @@ private[sbt] final class CommandExchange { val tokenfile = serverDir / h / "token.json" val socketfile = serverDir / h / "sock" val pipeName = "sbt-server-" + h + val bspConnectionFile = s.baseDir / ".bsp" / "sbt.json" val connection = ServerConnection( connectionType, host, @@ -156,6 +158,8 @@ private[sbt] final class CommandExchange { tokenfile, socketfile, pipeName, + bspConnectionFile, + sbtVersion ) val serverInstance = Server.start(connection, onIncomingSocket, s.log) // don't throw exception when it times out diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index 8ac1919d9..5cb176826 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -25,11 +25,9 @@ import sjsonnew.support.scalajson.unsafe.Converter object BuildServerProtocol { import sbt.internal.bsp.codec.JsonProtocol._ - private val bspVersion = "2.0.0-M5" - private val languageIds = Vector("scala") private val bspTargetConfigs = Set("compile", "test") private val capabilities = BuildServerCapabilities( - CompileProvider(languageIds), + CompileProvider(BuildServerConnection.languages), dependencySourcesProvider = true ) @@ -141,7 +139,13 @@ object BuildServerProtocol { { case r: JsonRpcRequestMessage if r.method == "build/initialize" => val _ = Converter.fromJson[InitializeBuildParams](json(r)).get - val response = InitializeBuildResult("sbt", sbtVersion, bspVersion, capabilities, None) + val response = InitializeBuildResult( + "sbt", + sbtVersion, + BuildServerConnection.bspVersion, + capabilities, + None + ) callback.jsonRpcRespond(response, Some(r.id)); () case r: JsonRpcRequestMessage if r.method == "workspace/buildTargets" => @@ -223,7 +227,7 @@ object BuildServerProtocol { Some(baseDirectory), tags, capabilities, - languageIds, + BuildServerConnection.languages, projectDependencies.join.value.toVector, dataKind = Some("scala"), data = Some(Converter.toJsonUnsafe(compileData)), diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/BspConnectionDetails.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/BspConnectionDetails.scala new file mode 100644 index 000000000..16cb874e5 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/BspConnectionDetails.scala @@ -0,0 +1,56 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp +/** + * https://build-server-protocol.github.io/docs/server-discovery.html + * @param name The name of the build tool + * @param version The version of the build tool + * @param bspVersion The bsp version of the build tool + * @param languages A collection of languages supported by this BSP server + * @param argv Command arguments runnable via system processes to start a BSP server + */ +final class BspConnectionDetails private ( + val name: String, + val version: String, + val bspVersion: String, + val languages: Vector[String], + val argv: Vector[String]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: BspConnectionDetails => (this.name == x.name) && (this.version == x.version) && (this.bspVersion == x.bspVersion) && (this.languages == x.languages) && (this.argv == x.argv) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.bsp.BspConnectionDetails".##) + name.##) + version.##) + bspVersion.##) + languages.##) + argv.##) + } + override def toString: String = { + "BspConnectionDetails(" + name + ", " + version + ", " + bspVersion + ", " + languages + ", " + argv + ")" + } + private[this] def copy(name: String = name, version: String = version, bspVersion: String = bspVersion, languages: Vector[String] = languages, argv: Vector[String] = argv): BspConnectionDetails = { + new BspConnectionDetails(name, version, bspVersion, languages, argv) + } + def withName(name: String): BspConnectionDetails = { + copy(name = name) + } + def withVersion(version: String): BspConnectionDetails = { + copy(version = version) + } + def withBspVersion(bspVersion: String): BspConnectionDetails = { + copy(bspVersion = bspVersion) + } + def withLanguages(languages: Vector[String]): BspConnectionDetails = { + copy(languages = languages) + } + def withArgv(argv: Vector[String]): BspConnectionDetails = { + copy(argv = argv) + } +} +object BspConnectionDetails { + + def apply(name: String, version: String, bspVersion: String, languages: Vector[String], argv: Vector[String]): BspConnectionDetails = new BspConnectionDetails(name, version, bspVersion, languages, argv) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/BspConnectionDetailsFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/BspConnectionDetailsFormats.scala new file mode 100644 index 000000000..6f6b54123 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/BspConnectionDetailsFormats.scala @@ -0,0 +1,35 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait BspConnectionDetailsFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val BspConnectionDetailsFormat: JsonFormat[sbt.internal.bsp.BspConnectionDetails] = new JsonFormat[sbt.internal.bsp.BspConnectionDetails] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.BspConnectionDetails = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val name = unbuilder.readField[String]("name") + val version = unbuilder.readField[String]("version") + val bspVersion = unbuilder.readField[String]("bspVersion") + val languages = unbuilder.readField[Vector[String]]("languages") + val argv = unbuilder.readField[Vector[String]]("argv") + unbuilder.endObject() + sbt.internal.bsp.BspConnectionDetails(name, version, bspVersion, languages, argv) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.bsp.BspConnectionDetails, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("name", obj.name) + builder.addField("version", obj.version) + builder.addField("bspVersion", obj.bspVersion) + builder.addField("languages", obj.languages) + builder.addField("argv", obj.argv) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala index 1512fac66..6eff5c734 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala @@ -37,4 +37,5 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.internal.bsp.codec.ScalacOptionsParamsFormats with sbt.internal.bsp.codec.ScalacOptionsItemFormats with sbt.internal.bsp.codec.ScalacOptionsResultFormats + with sbt.internal.bsp.codec.BspConnectionDetailsFormats object JsonProtocol extends JsonProtocol \ No newline at end of file diff --git a/protocol/src/main/contraband/bsp.contra b/protocol/src/main/contraband/bsp.contra index 634ea76d3..6151fd95d 100644 --- a/protocol/src/main/contraband/bsp.contra +++ b/protocol/src/main/contraband/bsp.contra @@ -416,3 +416,21 @@ type ScalacOptionsItem { ## The output directory for classfiles produced by this target classDirectory: java.net.URI } + +## https://build-server-protocol.github.io/docs/server-discovery.html +type BspConnectionDetails { + ## The name of the build tool + name: String! + + ## The version of the build tool + version: String! + + ## The bsp version of the build tool + bspVersion: String! + + ## A collection of languages supported by this BSP server + languages: [String] + + ## Command arguments runnable via system processes to start a BSP server + argv: [String] +} diff --git a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala new file mode 100644 index 000000000..bde9c9b26 --- /dev/null +++ b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala @@ -0,0 +1,11 @@ +package sbt.internal.bsp + +object BuildServerConnection { + final val name = "sbt" + final val bspVersion = "2.0.0-M5" + final val languages = Vector("scala") + final val argv = Vector("sbt", "-bsp") + def details(sbtVersion: String): BspConnectionDetails = { + BspConnectionDetails(name, sbtVersion, bspVersion, languages, argv) + } +}