From 45e8e2f90d3e391effedc86ef6c90cbf60bb4447 Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Thu, 17 Jun 2021 13:35:33 +1000 Subject: [PATCH 01/23] Support the SBT extension in BSP import This enables code assist in the .sbt and project/*.scala files in IntelliJ and any other IDEs that implement this extension. --- main/src/main/scala/sbt/Keys.scala | 5 +- .../internal/server/BuildServerProtocol.scala | 166 +++++++++++++++--- 2 files changed, 142 insertions(+), 29 deletions(-) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 33f4d628d..284d24f36 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -10,7 +10,6 @@ package sbt import java.nio.file.{ Path => NioPath } import java.io.File import java.net.URL - import lmcoursier.definitions.{ CacheLogger, ModuleMatchers, Reconciliation } import lmcoursier.{ CoursierConfiguration, FallbackDependency } import org.apache.ivy.core.module.descriptor.ModuleDescriptor @@ -26,6 +25,7 @@ import sbt.internal.inc.ScalaInstance import sbt.internal.io.WatchState import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt } import sbt.internal.remotecache.RemoteCacheArtifact +import sbt.internal.server.BuildServerProtocol.BspWorkspace import sbt.internal.server.{ BuildServerReporter, ServerHandler } import sbt.internal.util.{ AttributeKey, ProgressState, SourcePosition } import sbt.io._ @@ -399,10 +399,11 @@ object Keys { val bspConfig = taskKey[Unit]("Create or update the BSP connection files").withRank(DSetting) val bspEnabled = SettingKey[Boolean](BasicKeys.bspEnabled) val bspTargetIdentifier = settingKey[BuildTargetIdentifier]("Build target identifier of a project and configuration.").withRank(DSetting) - val bspWorkspace = settingKey[Map[BuildTargetIdentifier, Scope]]("Mapping of BSP build targets to sbt scopes").withRank(DSetting) + val bspWorkspace = settingKey[BspWorkspace]("Mapping of BSP build targets to sbt scopes").withRank(DSetting) val bspInternalDependencyConfigurations = settingKey[Seq[(ProjectRef, Set[ConfigKey])]]("The project configurations that this configuration depends on, possibly transitivly").withRank(DSetting) val bspWorkspaceBuildTargets = taskKey[Seq[BuildTarget]]("List all the BSP build targets").withRank(DTask) val bspBuildTarget = taskKey[BuildTarget]("Description of the BSP build targets").withRank(DTask) + val bspSbtBuildTarget = taskKey[List[BuildTarget]]("Description of the BSP SBT build targets").withRank(DTask) val bspBuildTargetSources = inputKey[Unit]("").withRank(DTask) val bspBuildTargetSourcesItem = taskKey[SourcesItem]("").withRank(DTask) val bspBuildTargetResources = inputKey[Unit]("").withRank(DTask) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index c8ff79bf7..2329c837a 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -10,6 +10,8 @@ package internal package server import java.net.URI +import sbt.BasicCommandStrings.Shutdown +import sbt.BuildPaths.{ configurationSources, projectStandard } import sbt.BuildSyntax._ import sbt.Def._ import sbt.Keys._ @@ -22,13 +24,17 @@ import sbt.internal.langserver.ErrorCodes import sbt.internal.protocol.JsonRpcRequestMessage import sbt.internal.util.Attributed import sbt.internal.util.complete.{ Parser, Parsers } -import sbt.librarymanagement.Configuration +import sbt.librarymanagement.CrossVersion.binaryScalaVersion +import sbt.librarymanagement.{ Configuration, ScalaArtifacts } import sbt.std.TaskExtra import sbt.util.Logger import sjsonnew.shaded.scalajson.ast.unsafe.{ JNull, JValue } import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser => JsonParser } import xsbti.CompileFailed +import java.io.File +import scala.collection.mutable + // import scala.annotation.nowarn import scala.util.control.NonFatal import scala.util.{ Failure, Success, Try } @@ -94,26 +100,39 @@ object BuildServerProtocol { }, bspEnabled := true, bspWorkspace := bspWorkspaceSetting.value, + bspSbtBuildTarget := sbtBuildTargetTask.value, bspWorkspaceBuildTargets := Def.taskDyn { val workspace = Keys.bspWorkspace.value val state = Keys.state.value - val allTargets = ScopeFilter.in(workspace.values.toSeq) + val allTargets = ScopeFilter.in(workspace.scopes.values.toSeq) Def.task { + val sbtTargets = bspSbtBuildTarget.value val buildTargets = Keys.bspBuildTarget.all(allTargets).value.toVector - state.respondEvent(WorkspaceBuildTargetsResult(buildTargets)) - buildTargets + val allBuildTargets = buildTargets ++ sbtTargets + state.respondEvent(WorkspaceBuildTargetsResult(allBuildTargets)) + allBuildTargets } }.value, // https://github.com/build-server-protocol/build-server-protocol/blob/master/docs/specification.md#build-target-sources-request bspBuildTargetSources := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val filter = ScopeFilter.in(targets.map(workspace)) + val workspace = bspWorkspace.value.filter(targets) + val filter = ScopeFilter.in(workspace.scopes.values.toList) // run the worker task concurrently Def.task { val items = bspBuildTargetSourcesItem.all(filter).value - val result = SourcesResult(items.toVector) + val buildItems = workspace.builds.toVector.map { + case (id, loadedBuildUnit) => + val base = loadedBuildUnit.localBase + val sbtFiles = configurationSources(base) + val defDir = projectStandard(base) + val sbtFilesItems = + sbtFiles.map(f => SourceItem(f.toURI, SourceItemKind.File, generated = false)) + val defDirItem = SourceItem(defDir.toURI, SourceItemKind.Directory, generated = false) + SourcesItem(id, (defDirItem +: sbtFilesItems).toVector) + } + val result = SourcesResult((items ++ buildItems).toVector) s.respondEvent(result) } }.evaluated, @@ -133,9 +152,9 @@ object BuildServerProtocol { bspBuildTargetResources / aggregate := false, bspBuildTargetDependencySources := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val filter = ScopeFilter.in(targets.map(workspace)) + val workspace = bspWorkspace.value.filter(targets) + val filter = ScopeFilter.in(workspace.scopes.values.toList) // run the worker task concurrently Def.task { import sbt.internal.bsp.codec.JsonProtocol._ @@ -147,9 +166,9 @@ object BuildServerProtocol { bspBuildTargetDependencySources / aggregate := false, bspBuildTargetCompile := Def.inputTaskDyn { val s: State = state.value - val workspace = bspWorkspace.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val filter = ScopeFilter.in(targets.map(workspace)) + val workspace = bspWorkspace.value.filter(targets) + val filter = ScopeFilter.in(workspace.scopes.values.toList) Def.task { val statusCode = Keys.bspBuildTargetCompileItem.all(filter).value.max s.respondEvent(BspCompileResult(None, statusCode)) @@ -160,21 +179,36 @@ object BuildServerProtocol { bspBuildTargetTest / aggregate := false, bspBuildTargetScalacOptions := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value + val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val filter = ScopeFilter.in(targets.map(workspace)) + val workspace = bspWorkspace.value.filter(targets) + val builds = workspace.builds + + val filter = ScopeFilter.in(workspace.scopes.values.toList) Def.task { val items = bspBuildTargetScalacOptionsItem.all(filter).value - val result = ScalacOptionsResult(items.toVector) + val appProvider = appConfiguration.value.provider() + val sbtJars = appProvider.mainClasspath() + val buildItems = builds.map { build => + val pluginClassPath = build._2.unit.plugins.classpath + val classpath = (pluginClassPath ++ sbtJars).map(_.toURI).toVector + ScalacOptionsItem( + build._1, + Vector(), + classpath, + new File(build._2.localBase, "project/target").toURI + ) + } + val result = ScalacOptionsResult((items ++ buildItems).toVector) s.respondEvent(result) } }.evaluated, bspBuildTargetScalacOptions / aggregate := false, bspScalaTestClasses := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val filter = ScopeFilter.in(targets.map(workspace)) + val workspace = bspWorkspace.value.filter(targets) + val filter = ScopeFilter.in(workspace.scopes.values.toList) Def.task { val items = bspScalaTestClassesItem.all(filter).value val result = ScalaTestClassesResult(items.toVector, None) @@ -183,9 +217,9 @@ object BuildServerProtocol { }.evaluated, bspScalaMainClasses := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val filter = ScopeFilter.in(targets.map(workspace)) + val workspace = bspWorkspace.value.filter(targets) + val filter = ScopeFilter.in(workspace.scopes.values.toList) Def.task { val items = bspScalaMainClassesItem.all(filter).value val result = ScalaMainClassesResult(items.toVector, None) @@ -250,7 +284,7 @@ object BuildServerProtocol { def handler( loadedBuild: LoadedBuild, - workspace: Map[BuildTargetIdentifier, Scope], + workspace: BspWorkspace, sbtVersion: String, semanticdbEnabled: Boolean, semanticdbVersion: String @@ -312,7 +346,7 @@ object BuildServerProtocol { case r if r.method == "buildTarget/run" => val paramJson = json(r) val param = Converter.fromJson[RunParams](json(r)).get - val scope = workspace.getOrElse( + val scope = workspace.scopes.getOrElse( param.target, throw LangServerError( ErrorCodes.InvalidParams, @@ -403,7 +437,7 @@ object BuildServerProtocol { ) @nowarn - private def bspWorkspaceSetting: Def.Initialize[Map[BuildTargetIdentifier, Scope]] = + private def bspWorkspaceSetting: Def.Initialize[BspWorkspace] = Def.settingDyn { val loadedBuild = Keys.loadedBuild.value @@ -423,11 +457,24 @@ object BuildServerProtocol { .map(_ / Keys.bspEnabled) .join .value - val result = for { + val buildsMap = + mutable.HashMap[BuildTargetIdentifier, mutable.ListBuffer[BuildTargetIdentifier]]() + + val scopeMap = for { (targetId, scope, bspEnabled) <- (targetIds, scopes, bspEnabled).zipped if bspEnabled - } yield targetId -> scope - result.toMap + } yield { + scope.project.toOption match { + case Some(ProjectRef(buildUri, _)) => + val loadedBuildUnit = loadedBuild.units(buildUri) + buildsMap.getOrElseUpdate(toId(loadedBuildUnit), new mutable.ListBuffer) += targetId + } + targetId -> scope + } + val buildMap = for (loadedBuildUnit <- loadedBuild.units.values) yield { + toId(loadedBuildUnit) -> loadedBuildUnit + } + BspWorkspace(scopeMap.toMap, buildMap.toMap, buildsMap.mapValues(_.result()).toMap) } } @@ -469,6 +516,50 @@ object BuildServerProtocol { } } + private def sbtBuildTargetTask: Def.Initialize[Task[List[BuildTarget]]] = Def.taskDyn { + val structure = buildStructure.value + val loadedUnits = structure.units.values.toSeq.distinct + + val scalaProvider = appConfiguration.value.provider().scalaProvider() + appConfiguration.value.provider().mainClasspath() + val scalaJars = scalaProvider.jars() + val compileData = ScalaBuildTarget( + scalaOrganization = ScalaArtifacts.Organization, + scalaVersion = scalaProvider.version(), + scalaBinaryVersion = binaryScalaVersion(scalaProvider.version()), + platform = ScalaPlatform.JVM, + jars = scalaJars.toVector.map(_.toURI.toString) + ) + val workspace = bspWorkspaceSetting.value + Def.task { + val sbtVersionValue = sbtVersion.value + loadedUnits.toList.map { loadedUnit => + val buildTargetIdentifier = toId(loadedUnit) + val sbtData = SbtBuildTarget( + sbtVersionValue, + loadedUnit.imports.toVector, + compileData, + None, + workspace.buildToScope.getOrElse(buildTargetIdentifier, Nil).toVector + ) + + BuildTarget( + buildTargetIdentifier, + // naming convention still seems like the only way to get IntelliJ to import this correctly + // https://github.com/JetBrains/intellij-scala/blob/a54c2a7c157236f35957049cbfd8c10587c9e60c/scala/scala-impl/src/org/jetbrains/sbt/language/SbtFileImpl.scala#L82-L84 + structure.rootProject(loadedUnit.unit.uri) + "-build", + projectStandard(loadedUnit.unit.localBase).toURI, + Vector(), + BuildTargetCapabilities(canCompile = false, canTest = false, canRun = false), + BuildServerConnection.languages, + Vector(), + "sbt", + data = Converter.toJsonUnsafe(sbtData), + ) + } + } + } + private def scalacOptionsTask: Def.Initialize[Task[ScalacOptionsItem]] = Def.taskDyn { val target = Keys.bspTargetIdentifier.value val scalacOptions = Keys.scalacOptions.value @@ -582,7 +673,7 @@ object BuildServerProtocol { case Success(value) => value.testClasses } val testTasks: Seq[Def.Initialize[Task[Unit]]] = items.map { item => - val scope = workspace(item.target) + val scope = workspace.scopes(item.target) item.classes.toList match { case Nil => Def.task(()) case classes => @@ -599,7 +690,7 @@ object BuildServerProtocol { case None => // run allTests in testParams.targets - val filter = ScopeFilter.in(testParams.targets.map(workspace)) + val filter = ScopeFilter.in(testParams.targets.map(workspace.scopes)) test.all(filter).result } @@ -644,7 +735,7 @@ object BuildServerProtocol { @nowarn private def internalDependencyConfigurationsSetting = Def.settingDyn { - val allScopes = bspWorkspace.value.map { case (_, scope) => scope }.toSet + val allScopes = bspWorkspace.value.scopes.map { case (_, scope) => scope }.toSet val directDependencies = Keys.internalDependencyConfigurations.value .map { case (project, rawConfigs) => @@ -705,6 +796,15 @@ object BuildServerProtocol { ) } + private val SbtBuildSuffix = "#sbt-build" + private def toId(ref: LoadedBuildUnit): BuildTargetIdentifier = { + val build = ref.unit.uri + val sanitized = build.toString.indexOf("#") match { + case i if i > 0 => build.toString.take(i) + case _ => build.toString + } + BuildTargetIdentifier(new URI(sanitized + SbtBuildSuffix)) + } private def toId(ref: ProjectReference, config: Configuration): BuildTargetIdentifier = ref match { case ProjectRef(build, project) => @@ -733,4 +833,16 @@ object BuildServerProtocol { } } } + + final case class BspWorkspace( + scopes: Map[BuildTargetIdentifier, Scope], + builds: Map[BuildTargetIdentifier, LoadedBuildUnit], + buildToScope: Map[BuildTargetIdentifier, Seq[BuildTargetIdentifier]] + ) { + def filter(targets: Seq[BuildTargetIdentifier]): BspWorkspace = { + val set = targets.toSet + def filterMap[T](map: Map[BuildTargetIdentifier, T]) = map.filter(x => set.contains(x._1)) + BspWorkspace(filterMap(scopes), filterMap(builds), buildToScope) + } + } } From af14864986840bd96d0c42a641b06e31083c2f06 Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Wed, 23 Jun 2021 14:00:32 +1000 Subject: [PATCH 02/23] Address review feedback - Restore old type of `bspWorkspace` key for backwards compat. Instead, introduce `bspFullWorkspace` that includes the SBT targets - Log a warning if the client requests, e.g. `scalaMainClasses` for a SBT target - Refactor the code that creates the SBT build targets so it doesn't depend on `sbtFullWorkspace`. - Add a setting `bspSbtEnabled` to let the user opt-opt of SBT target export (e.g. to compensate for a client that does not yet support them) --- main/src/main/scala/sbt/Defaults.scala | 2 +- main/src/main/scala/sbt/Keys.scala | 7 +- .../internal/server/BuildServerProtocol.scala | 159 +++++++++++------- 3 files changed, 99 insertions(+), 69 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index f4f96f1f5..27a19aab6 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -429,7 +429,7 @@ object Defaults extends BuildCommon { LanguageServerProtocol.handler(fileConverter.value), BuildServerProtocol.handler( loadedBuild.value, - bspWorkspace.value, + bspFullWorkspace.value, sbtVersion.value, semanticdbEnabled.value, semanticdbVersion.value diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 284d24f36..b271fbc1e 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -25,7 +25,7 @@ import sbt.internal.inc.ScalaInstance import sbt.internal.io.WatchState import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt } import sbt.internal.remotecache.RemoteCacheArtifact -import sbt.internal.server.BuildServerProtocol.BspWorkspace +import sbt.internal.server.BuildServerProtocol.BspFullWorkspace import sbt.internal.server.{ BuildServerReporter, ServerHandler } import sbt.internal.util.{ AttributeKey, ProgressState, SourcePosition } import sbt.io._ @@ -398,12 +398,13 @@ object Keys { val bspConfig = taskKey[Unit]("Create or update the BSP connection files").withRank(DSetting) val bspEnabled = SettingKey[Boolean](BasicKeys.bspEnabled) + val bspSbtEnabled = settingKey[Boolean]("Should BSP export meta-targets for the SBT build itself?") val bspTargetIdentifier = settingKey[BuildTargetIdentifier]("Build target identifier of a project and configuration.").withRank(DSetting) - val bspWorkspace = settingKey[BspWorkspace]("Mapping of BSP build targets to sbt scopes").withRank(DSetting) + val bspWorkspace = settingKey[Map[BuildTargetIdentifier, Scope]]("Mapping of BSP build targets to sbt scopes").withRank(DSetting) + private[sbt] val bspFullWorkspace = settingKey[BspFullWorkspace]("Mapping of BSP build targets to sbt scopes and meta-targets for the SBT build itself").withRank(DSetting) val bspInternalDependencyConfigurations = settingKey[Seq[(ProjectRef, Set[ConfigKey])]]("The project configurations that this configuration depends on, possibly transitivly").withRank(DSetting) val bspWorkspaceBuildTargets = taskKey[Seq[BuildTarget]]("List all the BSP build targets").withRank(DTask) val bspBuildTarget = taskKey[BuildTarget]("Description of the BSP build targets").withRank(DTask) - val bspSbtBuildTarget = taskKey[List[BuildTarget]]("Description of the BSP SBT build targets").withRank(DTask) val bspBuildTargetSources = inputKey[Unit]("").withRank(DTask) val bspBuildTargetSourcesItem = taskKey[SourcesItem]("").withRank(DTask) val bspBuildTargetResources = inputKey[Unit]("").withRank(DTask) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index 2329c837a..d1ce4cbd6 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -10,13 +10,13 @@ package internal package server import java.net.URI -import sbt.BasicCommandStrings.Shutdown import sbt.BuildPaths.{ configurationSources, projectStandard } import sbt.BuildSyntax._ import sbt.Def._ import sbt.Keys._ import sbt.Project._ import sbt.ScopeFilter.Make._ +import sbt.Scoped.richTaskSeq import sbt.SlashSyntax0._ import sbt.StandardMain.exchange import sbt.internal.bsp._ @@ -99,16 +99,21 @@ object BuildServerProtocol { } }, bspEnabled := true, - bspWorkspace := bspWorkspaceSetting.value, - bspSbtBuildTarget := sbtBuildTargetTask.value, + bspSbtEnabled := true, + bspFullWorkspace := bspFullWorkspaceSetting.value, + bspWorkspace := bspFullWorkspace.value.scopes, bspWorkspaceBuildTargets := Def.taskDyn { - val workspace = Keys.bspWorkspace.value + val workspace = Keys.bspFullWorkspace.value val state = Keys.state.value val allTargets = ScopeFilter.in(workspace.scopes.values.toSeq) + val sbtTargets: List[Def.Initialize[Task[BuildTarget]]] = workspace.builds.map { + case (buildTargetIdentifier, loadedBuildUnit) => + val buildFor = workspace.buildToScope.getOrElse(buildTargetIdentifier, Nil) + sbtBuildTarget(loadedBuildUnit, buildTargetIdentifier, buildFor) + }.toList Def.task { - val sbtTargets = bspSbtBuildTarget.value val buildTargets = Keys.bspBuildTarget.all(allTargets).value.toVector - val allBuildTargets = buildTargets ++ sbtTargets + val allBuildTargets = buildTargets ++ sbtTargets.join.value state.respondEvent(WorkspaceBuildTargetsResult(allBuildTargets)) allBuildTargets } @@ -117,7 +122,7 @@ object BuildServerProtocol { bspBuildTargetSources := Def.inputTaskDyn { val s = state.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val workspace = bspWorkspace.value.filter(targets) + val workspace = bspFullWorkspace.value.filter(targets) val filter = ScopeFilter.in(workspace.scopes.values.toList) // run the worker task concurrently Def.task { @@ -153,7 +158,7 @@ object BuildServerProtocol { bspBuildTargetDependencySources := Def.inputTaskDyn { val s = state.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val workspace = bspWorkspace.value.filter(targets) + val workspace = bspFullWorkspace.value.filter(targets) val filter = ScopeFilter.in(workspace.scopes.values.toList) // run the worker task concurrently Def.task { @@ -167,7 +172,8 @@ object BuildServerProtocol { bspBuildTargetCompile := Def.inputTaskDyn { val s: State = state.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val workspace = bspWorkspace.value.filter(targets) + val workspace = bspFullWorkspace.value.filter(targets) + workspace.warnIfBuildsNonEmpty(Method.Compile, s.log) val filter = ScopeFilter.in(workspace.scopes.values.toList) Def.task { val statusCode = Keys.bspBuildTargetCompileItem.all(filter).value.max @@ -181,7 +187,7 @@ object BuildServerProtocol { val s = state.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val workspace = bspWorkspace.value.filter(targets) + val workspace = bspFullWorkspace.value.filter(targets) val builds = workspace.builds val filter = ScopeFilter.in(workspace.scopes.values.toList) @@ -207,7 +213,8 @@ object BuildServerProtocol { bspScalaTestClasses := Def.inputTaskDyn { val s = state.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val workspace = bspWorkspace.value.filter(targets) + val workspace = bspFullWorkspace.value.filter(targets) + workspace.warnIfBuildsNonEmpty(Method.ScalaTestClasses, s.log) val filter = ScopeFilter.in(workspace.scopes.values.toList) Def.task { val items = bspScalaTestClassesItem.all(filter).value @@ -218,7 +225,8 @@ object BuildServerProtocol { bspScalaMainClasses := Def.inputTaskDyn { val s = state.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val workspace = bspWorkspace.value.filter(targets) + val workspace = bspFullWorkspace.value.filter(targets) + workspace.warnIfBuildsNonEmpty(Method.ScalaMainClasses, s.log) val filter = ScopeFilter.in(workspace.scopes.values.toList) Def.task { val items = bspScalaMainClassesItem.all(filter).value @@ -281,10 +289,26 @@ object BuildServerProtocol { } } ) + private object Method { + final val Initialize = "build/initialize" + final val BuildTargets = "workspace/buildTargets" + final val Reload = "workspace/reload" + final val Shutdown = "build/shutdown" + final val Sources = "buildTarget/sources" + final val DependencySources = "buildTarget/dependencySources" + final val Compile = "buildTarget/compile" + final val Test = "buildTarget/test" + final val Run = "buildTarget/run" + final val ScalacOptions = "buildTarget/scalacOptions" + final val ScalaTestClasses = "buildTarget/scalaTestClasses" + final val ScalaMainClasses = "buildTarget/scalaMainClasses" + final val Exit = "build/exit" + } + identity(Method) // silence spurious "private object Method in object BuildServerProtocol is never used" warning! def handler( loadedBuild: LoadedBuild, - workspace: BspWorkspace, + workspace: BspFullWorkspace, sbtVersion: String, semanticdbEnabled: Boolean, semanticdbVersion: String @@ -298,7 +322,7 @@ object BuildServerProtocol { ServerHandler { callback => ServerIntent( onRequest = { - case r if r.method == "build/initialize" => + case r if r.method == Method.Initialize => val params = Converter.fromJson[InitializeBuildParams](json(r)).get checkMetalsCompatibility(semanticdbEnabled, semanticdbVersion, params, callback.log) @@ -311,39 +335,39 @@ object BuildServerProtocol { ) callback.jsonRpcRespond(response, Some(r.id)); () - case r if r.method == "workspace/buildTargets" => + case r if r.method == Method.BuildTargets => val _ = callback.appendExec(Keys.bspWorkspaceBuildTargets.key.toString, Some(r.id)) - case r if r.method == "workspace/reload" => + case r if r.method == Method.Reload => val _ = callback.appendExec(s"$bspReload ${r.id}", Some(r.id)) - case r if r.method == "build/shutdown" => + case r if r.method == Method.Shutdown => callback.jsonRpcRespond(JNull, Some(r.id)) - case r if r.method == "buildTarget/sources" => + case r if r.method == Method.Sources => val param = Converter.fromJson[SourcesParams](json(r)).get val targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspBuildTargetSources.key val _ = callback.appendExec(s"$command $targets", Some(r.id)) - case r if r.method == "buildTarget/dependencySources" => + case r if r.method == Method.DependencySources => val param = Converter.fromJson[DependencySourcesParams](json(r)).get val targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspBuildTargetDependencySources.key val _ = callback.appendExec(s"$command $targets", Some(r.id)) - case r if r.method == "buildTarget/compile" => + case r if r.method == Method.Compile => val param = Converter.fromJson[CompileParams](json(r)).get val targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspBuildTargetCompile.key val _ = callback.appendExec(s"$command $targets", Some(r.id)) - case r: JsonRpcRequestMessage if r.method == "buildTarget/test" => + case r: JsonRpcRequestMessage if r.method == Method.Test => val task = bspBuildTargetTest.key val paramStr = CompactPrinter(json(r)) val _ = callback.appendExec(s"$task $paramStr", Some(r.id)) - case r if r.method == "buildTarget/run" => + case r if r.method == Method.Run => val paramJson = json(r) val param = Converter.fromJson[RunParams](json(r)).get val scope = workspace.scopes.getOrElse( @@ -362,19 +386,19 @@ object BuildServerProtocol { Some(r.id) ) - case r if r.method == "buildTarget/scalacOptions" => + case r if r.method == Method.ScalacOptions => val param = Converter.fromJson[ScalacOptionsParams](json(r)).get val targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspBuildTargetScalacOptions.key val _ = callback.appendExec(s"$command $targets", Some(r.id)) - case r if r.method == "buildTarget/scalaTestClasses" => + case r if r.method == Method.ScalaTestClasses => val param = Converter.fromJson[ScalaTestClassesParams](json(r)).get val targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspScalaTestClasses.key val _ = callback.appendExec(s"$command $targets", Some(r.id)) - case r if r.method == "buildTarget/scalaMainClasses" => + case r if r.method == Method.ScalaMainClasses => val param = Converter.fromJson[ScalaMainClassesParams](json(r)).get val targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspScalaMainClasses.key @@ -388,7 +412,7 @@ object BuildServerProtocol { }, onResponse = PartialFunction.empty, onNotification = { - case r if r.method == "build/exit" => + case r if r.method == Method.Exit => val _ = callback.appendExec(BasicCommandStrings.TerminateAction, None) }, ) @@ -437,7 +461,7 @@ object BuildServerProtocol { ) @nowarn - private def bspWorkspaceSetting: Def.Initialize[BspWorkspace] = + private def bspFullWorkspaceSetting: Def.Initialize[BspFullWorkspace] = Def.settingDyn { val loadedBuild = Keys.loadedBuild.value @@ -471,10 +495,14 @@ object BuildServerProtocol { } targetId -> scope } - val buildMap = for (loadedBuildUnit <- loadedBuild.units.values) yield { - toId(loadedBuildUnit) -> loadedBuildUnit + val buildMap = if (bspSbtEnabled.value) { + for (loadedBuildUnit <- loadedBuild.units.values) yield { + toId(loadedBuildUnit) -> loadedBuildUnit + } + } else { + Nil } - BspWorkspace(scopeMap.toMap, buildMap.toMap, buildsMap.mapValues(_.result()).toMap) + BspFullWorkspace(scopeMap.toMap, buildMap.toMap, buildsMap.mapValues(_.result()).toMap) } } @@ -516,10 +544,12 @@ object BuildServerProtocol { } } - private def sbtBuildTargetTask: Def.Initialize[Task[List[BuildTarget]]] = Def.taskDyn { + private def sbtBuildTarget( + loadedUnit: LoadedBuildUnit, + buildTargetIdentifier: BuildTargetIdentifier, + buildFor: Seq[BuildTargetIdentifier] + ): Def.Initialize[Task[BuildTarget]] = Def.task { val structure = buildStructure.value - val loadedUnits = structure.units.values.toSeq.distinct - val scalaProvider = appConfiguration.value.provider().scalaProvider() appConfiguration.value.provider().mainClasspath() val scalaJars = scalaProvider.jars() @@ -530,34 +560,28 @@ object BuildServerProtocol { platform = ScalaPlatform.JVM, jars = scalaJars.toVector.map(_.toURI.toString) ) - val workspace = bspWorkspaceSetting.value - Def.task { - val sbtVersionValue = sbtVersion.value - loadedUnits.toList.map { loadedUnit => - val buildTargetIdentifier = toId(loadedUnit) - val sbtData = SbtBuildTarget( - sbtVersionValue, - loadedUnit.imports.toVector, - compileData, - None, - workspace.buildToScope.getOrElse(buildTargetIdentifier, Nil).toVector - ) + val sbtVersionValue = sbtVersion.value + val sbtData = SbtBuildTarget( + sbtVersionValue, + loadedUnit.imports.toVector, + compileData, + None, + buildFor.toVector + ) - BuildTarget( - buildTargetIdentifier, - // naming convention still seems like the only way to get IntelliJ to import this correctly - // https://github.com/JetBrains/intellij-scala/blob/a54c2a7c157236f35957049cbfd8c10587c9e60c/scala/scala-impl/src/org/jetbrains/sbt/language/SbtFileImpl.scala#L82-L84 - structure.rootProject(loadedUnit.unit.uri) + "-build", - projectStandard(loadedUnit.unit.localBase).toURI, - Vector(), - BuildTargetCapabilities(canCompile = false, canTest = false, canRun = false), - BuildServerConnection.languages, - Vector(), - "sbt", - data = Converter.toJsonUnsafe(sbtData), - ) - } - } + BuildTarget( + buildTargetIdentifier, + // naming convention still seems like the only way to get IntelliJ to import this correctly + // https://github.com/JetBrains/intellij-scala/blob/a54c2a7c157236f35957049cbfd8c10587c9e60c/scala/scala-impl/src/org/jetbrains/sbt/language/SbtFileImpl.scala#L82-L84 + structure.rootProject(loadedUnit.unit.uri) + "-build", + projectStandard(loadedUnit.unit.localBase).toURI, + Vector(), + BuildTargetCapabilities(canCompile = false, canTest = false, canRun = false), + BuildServerConnection.languages, + Vector(), + "sbt", + data = Converter.toJsonUnsafe(sbtData), + ) } private def scalacOptionsTask: Def.Initialize[Task[ScalacOptionsItem]] = Def.taskDyn { @@ -662,7 +686,7 @@ object BuildServerProtocol { .map(_.flatMap(json => Converter.fromJson[TestParams](json))) .parsed .get - val workspace = bspWorkspace.value + val workspace = bspFullWorkspace.value val resultTask: Def.Initialize[Task[Result[Seq[Unit]]]] = testParams.dataKind match { case Some("scala-test") => @@ -735,7 +759,7 @@ object BuildServerProtocol { @nowarn private def internalDependencyConfigurationsSetting = Def.settingDyn { - val allScopes = bspWorkspace.value.scopes.map { case (_, scope) => scope }.toSet + val allScopes = bspFullWorkspace.value.scopes.map { case (_, scope) => scope }.toSet val directDependencies = Keys.internalDependencyConfigurations.value .map { case (project, rawConfigs) => @@ -834,15 +858,20 @@ object BuildServerProtocol { } } - final case class BspWorkspace( + /** The regular targets for each scope and meta-targets for the SBT build. */ + private[sbt] final case class BspFullWorkspace( scopes: Map[BuildTargetIdentifier, Scope], builds: Map[BuildTargetIdentifier, LoadedBuildUnit], buildToScope: Map[BuildTargetIdentifier, Seq[BuildTargetIdentifier]] ) { - def filter(targets: Seq[BuildTargetIdentifier]): BspWorkspace = { + def filter(targets: Seq[BuildTargetIdentifier]): BspFullWorkspace = { val set = targets.toSet def filterMap[T](map: Map[BuildTargetIdentifier, T]) = map.filter(x => set.contains(x._1)) - BspWorkspace(filterMap(scopes), filterMap(builds), buildToScope) + BspFullWorkspace(filterMap(scopes), filterMap(builds), buildToScope) + } + def warnIfBuildsNonEmpty(method: String, log: Logger): Unit = { + if (builds.nonEmpty) + log.warn(s"$method is a no-op for SBT targets: ${builds.keys.mkString("[", ",", "]")}") } } } From 111cc5f473e546ed332a296462faf4589cfdc148 Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Thu, 24 Jun 2021 07:42:07 +1000 Subject: [PATCH 03/23] Add scalacOptions to the SBT targets --- .../internal/server/BuildServerProtocol.scala | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index d1ce4cbd6..9bd78f513 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -195,15 +195,18 @@ object BuildServerProtocol { val items = bspBuildTargetScalacOptionsItem.all(filter).value val appProvider = appConfiguration.value.provider() val sbtJars = appProvider.mainClasspath() - val buildItems = builds.map { build => - val pluginClassPath = build._2.unit.plugins.classpath - val classpath = (pluginClassPath ++ sbtJars).map(_.toURI).toVector - ScalacOptionsItem( - build._1, - Vector(), - classpath, - new File(build._2.localBase, "project/target").toURI - ) + val buildItems = builds.map { + build => + val plugins: LoadedPlugins = build._2.unit.plugins + val scalacOptions = plugins.pluginData.scalacOptions + val pluginClassPath = plugins.classpath + val classpath = (pluginClassPath ++ sbtJars).map(_.toURI).toVector + ScalacOptionsItem( + build._1, + scalacOptions.toVector, + classpath, + new File(build._2.localBase, "project/target").toURI + ) } val result = ScalacOptionsResult((items ++ buildItems).toVector) s.respondEvent(result) From 0bd736be2a5e1a048124cd1bc134454af12fb0ec Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Sat, 26 Jun 2021 16:18:06 +1000 Subject: [PATCH 04/23] Record build sources in PluginData for BSP --- main/src/main/scala/sbt/EvaluateTask.scala | 6 ++++-- main/src/main/scala/sbt/internal/Load.scala | 10 ++++++++-- .../sbt/internal/server/BuildServerProtocol.scala | 13 +++++++++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 3557b6b30..58f1e1740 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -143,14 +143,16 @@ final case class PluginData( definitionClasspath: Seq[Attributed[File]], resolvers: Option[Vector[Resolver]], report: Option[UpdateReport], - scalacOptions: Seq[String] + scalacOptions: Seq[String], + unmanagedSources: Seq[File], + managedSources: Seq[File] ) { val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath } object PluginData { private[sbt] def apply(dependencyClasspath: Def.Classpath): PluginData = - PluginData(dependencyClasspath, Nil, None, None, Nil) + PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil) } object EvaluateTask { diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index ef6e08149..6830d6749 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -1164,12 +1164,16 @@ private[sbt] object Load { val prod = (Configurations.Runtime / exportedProducts).value val cp = (Configurations.Runtime / fullClasspath).value val opts = (Configurations.Compile / scalacOptions).value + val managedSrcs = (Configurations.Compile / managedSources).value + val unmanagedSrcs = (Configurations.Compile / unmanagedSources).value PluginData( removeEntries(cp, prod), prod, Some(fullResolvers.value.toVector), Some(update.value), - opts + opts, + unmanagedSrcs, + managedSrcs ) }, scalacOptions += "-Wconf:cat=unused-nowarn:s", @@ -1225,7 +1229,7 @@ private[sbt] object Load { loadPluginDefinition( dir, config, - PluginData(config.globalPluginClasspath, Nil, None, None, Nil) + PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, Nil) ) def buildPlugins(dir: File, s: State, config: LoadBuildConfiguration): LoadedPlugins = @@ -1417,6 +1421,8 @@ final case class LoadBuildConfiguration( data.internalClasspath, Some(data.resolvers), Some(data.updateReport), + Nil, + Nil, Nil ) case None => PluginData(globalPluginClasspath) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index 9bd78f513..bbb5c2b65 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -131,11 +131,16 @@ object BuildServerProtocol { case (id, loadedBuildUnit) => val base = loadedBuildUnit.localBase val sbtFiles = configurationSources(base) - val defDir = projectStandard(base) + val pluginData = loadedBuildUnit.unit.plugins.pluginData + val unmanagedSources = pluginData.unmanagedSources.map( + f => SourceItem(f.toURI, SourceItemKind.File, generated = false) + ) + val managedSources = pluginData.managedSources.map( + f => SourceItem(f.toURI, SourceItemKind.File, generated = true) + ) val sbtFilesItems = sbtFiles.map(f => SourceItem(f.toURI, SourceItemKind.File, generated = false)) - val defDirItem = SourceItem(defDir.toURI, SourceItemKind.Directory, generated = false) - SourcesItem(id, (defDirItem +: sbtFilesItems).toVector) + SourcesItem(id, (unmanagedSources ++ managedSources ++ sbtFilesItems).toVector) } val result = SourcesResult((items ++ buildItems).toVector) s.respondEvent(result) @@ -874,7 +879,7 @@ object BuildServerProtocol { } def warnIfBuildsNonEmpty(method: String, log: Logger): Unit = { if (builds.nonEmpty) - log.warn(s"$method is a no-op for SBT targets: ${builds.keys.mkString("[", ",", "]")}") + log.warn(s"$method is a no-op for build.sbt targets: ${builds.keys.mkString("[", ",", "]")}") } } } From fe046476b11030d5b6922b798e78ffc4e0c86feb Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Wed, 30 Jun 2021 11:19:24 +1000 Subject: [PATCH 05/23] Refine and test SBT BSP target - Pass source dirs and current list of files - Align display name and URI --- main/src/main/scala/sbt/EvaluateTask.scala | 4 +- main/src/main/scala/sbt/internal/Load.scala | 12 +++- .../internal/server/BuildServerProtocol.scala | 52 ++++++++++------- .../server-test/buildserver/project/A.scala | 0 .../project/src/main/scala/B.scala | 0 .../test/scala/testpkg/BuildServerTest.scala | 51 +++++++++++++---- .../src/test/scala/testpkg/TestServer.scala | 56 ++++++++++++++++++- 7 files changed, 140 insertions(+), 35 deletions(-) create mode 100644 server-test/src/server-test/buildserver/project/A.scala create mode 100644 server-test/src/server-test/buildserver/project/src/main/scala/B.scala diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 58f1e1740..0235e31d8 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -144,7 +144,9 @@ final case class PluginData( resolvers: Option[Vector[Resolver]], report: Option[UpdateReport], scalacOptions: Seq[String], + unmanagedSourceDirectories: Seq[File], unmanagedSources: Seq[File], + managedSourceDirectories: Seq[File], managedSources: Seq[File] ) { val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath @@ -152,7 +154,7 @@ final case class PluginData( object PluginData { private[sbt] def apply(dependencyClasspath: Def.Classpath): PluginData = - PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil) + PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil) } object EvaluateTask { diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index 6830d6749..684586f5a 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -1164,16 +1164,20 @@ private[sbt] object Load { val prod = (Configurations.Runtime / exportedProducts).value val cp = (Configurations.Runtime / fullClasspath).value val opts = (Configurations.Compile / scalacOptions).value - val managedSrcs = (Configurations.Compile / managedSources).value + val unmanagedSrcDirs = (Configurations.Compile / unmanagedSourceDirectories).value val unmanagedSrcs = (Configurations.Compile / unmanagedSources).value + val managedSrcDirs = (Configurations.Compile / managedSourceDirectories).value + val managedSrcs = (Configurations.Compile / managedSources).value PluginData( removeEntries(cp, prod), prod, Some(fullResolvers.value.toVector), Some(update.value), opts, + unmanagedSrcDirs, unmanagedSrcs, - managedSrcs + managedSrcDirs, + managedSrcs, ) }, scalacOptions += "-Wconf:cat=unused-nowarn:s", @@ -1229,7 +1233,7 @@ private[sbt] object Load { loadPluginDefinition( dir, config, - PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, Nil) + PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil) ) def buildPlugins(dir: File, s: State, config: LoadBuildConfiguration): LoadedPlugins = @@ -1423,6 +1427,8 @@ final case class LoadBuildConfiguration( Some(data.updateReport), Nil, Nil, + Nil, + Nil, Nil ) case None => PluginData(globalPluginClasspath) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index bbb5c2b65..c400fd9c3 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -132,15 +132,21 @@ object BuildServerProtocol { val base = loadedBuildUnit.localBase val sbtFiles = configurationSources(base) val pluginData = loadedBuildUnit.unit.plugins.pluginData - val unmanagedSources = pluginData.unmanagedSources.map( - f => SourceItem(f.toURI, SourceItemKind.File, generated = false) - ) - val managedSources = pluginData.managedSources.map( - f => SourceItem(f.toURI, SourceItemKind.File, generated = true) - ) - val sbtFilesItems = - sbtFiles.map(f => SourceItem(f.toURI, SourceItemKind.File, generated = false)) - SourcesItem(id, (unmanagedSources ++ managedSources ++ sbtFilesItems).toVector) + val all = Vector.newBuilder[SourceItem] + def add(fs: Seq[File], sourceItemKind: Int, generated: Boolean): Unit = { + fs.foreach(f => all += (SourceItem(f.toURI, sourceItemKind, generated = generated))) + } + all += (SourceItem( + loadedBuildUnit.unit.plugins.base.toURI, + SourceItemKind.Directory, + generated = false + )) + add(pluginData.unmanagedSourceDirectories, SourceItemKind.Directory, generated = false) + add(pluginData.unmanagedSources, SourceItemKind.File, generated = false) + add(pluginData.managedSourceDirectories, SourceItemKind.Directory, generated = true) + add(pluginData.managedSources, SourceItemKind.File, generated = true) + add(sbtFiles, SourceItemKind.File, generated = false) + SourcesItem(id, all.result()) } val result = SourcesResult((items ++ buildItems).toVector) s.respondEvent(result) @@ -499,13 +505,17 @@ object BuildServerProtocol { scope.project.toOption match { case Some(ProjectRef(buildUri, _)) => val loadedBuildUnit = loadedBuild.units(buildUri) - buildsMap.getOrElseUpdate(toId(loadedBuildUnit), new mutable.ListBuffer) += targetId + buildsMap.getOrElseUpdate( + toSbtTargetId(loadedBuildUnit), + new mutable.ListBuffer + ) += targetId } targetId -> scope } val buildMap = if (bspSbtEnabled.value) { for (loadedBuildUnit <- loadedBuild.units.values) yield { - toId(loadedBuildUnit) -> loadedBuildUnit + val rootProjectId = loadedBuildUnit.root + toSbtTargetId(loadedBuildUnit) -> loadedBuildUnit } } else { Nil @@ -557,7 +567,6 @@ object BuildServerProtocol { buildTargetIdentifier: BuildTargetIdentifier, buildFor: Seq[BuildTargetIdentifier] ): Def.Initialize[Task[BuildTarget]] = Def.task { - val structure = buildStructure.value val scalaProvider = appConfiguration.value.provider().scalaProvider() appConfiguration.value.provider().mainClasspath() val scalaJars = scalaProvider.jars() @@ -579,9 +588,7 @@ object BuildServerProtocol { BuildTarget( buildTargetIdentifier, - // naming convention still seems like the only way to get IntelliJ to import this correctly - // https://github.com/JetBrains/intellij-scala/blob/a54c2a7c157236f35957049cbfd8c10587c9e60c/scala/scala-impl/src/org/jetbrains/sbt/language/SbtFileImpl.scala#L82-L84 - structure.rootProject(loadedUnit.unit.uri) + "-build", + toSbtTargetIdName(loadedUnit), projectStandard(loadedUnit.unit.localBase).toURI, Vector(), BuildTargetCapabilities(canCompile = false, canTest = false, canRun = false), @@ -828,14 +835,19 @@ object BuildServerProtocol { ) } - private val SbtBuildSuffix = "#sbt-build" - private def toId(ref: LoadedBuildUnit): BuildTargetIdentifier = { + // naming convention still seems like the only reliable way to get IntelliJ to import this correctly + // https://github.com/JetBrains/intellij-scala/blob/a54c2a7c157236f35957049cbfd8c10587c9e60c/scala/scala-impl/src/org/jetbrains/sbt/language/SbtFileImpl.scala#L82-L84 + private def toSbtTargetIdName(ref: LoadedBuildUnit): String = { + ref.root + "-build" + } + private def toSbtTargetId(ref: LoadedBuildUnit): BuildTargetIdentifier = { + val name = toSbtTargetIdName(ref) val build = ref.unit.uri val sanitized = build.toString.indexOf("#") match { case i if i > 0 => build.toString.take(i) case _ => build.toString } - BuildTargetIdentifier(new URI(sanitized + SbtBuildSuffix)) + BuildTargetIdentifier(new URI(sanitized + "#" + name)) } private def toId(ref: ProjectReference, config: Configuration): BuildTargetIdentifier = ref match { @@ -879,7 +891,9 @@ object BuildServerProtocol { } def warnIfBuildsNonEmpty(method: String, log: Logger): Unit = { if (builds.nonEmpty) - log.warn(s"$method is a no-op for build.sbt targets: ${builds.keys.mkString("[", ",", "]")}") + log.warn( + s"$method is a no-op for build.sbt targets: ${builds.keys.mkString("[", ",", "]")}" + ) } } } diff --git a/server-test/src/server-test/buildserver/project/A.scala b/server-test/src/server-test/buildserver/project/A.scala new file mode 100644 index 000000000..e69de29bb diff --git a/server-test/src/server-test/buildserver/project/src/main/scala/B.scala b/server-test/src/server-test/buildserver/project/src/main/scala/B.scala new file mode 100644 index 000000000..e69de29bb diff --git a/server-test/src/test/scala/testpkg/BuildServerTest.scala b/server-test/src/test/scala/testpkg/BuildServerTest.scala index 5509cf454..9d4e163a7 100644 --- a/server-test/src/test/scala/testpkg/BuildServerTest.scala +++ b/server-test/src/test/scala/testpkg/BuildServerTest.scala @@ -7,10 +7,18 @@ package testpkg +import sbt.internal.bsp.SourcesResult + +import java.io.File +import sbt.internal.bsp.WorkspaceBuildTargetsResult + import scala.concurrent.duration._ // starts svr using server-test/buildserver and perform custom server tests object BuildServerTest extends AbstractServerTest { + + import sbt.internal.bsp.codec.JsonProtocol._ + override val testDirectory: String = "buildserver" test("build/initialize") { _ => @@ -26,12 +34,12 @@ object BuildServerTest extends AbstractServerTest { """{ "jsonrpc": "2.0", "id": "16", "method": "workspace/buildTargets", "params": {} }""" ) assert(processing("workspace/buildTargets")) - assert { - svr.waitForString(10.seconds) { s => - (s contains """"id":"16"""") && - (s contains """"displayName":"util"""") - } - } + val result = svr.waitFor[WorkspaceBuildTargetsResult](10.seconds) + val utilTarget = result.targets.find(_.displayName.contains("util")).get + assert(utilTarget.id.uri.toString.endsWith("#util/Compile")) + val buildServerBuildTarget = + result.targets.find(_.displayName.contains("buildserver-build")).get + assert(buildServerBuildTarget.id.uri.toString.endsWith("#buildserver-build")) } test("buildTarget/sources") { _ => @@ -42,10 +50,33 @@ object BuildServerTest extends AbstractServerTest { |} }""".stripMargin ) assert(processing("buildTarget/sources")) - assert(svr.waitForString(10.seconds) { s => - (s contains """"id":"24"""") && - (s contains "util/src/main/scala") - }) + val s = svr.waitFor[SourcesResult](10.seconds) + val sources = s.items.head.sources.map(_.uri) + assert(sources.contains(new File(svr.baseDirectory, "util/src/main/scala").toURI)) + } + test("buildTarget/sources SBT") { _ => + val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#buildserver-build" + svr.sendJsonRpc( + s"""{ "jsonrpc": "2.0", "id": "25", "method": "buildTarget/sources", "params": { + | "targets": [{ "uri": "$x" }] + |} }""".stripMargin + ) + assert(processing("buildTarget/sources")) + val s = svr.waitFor[SourcesResult](10.seconds) + val sources = s.items.head.sources.map(_.uri).sorted + val expectedSources = Vector( + "build.sbt", + "project/", + "project/A.scala", + "project/src/main/java", + "project/src/main/scala-2", + "project/src/main/scala-2.12", + "project/src/main/scala-sbt-1.0", + "project/src/main/scala/", + "project/src/main/scala/B.scala", + "project/target/scala-2.12/sbt-1.0/src_managed/main" + ).map(rel => new File(svr.baseDirectory.getAbsoluteFile, rel).toURI).sorted + assert(sources == expectedSources) } test("buildTarget/compile") { _ => diff --git a/server-test/src/test/scala/testpkg/TestServer.scala b/server-test/src/test/scala/testpkg/TestServer.scala index b2c6a2f62..dac89ff79 100644 --- a/server-test/src/test/scala/testpkg/TestServer.scala +++ b/server-test/src/test/scala/testpkg/TestServer.scala @@ -12,17 +12,18 @@ import java.net.Socket import java.nio.file.{ Files, Path } import java.util.concurrent.{ LinkedBlockingQueue, TimeUnit } import java.util.concurrent.atomic.AtomicBoolean - import verify._ import sbt.{ ForkOptions, OutputStrategy, RunFromSourceMain } import sbt.io.IO import sbt.io.syntax._ import sbt.protocol.ClientSocket +import sjsonnew.JsonReader +import sjsonnew.support.scalajson.unsafe.{ Converter, Parser } import scala.annotation.tailrec import scala.concurrent._ import scala.concurrent.duration._ -import scala.util.{ Success, Try } +import scala.util.{ Failure, Success, Try } trait AbstractServerTest extends TestSuite[Unit] { private var temp: File = _ @@ -293,6 +294,57 @@ case class TestServer( } impl() } + final def waitFor[T: JsonReader](duration: FiniteDuration): T = { + val deadline = duration.fromNow + var lastEx: Throwable = null + @tailrec def impl(): T = + lines.poll(deadline.timeLeft.toMillis, TimeUnit.MILLISECONDS) match { + case null => + if (lastEx != null) throw lastEx + else throw new TimeoutException + case s => + Parser + .parseFromString(s) + .flatMap( + jvalue => + Converter.fromJson[T]( + jvalue.toStandard + .asInstanceOf[sjsonnew.shaded.scalajson.ast.JObject] + .value("result") + .toUnsafe + ) + ) match { + case Success(value) => + value + case Failure(exception) => + if (deadline.isOverdue) { + val ex = new TimeoutException() + ex.initCause(exception) + throw ex + } else { + lastEx = exception + impl() + } + } + } + impl() + } + final def waitForResponse(duration: FiniteDuration, id: Int): String = { + val deadline = duration.fromNow + @tailrec def impl(): String = + lines.poll(deadline.timeLeft.toMillis, TimeUnit.MILLISECONDS) match { + case null => + throw new TimeoutException() + case s => + val s1 = s + val correctId = s1.contains("\"id\":\"" + id + "\"") + if (!correctId && !deadline.isOverdue) impl() + else if (deadline.isOverdue) + throw new TimeoutException() + else s + } + impl() + } final def neverReceive(duration: FiniteDuration)(f: String => Boolean): Boolean = { val deadline = duration.fromNow From df5fb17c6ffa88e0112dd54897ea92cc72150fda Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Wed, 30 Jun 2021 11:25:47 +1000 Subject: [PATCH 06/23] Exclude create/copy methods of PluginData from MiMa's consideration --- build.sbt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 8ca6d0ac6..bc81d2bef 100644 --- a/build.sbt +++ b/build.sbt @@ -164,7 +164,10 @@ def mimaSettingsSince(versions: Seq[String]): Seq[Def.Setting[_]] = Def settings exclude[FinalClassProblem]("sbt.internal.*"), exclude[FinalMethodProblem]("sbt.internal.*"), exclude[IncompatibleResultTypeProblem]("sbt.internal.*"), - exclude[ReversedMissingMethodProblem]("sbt.internal.*") + exclude[ReversedMissingMethodProblem]("sbt.internal.*"), + exclude[DirectMissingMethodProblem]("sbt.PluginData.apply"), + exclude[DirectMissingMethodProblem]("sbt.PluginData.copy"), + exclude[DirectMissingMethodProblem]("sbt.PluginData.this"), ), ) From 3d92827af34d0096b15442151325d31a7f3f1db4 Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Wed, 30 Jun 2021 12:12:24 +1000 Subject: [PATCH 07/23] Fix compilation error in test --- main/src/test/scala/PluginCommandTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/test/scala/PluginCommandTest.scala b/main/src/test/scala/PluginCommandTest.scala index 46ec1c753..b8cf07768 100644 --- a/main/src/test/scala/PluginCommandTest.scala +++ b/main/src/test/scala/PluginCommandTest.scala @@ -114,7 +114,7 @@ object FakeState { Nil ) - val pluginData = PluginData(Nil, Nil, None, None, Nil) + val pluginData = PluginData(Nil, Nil, None, None, Nil, Nil, Nil, Nil, Nil) val builds: DetectedModules[BuildDef] = new DetectedModules[BuildDef](Nil) val detectedAutoPlugins: Seq[DetectedAutoPlugin] = From a825c115c295deea0286afce058e5cc0e714badc Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Wed, 30 Jun 2021 10:31:43 +0200 Subject: [PATCH 08/23] [BSP] Ignore sbt target in resources request --- .../scala/sbt/internal/server/BuildServerProtocol.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index c400fd9c3..7036a379b 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -155,9 +155,10 @@ object BuildServerProtocol { bspBuildTargetSources / aggregate := false, bspBuildTargetResources := Def.inputTaskDyn { val s = state.value - val workspace = bspWorkspace.value val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) - val filter = ScopeFilter.in(targets.map(workspace)) + val workspace = bspFullWorkspace.value.filter(targets) + workspace.warnIfBuildsNonEmpty(Method.Resources, s.log) + val filter = ScopeFilter.in(workspace.scopes.values.toList) // run the worker task concurrently Def.task { val items = bspBuildTargetResourcesItem.all(filter).value @@ -309,6 +310,7 @@ object BuildServerProtocol { final val Reload = "workspace/reload" final val Shutdown = "build/shutdown" final val Sources = "buildTarget/sources" + final val Resources = "buildTarget/resources" final val DependencySources = "buildTarget/dependencySources" final val Compile = "buildTarget/compile" final val Test = "buildTarget/test" @@ -418,7 +420,7 @@ object BuildServerProtocol { val command = Keys.bspScalaMainClasses.key val _ = callback.appendExec(s"$command $targets", Some(r.id)) - case r if r.method == "buildTarget/resources" => + case r if r.method == Method.Resources => val param = Converter.fromJson[ResourcesParams](json(r)).get val targets = param.targets.map(_.uri).mkString(" ") val command = Keys.bspBuildTargetResources.key From 4804cc2fa4f85928f89af8c88ddc4c5f68d458f8 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Fri, 25 Jun 2021 14:01:01 +0200 Subject: [PATCH 09/23] Simplify bspReload cmd by calling doLoadProject Fixes #6010: Send real error message when realod failed --- .../internal/server/BuildServerProtocol.scala | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index 7036a379b..d97e77960 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -22,7 +22,7 @@ import sbt.StandardMain.exchange import sbt.internal.bsp._ import sbt.internal.langserver.ErrorCodes import sbt.internal.protocol.JsonRpcRequestMessage -import sbt.internal.util.Attributed +import sbt.internal.util.{ Attributed, ErrorHandling } import sbt.internal.util.complete.{ Parser, Parsers } import sbt.librarymanagement.CrossVersion.binaryScalaVersion import sbt.librarymanagement.{ Configuration, ScalaArtifacts } @@ -53,35 +53,19 @@ object BuildServerProtocol { ) private val bspReload = "bspReload" - private val bspReloadFailed = "bspReloadFailed" - private val bspReloadSucceed = "bspReloadSucceed" lazy val commands: Seq[Command] = Seq( Command.single(bspReload) { (state, reqId) => - import sbt.BasicCommandStrings._ - import sbt.internal.CommandStrings._ - val result = List( - StashOnFailure, - s"$OnFailure $bspReloadFailed $reqId", - LoadProjectImpl, - s"$bspReloadSucceed $reqId", - PopOnFailure, - FailureWall - ) ::: state - result - }, - Command.single(bspReloadFailed) { (state, reqId) => - exchange.respondError( - ErrorCodes.InternalError, - "reload failed", - Some(reqId), - state.source - ) - state - }, - Command.single(bspReloadSucceed) { (state, reqId) => - exchange.respondEvent(JNull, Some(reqId), state.source) - state + try { + val newState = BuiltinCommands.doLoadProject(state, Project.LoadAction.Current) + exchange.respondEvent(JNull, Some(reqId), state.source) + newState + } catch { + case NonFatal(e) => + val msg = ErrorHandling.reducedToString(e) + exchange.respondError(ErrorCodes.InternalError, msg, Some(reqId), state.source) + state.fail + } } ) From a76e209dde0b4d4aa5a26aa96bd1f44b19bc4e1d Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Fri, 25 Jun 2021 18:14:59 +0200 Subject: [PATCH 10/23] [BSP] send diagnostics when evaluating build.sbt Since build.sbt is compiled/evaluated in `sbt.compiler.Eval`, this commit introduces a `BuildServerEvalReporter` to redirect the compiler errors to the BSP clients. A new `finalReport` method is added in the new `EvalReporter` base class to reset the old diagnostics. --- .../src/main/scala/sbt/compiler/Eval.scala | 66 +++++++------ .../scala/sbt/compiler/EvalReporter.scala | 54 +++++++++++ .../test/scala/sbt/compiler/EvalTest.scala | 4 +- main/src/main/scala/sbt/EvaluateTask.scala | 6 +- main/src/main/scala/sbt/internal/Load.scala | 42 ++++++--- .../server/BuildServerEvalReporter.scala | 93 +++++++++++++++++++ main/src/test/scala/PluginCommandTest.scala | 2 +- 7 files changed, 223 insertions(+), 44 deletions(-) create mode 100644 main-actions/src/main/scala/sbt/compiler/EvalReporter.scala create mode 100644 main/src/main/scala/sbt/internal/server/BuildServerEvalReporter.scala diff --git a/main-actions/src/main/scala/sbt/compiler/Eval.scala b/main-actions/src/main/scala/sbt/compiler/Eval.scala index 461a8c727..ea841dbd2 100644 --- a/main-actions/src/main/scala/sbt/compiler/Eval.scala +++ b/main-actions/src/main/scala/sbt/compiler/Eval.scala @@ -12,7 +12,7 @@ import scala.collection.mutable.ListBuffer import scala.tools.nsc.{ ast, io, reporters, CompilerCommand, Global, Phase, Settings } import io.{ AbstractFile, PlainFile, VirtualDirectory } import ast.parser.Tokens -import reporters.{ ConsoleReporter, Reporter } +import reporters.Reporter import scala.reflect.internal.util.{ AbstractFileClassLoader, BatchSourceFile } import Tokens.{ EOF, NEWLINE, NEWLINES, SEMI } import java.io.{ File, FileNotFoundException } @@ -65,12 +65,12 @@ final class EvalException(msg: String) extends RuntimeException(msg) final class Eval( optionsNoncp: Seq[String], classpath: Seq[File], - mkReporter: Settings => Reporter, + mkReporter: Settings => EvalReporter, backing: Option[File] ) { - def this(mkReporter: Settings => Reporter, backing: Option[File]) = + def this(mkReporter: Settings => EvalReporter, backing: Option[File]) = this(Nil, IO.classLocationPath[Product].toFile :: Nil, mkReporter, backing) - def this() = this(s => new ConsoleReporter(s), None) + def this() = this(EvalReporter.console, None) backing.foreach(IO.createDirectory) val classpathString = Path.makeString(classpath ++ backing.toList) @@ -114,6 +114,7 @@ final class Eval( line: Int = DefaultStartLine ): EvalResult = { val ev = new EvalType[String] { + def sourceName: String = srcName def makeUnit = mkUnit(srcName, line, expression) def unlink = true def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = { @@ -142,6 +143,7 @@ final class Eval( require(definitions.nonEmpty, "Definitions to evaluate cannot be empty.") val ev = new EvalType[Seq[String]] { lazy val (fullUnit, defUnits) = mkDefsUnit(srcName, definitions) + def sourceName: String = srcName def makeUnit = fullUnit def unlink = false def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = { @@ -202,28 +204,19 @@ final class Eval( val hash = Hash.toHex(d) val moduleName = makeModuleName(hash) - lazy val unit = { - reporter.reset - ev.makeUnit - } - lazy val run = new Run { - override def units = (unit :: Nil).iterator - } - def unlinkAll(): Unit = - for ((sym, _) <- run.symSource) if (ev.unlink) unlink(sym) else toUnlinkLater ::= sym - - val (extra, loader) = backing match { - case Some(back) if classExists(back, moduleName) => - val loader = (parent: ClassLoader) => - (new URLClassLoader(Array(back.toURI.toURL), parent): ClassLoader) - val extra = ev.read(cacheFile(back, moduleName)) - (extra, loader) - case _ => - try { - compileAndLoad(run, unit, imports, backing, moduleName, ev) - } finally { - unlinkAll() - } + val (extra, loader) = try { + backing match { + case Some(back) if classExists(back, moduleName) => + val loader = (parent: ClassLoader) => + (new URLClassLoader(Array(back.toURI.toURL), parent): ClassLoader) + val extra = ev.read(cacheFile(back, moduleName)) + (extra, loader) + case _ => + compileAndLoad(imports, backing, moduleName, ev) + } + } finally { + // send a final report even if the class file was backed to reset preceding diagnostics + reporter.finalReport(ev.sourceName) } val generatedFiles = getGeneratedFiles(backing, moduleName) @@ -232,6 +225,25 @@ final class Eval( // location of the cached type or definition information private[this] def cacheFile(base: File, moduleName: String): File = new File(base, moduleName + ".cache") + + private def compileAndLoad[T]( + imports: EvalImports, + backing: Option[File], + moduleName: String, + ev: EvalType[T] + ): (T, ClassLoader => ClassLoader) = { + reporter.reset() + val unit = ev.makeUnit + val run = new Run { + override def units = (unit :: Nil).iterator + } + try { + compileAndLoad(run, unit, imports, backing, moduleName, ev) + } finally { + // unlink all + for ((sym, _) <- run.symSource) if (ev.unlink) unlink(sym) else toUnlinkLater ::= sym + } + } private[this] def compileAndLoad[T]( run: Run, unit: CompilationUnit, @@ -457,6 +469,8 @@ final class Eval( /** Serializes the extra information to a cache file, where it can be `read` back if inputs haven't changed.*/ def write(value: T, file: File): Unit + def sourceName: String + /** * Constructs the full compilation unit for this evaluation. * This is used for error reporting during compilation. diff --git a/main-actions/src/main/scala/sbt/compiler/EvalReporter.scala b/main-actions/src/main/scala/sbt/compiler/EvalReporter.scala new file mode 100644 index 000000000..485e8c7c2 --- /dev/null +++ b/main-actions/src/main/scala/sbt/compiler/EvalReporter.scala @@ -0,0 +1,54 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.compiler + +import scala.reflect.internal.settings.MutableSettings +import scala.reflect.internal.util.Position +import scala.tools.nsc.Settings +import scala.tools.nsc.reporters.{ ConsoleReporter, FilteringReporter } + +abstract class EvalReporter extends FilteringReporter { + def finalReport(sourceName: String): Unit +} + +object EvalReporter { + def console(s: Settings): EvalReporter = new ForwardingReporter(new ConsoleReporter(s)) +} + +class ForwardingReporter(delegate: FilteringReporter) extends EvalReporter { + def settings: Settings = delegate.settings + + def doReport(pos: Position, msg: String, severity: Severity): Unit = + delegate.doReport(pos, msg, severity) + + override def filter(pos: Position, msg: String, severity: Severity): Int = + delegate.filter(pos, msg, severity) + + override def increment(severity: Severity): Unit = delegate.increment(severity) + + override def errorCount: Int = delegate.errorCount + override def warningCount: Int = delegate.warningCount + + override def hasErrors: Boolean = delegate.hasErrors + override def hasWarnings: Boolean = delegate.hasWarnings + + override def comment(pos: Position, msg: String): Unit = delegate.comment(pos, msg) + + override def cancelled: Boolean = delegate.cancelled + override def cancelled_=(b: Boolean): Unit = delegate.cancelled_=(b) + + override def flush(): Unit = delegate.flush() + override def finish(): Unit = delegate.finish() + override def reset(): Unit = + delegate.reset() // super.reset not necessary, own state is never modified + + override def rerunWithDetails(setting: MutableSettings#Setting, name: String): String = + delegate.rerunWithDetails(setting, name) + + override def finalReport(sourceName: String): Unit = () +} diff --git a/main-actions/src/test/scala/sbt/compiler/EvalTest.scala b/main-actions/src/test/scala/sbt/compiler/EvalTest.scala index 21ffe61b9..4bea445b9 100644 --- a/main-actions/src/test/scala/sbt/compiler/EvalTest.scala +++ b/main-actions/src/test/scala/sbt/compiler/EvalTest.scala @@ -19,7 +19,7 @@ import sbt.io.IO class EvalTest extends Properties("eval") { private[this] lazy val reporter = new StoreReporter(new Settings()) import reporter.ERROR - private[this] lazy val eval = new Eval(_ => reporter, None) + private[this] lazy val eval = new Eval(_ => new ForwardingReporter(reporter), None) property("inferred integer") = forAll { (i: Int) => val result = eval.eval(i.toString) @@ -46,7 +46,7 @@ class EvalTest extends Properties("eval") { property("backed local class") = forAll { (i: Int) => IO.withTemporaryDirectory { dir => - val eval = new Eval(_ => reporter, backing = Some(dir)) + val eval = new Eval(_ => new ForwardingReporter(reporter), backing = Some(dir)) val result = eval.eval(local(i)) val v = value(result).asInstanceOf[{ def i: Int }].i (label("Value", v) |: (v == i)) && diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 0235e31d8..55da21c94 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -23,6 +23,7 @@ import sbt.librarymanagement.{ Resolver, UpdateReport } import sbt.std.Transform.DummyTaskMap import sbt.util.{ Logger, Show } import sbt.BuildSyntax._ +import sbt.internal.bsp.BuildTargetIdentifier import scala.annotation.nowarn import scala.Console.RED @@ -147,14 +148,15 @@ final case class PluginData( unmanagedSourceDirectories: Seq[File], unmanagedSources: Seq[File], managedSourceDirectories: Seq[File], - managedSources: Seq[File] + managedSources: Seq[File], + buildTarget: Option[BuildTargetIdentifier] ) { val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath } object PluginData { private[sbt] def apply(dependencyClasspath: Def.Classpath): PluginData = - PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil) + PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil, None) } object EvaluateTask { diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index 684586f5a..08965b0ce 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -8,19 +8,17 @@ package sbt package internal -import java.io.File -import java.net.URI - import sbt.BuildPaths._ import sbt.Def.{ ScopeLocal, ScopedKey, Setting, isDummy } import sbt.Keys._ import sbt.Project.inScope import sbt.Scope.GlobalScope import sbt.SlashSyntax0._ -import sbt.compiler.Eval +import sbt.compiler.{ Eval, EvalReporter } import sbt.internal.BuildStreams._ import sbt.internal.inc.classpath.ClasspathUtil import sbt.internal.inc.{ ScalaInstance, ZincLmUtil, ZincUtil } +import sbt.internal.server.BuildServerEvalReporter import sbt.internal.util.Attributed.data import sbt.internal.util.Types.const import sbt.internal.util.{ Attributed, Settings, ~> } @@ -31,6 +29,8 @@ import sbt.nio.Settings import sbt.util.{ Logger, Show } import xsbti.compile.{ ClasspathOptionsUtil, Compilers } +import java.io.File +import java.net.URI import scala.annotation.{ nowarn, tailrec } import scala.collection.mutable import scala.tools.nsc.reporters.ConsoleReporter @@ -426,14 +426,21 @@ private[sbt] object Load { () => eval } - def mkEval(unit: BuildUnit): Eval = - mkEval(unit.definitions, unit.plugins, unit.plugins.pluginData.scalacOptions) - - def mkEval(defs: LoadedDefinitions, plugs: LoadedPlugins, options: Seq[String]): Eval = - mkEval(defs.target ++ plugs.classpath, defs.base, options) + def mkEval(unit: BuildUnit): Eval = { + val defs = unit.definitions + mkEval(defs.target ++ unit.plugins.classpath, defs.base, unit.plugins.pluginData.scalacOptions) + } def mkEval(classpath: Seq[File], base: File, options: Seq[String]): Eval = - new Eval(options, classpath, s => new ConsoleReporter(s), Some(evalOutputDirectory(base))) + mkEval(classpath, base, options, EvalReporter.console) + + def mkEval( + classpath: Seq[File], + base: File, + options: Seq[String], + mkReporter: scala.tools.nsc.Settings => EvalReporter + ): Eval = + new Eval(options, classpath, mkReporter, Some(evalOutputDirectory(base))) /** * This will clean up left-over files in the config-classes directory if they are no longer used. @@ -703,7 +710,13 @@ private[sbt] object Load { // NOTE - because we create an eval here, we need a clean-eval later for this URI. lazy val eval = timed("Load.loadUnit: mkEval", log) { - mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions) + def mkReporter(settings: scala.tools.nsc.Settings): EvalReporter = + plugs.pluginData.buildTarget match { + case None => EvalReporter.console(settings) + case Some(buildTarget) => + new BuildServerEvalReporter(buildTarget, new ConsoleReporter(settings)) + } + mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions, mkReporter) } val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase)) ++ buildLevelExtraProjects @@ -1168,6 +1181,7 @@ private[sbt] object Load { val unmanagedSrcs = (Configurations.Compile / unmanagedSources).value val managedSrcDirs = (Configurations.Compile / managedSourceDirectories).value val managedSrcs = (Configurations.Compile / managedSources).value + val buildTarget = (Configurations.Compile / bspTargetIdentifier).value PluginData( removeEntries(cp, prod), prod, @@ -1178,6 +1192,7 @@ private[sbt] object Load { unmanagedSrcs, managedSrcDirs, managedSrcs, + Some(buildTarget) ) }, scalacOptions += "-Wconf:cat=unused-nowarn:s", @@ -1233,7 +1248,7 @@ private[sbt] object Load { loadPluginDefinition( dir, config, - PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil) + PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil, None) ) def buildPlugins(dir: File, s: State, config: LoadBuildConfiguration): LoadedPlugins = @@ -1429,7 +1444,8 @@ final case class LoadBuildConfiguration( Nil, Nil, Nil, - Nil + Nil, + None ) case None => PluginData(globalPluginClasspath) } diff --git a/main/src/main/scala/sbt/internal/server/BuildServerEvalReporter.scala b/main/src/main/scala/sbt/internal/server/BuildServerEvalReporter.scala new file mode 100644 index 000000000..cc4475e75 --- /dev/null +++ b/main/src/main/scala/sbt/internal/server/BuildServerEvalReporter.scala @@ -0,0 +1,93 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.server + +import sbt.StandardMain.exchange +import sbt.compiler.ForwardingReporter +import sbt.internal.bsp +import sbt.internal.bsp.{ + BuildTargetIdentifier, + Diagnostic, + DiagnosticSeverity, + PublishDiagnosticsParams, + Range, + TextDocumentIdentifier +} + +import java.nio.file.{ Files, Path, Paths } +import scala.collection.mutable +import scala.reflect.internal.Reporter +import scala.reflect.internal.util.{ DefinedPosition, Position } +import scala.tools.nsc.reporters.FilteringReporter +import sbt.internal.bsp.codec.JsonProtocol._ + +class BuildServerEvalReporter(buildTarget: BuildTargetIdentifier, delegate: FilteringReporter) + extends ForwardingReporter(delegate) { + private val problemsByFile = mutable.Map[Path, Vector[Diagnostic]]() + + override def doReport(pos: Position, msg: String, severity: Severity): Unit = { + for { + filePath <- if (pos.source.file.exists) Some(Paths.get(pos.source.file.path)) else None + range <- convertToRange(pos) + } { + val bspSeverity = convertToBsp(severity) + val diagnostic = Diagnostic(range, bspSeverity, None, Option("sbt"), msg) + problemsByFile(filePath) = problemsByFile.getOrElse(filePath, Vector()) :+ diagnostic + val params = PublishDiagnosticsParams( + TextDocumentIdentifier(filePath.toUri), + buildTarget, + originId = None, + Vector(diagnostic), + reset = false + ) + exchange.notifyEvent("build/publishDiagnostics", params) + } + super.doReport(pos, msg, severity) + } + + override def finalReport(sourceName: String): Unit = { + val filePath = Paths.get(sourceName) + if (Files.exists(filePath)) { + val diagnostics = problemsByFile.getOrElse(filePath, Vector()) + val params = PublishDiagnosticsParams( + textDocument = TextDocumentIdentifier(filePath.toUri), + buildTarget, + originId = None, + diagnostics, + reset = true + ) + exchange.notifyEvent("build/publishDiagnostics", params) + } + } + + private def convertToBsp(severity: Severity): Option[Long] = { + val result = severity match { + case Reporter.INFO => DiagnosticSeverity.Information + case Reporter.WARNING => DiagnosticSeverity.Warning + case Reporter.ERROR => DiagnosticSeverity.Error + } + Some(result) + } + + private def convertToRange(pos: Position): Option[Range] = { + pos match { + case _: DefinedPosition => + val startLine = pos.source.offsetToLine(pos.start) + val startChar = pos.start - pos.source.lineToOffset(startLine) + val endLine = pos.source.offsetToLine(pos.end) + val endChar = pos.end - pos.source.lineToOffset(endLine) + Some( + Range( + bsp.Position(startLine.toLong, startChar.toLong), + bsp.Position(endLine.toLong, endChar.toLong) + ) + ) + case _ => None + } + } +} diff --git a/main/src/test/scala/PluginCommandTest.scala b/main/src/test/scala/PluginCommandTest.scala index b8cf07768..eb8d9d593 100644 --- a/main/src/test/scala/PluginCommandTest.scala +++ b/main/src/test/scala/PluginCommandTest.scala @@ -114,7 +114,7 @@ object FakeState { Nil ) - val pluginData = PluginData(Nil, Nil, None, None, Nil, Nil, Nil, Nil, Nil) + val pluginData = PluginData(Nil, Nil, None, None, Nil, Nil, Nil, Nil, Nil, None) val builds: DetectedModules[BuildDef] = new DetectedModules[BuildDef](Nil) val detectedAutoPlugins: Seq[DetectedAutoPlugin] = From b4c6299b9cc71a62ecb14a75519617470c3da6f3 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Mon, 28 Jun 2021 15:35:09 +0200 Subject: [PATCH 11/23] [BSP] Add test on failing workspace/reload --- .../test/scala/testpkg/BuildServerTest.scala | 122 +++++++++++++----- 1 file changed, 92 insertions(+), 30 deletions(-) diff --git a/server-test/src/test/scala/testpkg/BuildServerTest.scala b/server-test/src/test/scala/testpkg/BuildServerTest.scala index 9d4e163a7..f9c31ecb7 100644 --- a/server-test/src/test/scala/testpkg/BuildServerTest.scala +++ b/server-test/src/test/scala/testpkg/BuildServerTest.scala @@ -8,10 +8,11 @@ package testpkg import sbt.internal.bsp.SourcesResult +import sbt.internal.bsp.WorkspaceBuildTargetsResult +import sbt.internal.langserver.ErrorCodes +import sbt.IO import java.io.File -import sbt.internal.bsp.WorkspaceBuildTargetsResult - import scala.concurrent.duration._ // starts svr using server-test/buildserver and perform custom server tests @@ -43,10 +44,10 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/sources") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#util/Compile" + val buildTarget = buildTargetUri("util", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "24", "method": "buildTarget/sources", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/sources")) @@ -80,10 +81,10 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/compile") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#util/Compile" + val buildTarget = buildTargetUri("util", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "32", "method": "buildTarget/compile", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/compile")) @@ -94,10 +95,10 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/scalacOptions") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#util/Compile" + val buildTarget = buildTargetUri("util", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "40", "method": "buildTarget/scalacOptions", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/scalacOptions")) @@ -118,11 +119,66 @@ object BuildServerTest extends AbstractServerTest { }) } + test("workspace/reload: send diagnostic and respond with error") { _ => + // write an other-build.sbt file that does not compile + val otherBuildFile = new File(svr.baseDirectory, "other-build.sbt") + IO.write( + otherBuildFile, + """ + |val someSettings = Seq( + | scalacOptions ++= "-deprecation" + |) + |""".stripMargin + ) + // reload + svr.sendJsonRpc( + """{ "jsonrpc": "2.0", "id": "52", "method": "workspace/reload"}""" + ) + assert( + svr.waitForString(10.seconds) { s => + s.contains(s""""buildTarget":{"uri":"$metaBuildTarget"}""") && + s.contains(s""""textDocument":{"uri":"${otherBuildFile.toPath.toUri}"}""") && + s.contains(""""severity":1""") && + s.contains(""""reset":true""") + } + ) + assert( + svr.waitForString(10.seconds) { s => + s.contains(""""id":"52"""") && + s.contains(""""error"""") && + s.contains(s""""code":${ErrorCodes.InternalError}""") && + s.contains("Type error in expression") + } + ) + // fix the other-build.sbt file and reload again + IO.write( + otherBuildFile, + """ + |val someSettings = Seq( + | scalacOptions += "-deprecation" + |) + |""".stripMargin + ) + svr.sendJsonRpc( + """{ "jsonrpc": "2.0", "id": "52", "method": "workspace/reload"}""" + ) + // assert received an empty diagnostic + assert( + svr.waitForString(10.seconds) { s => + s.contains(s""""buildTarget":{"uri":"$metaBuildTarget"}""") && + s.contains(s""""textDocument":{"uri":"${otherBuildFile.toPath.toUri}"}""") && + s.contains(""""diagnostics":[]""") && + s.contains(""""reset":true""") + } + ) + IO.delete(otherBuildFile) + } + test("buildTarget/scalaMainClasses") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#runAndTest/Compile" + val buildTarget = buildTargetUri("runAndTest", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "56", "method": "buildTarget/scalaMainClasses", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/scalaMainClasses")) @@ -133,10 +189,10 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/run") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#runAndTest/Compile" + val buildTarget = buildTargetUri("runAndTest", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "64", "method": "buildTarget/run", "params": { - | "target": { "uri": "$x" }, + | "target": { "uri": "$buildTarget" }, | "dataKind": "scala-main-class", | "data": { "class": "main.Main" } |} }""".stripMargin @@ -153,10 +209,10 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/scalaTestClasses") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#runAndTest/Test" + val buildTarget = buildTargetUri("runAndTest", "Test") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "72", "method": "buildTarget/scalaTestClasses", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/scalaTestClasses")) @@ -168,10 +224,10 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/test: run all tests") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#runAndTest/Test" + val buildTarget = buildTargetUri("runAndTest", "Test") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "80", "method": "buildTarget/test", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/test")) @@ -182,15 +238,15 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/test: run one test class") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#runAndTest/Test" + val buildTarget = buildTargetUri("runAndTest", "Test") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "84", "method": "buildTarget/test", "params": { - | "targets": [{ "uri": "$x" }], + | "targets": [{ "uri": "$buildTarget" }], | "dataKind": "scala-test", | "data": { | "testClasses": [ | { - | "target": { "uri": "$x" }, + | "target": { "uri": "$buildTarget" }, | "classes": ["tests.PassingTest"] | } | ] @@ -205,53 +261,53 @@ object BuildServerTest extends AbstractServerTest { } test("buildTarget/compile: report error") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#reportError/Compile" + val buildTarget = buildTargetUri("reportError", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "88", "method": "buildTarget/compile", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(svr.waitForString(10.seconds) { s => - (s contains s""""buildTarget":{"uri":"$x"}""") && + (s contains s""""buildTarget":{"uri":"$buildTarget"}""") && (s contains """"severity":1""") && (s contains """"reset":true""") }) } test("buildTarget/compile: report warning") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#reportWarning/Compile" + val buildTarget = buildTargetUri("reportWarning", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "90", "method": "buildTarget/compile", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(svr.waitForString(10.seconds) { s => - (s contains s""""buildTarget":{"uri":"$x"}""") && + (s contains s""""buildTarget":{"uri":"$buildTarget"}""") && (s contains """"severity":2""") && (s contains """"reset":true""") }) } test("buildTarget/compile: respond error") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#respondError/Compile" + val buildTarget = buildTargetUri("respondError", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "92", "method": "buildTarget/compile", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(svr.waitForString(10.seconds) { s => s.contains(""""id":"92"""") && s.contains(""""error"""") && - s.contains(""""code":-32603""") && + s.contains(s""""code":${ErrorCodes.InternalError}""") && s.contains("custom message") }) } test("buildTarget/resources") { _ => - val x = s"${svr.baseDirectory.getAbsoluteFile.toURI}#util/Compile" + val buildTarget = buildTargetUri("util", "Compile") svr.sendJsonRpc( s"""{ "jsonrpc": "2.0", "id": "96", "method": "buildTarget/resources", "params": { - | "targets": [{ "uri": "$x" }] + | "targets": [{ "uri": "$buildTarget" }] |} }""".stripMargin ) assert(processing("buildTarget/resources")) @@ -281,4 +337,10 @@ object BuildServerTest extends AbstractServerTest { msg.contains(s""""message":"Processing $method"""") } } + + private def buildTargetUri(project: String, config: String): String = + s"${svr.baseDirectory.getAbsoluteFile.toURI}#$project/$config" + + private def metaBuildTarget: String = + s"${svr.baseDirectory.getAbsoluteFile.toURI}project/#buildserver-build/Compile" } From 94cc9a84ac733bbd9dfdf4f2f2c60512db1a58c4 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Thu, 8 Jul 2021 09:13:22 +0200 Subject: [PATCH 12/23] Fix mima binary issues --- build.sbt | 2 ++ .../src/main/scala/sbt/compiler/Eval.scala | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/build.sbt b/build.sbt index bc81d2bef..55a26b6ab 100644 --- a/build.sbt +++ b/build.sbt @@ -681,6 +681,8 @@ lazy val actionsProj = (project in file("main-actions")) exclude[DirectMissingMethodProblem]("sbt.compiler.Eval.filesModifiedBytes"), exclude[DirectMissingMethodProblem]("sbt.compiler.Eval.fileModifiedBytes"), exclude[DirectMissingMethodProblem]("sbt.Doc.$init$"), + // Added field in nested private[this] class + exclude[ReversedMissingMethodProblem]("sbt.compiler.Eval#EvalType.sourceName"), ), ) .configure( diff --git a/main-actions/src/main/scala/sbt/compiler/Eval.scala b/main-actions/src/main/scala/sbt/compiler/Eval.scala index ea841dbd2..df0386262 100644 --- a/main-actions/src/main/scala/sbt/compiler/Eval.scala +++ b/main-actions/src/main/scala/sbt/compiler/Eval.scala @@ -81,8 +81,8 @@ final class Eval( new CompilerCommand(options.toList, s) // this side-effects on Settings.. s } - lazy val reporter = mkReporter(settings) - + private lazy val evalReporter = mkReporter(settings) + def reporter: Reporter = evalReporter // kept for binary compatibility /** * Subclass of Global which allows us to mutate currentRun from outside. * See for rationale https://issues.scala-lang.org/browse/SI-8794 @@ -95,7 +95,7 @@ final class Eval( } var curRun: Run = null } - lazy val global: EvalGlobal = new EvalGlobal(settings, reporter) + lazy val global: EvalGlobal = new EvalGlobal(settings, evalReporter) import global._ private[sbt] def unlinkDeferred(): Unit = { @@ -216,7 +216,7 @@ final class Eval( } } finally { // send a final report even if the class file was backed to reset preceding diagnostics - reporter.finalReport(ev.sourceName) + evalReporter.finalReport(ev.sourceName) } val generatedFiles = getGeneratedFiles(backing, moduleName) @@ -232,7 +232,7 @@ final class Eval( moduleName: String, ev: EvalType[T] ): (T, ClassLoader => ClassLoader) = { - reporter.reset() + evalReporter.reset() val unit = ev.makeUnit val run = new Run { override def units = (unit :: Nil).iterator @@ -262,7 +262,7 @@ final class Eval( def compile(phase: Phase): Unit = { globalPhase = phase - if (phase == null || phase == phase.next || reporter.hasErrors) + if (phase == null || phase == phase.next || evalReporter.hasErrors) () else { enteringPhase(phase) { phase.run } @@ -498,7 +498,7 @@ final class Eval( private[this] def mkUnit(srcName: String, firstLine: Int, s: String) = new CompilationUnit(new EvalSourceFile(srcName, firstLine, s)) private[this] def checkError(label: String) = - if (reporter.hasErrors) throw new EvalException(label) + if (evalReporter.hasErrors) throw new EvalException(label) private[this] final class EvalSourceFile(name: String, startLine: Int, contents: String) extends BatchSourceFile(name, contents) { From 63dcb1957bcdff790db54f54fcf542607c74b5b7 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Thu, 29 Apr 2021 13:30:48 +0200 Subject: [PATCH 13/23] Move scalaInstanceTopLoader to compileBase settings --- main/src/main/scala/sbt/Defaults.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 27a19aab6..0541ef90b 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -332,13 +332,6 @@ object Defaults extends BuildCommon { turbo :== SysProp.turbo, usePipelining :== SysProp.pipelining, exportPipelining := usePipelining.value, - useScalaReplJLine :== false, - scalaInstanceTopLoader := { - // the JLineLoader contains the SbtInterfaceClassLoader - if (!useScalaReplJLine.value) - classOf[org.jline.terminal.Terminal].getClassLoader // the JLineLoader - else classOf[Compilers].getClassLoader // the SbtInterfaceClassLoader - }, useSuperShell := { if (insideCI.value) false else ITerminal.console.isSupershellEnabled }, superShellThreshold :== SysProp.supershellThreshold, superShellMaxTasks :== SysProp.supershellMaxTasks, @@ -667,6 +660,13 @@ object Defaults extends BuildCommon { // This is included into JvmPlugin.projectSettings def compileBase = inTask(console)(compilersSetting :: Nil) ++ compileBaseGlobal ++ Seq( + useScalaReplJLine :== false, + scalaInstanceTopLoader := { + // the JLineLoader contains the SbtInterfaceClassLoader + if (!useScalaReplJLine.value) + classOf[org.jline.terminal.Terminal].getClassLoader // the JLineLoader + else classOf[Compilers].getClassLoader // the SbtInterfaceClassLoader + }, scalaInstance := scalaInstanceTask.value, crossVersion := (if (crossPaths.value) CrossVersion.binary else CrossVersion.disabled), pluginCrossBuild / sbtBinaryVersion := binarySbtVersion( From 69795cbed7c025742fed71fbddf895b78ad5ad18 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Thu, 29 Apr 2021 13:36:55 +0200 Subject: [PATCH 14/23] Move jansiExclusionLoader to scalaInstanceTopLoader --- main/src/main/scala/sbt/Defaults.scala | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 0541ef90b..fc70be22b 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -662,10 +662,20 @@ object Defaults extends BuildCommon { def compileBase = inTask(console)(compilersSetting :: Nil) ++ compileBaseGlobal ++ Seq( useScalaReplJLine :== false, scalaInstanceTopLoader := { - // the JLineLoader contains the SbtInterfaceClassLoader - if (!useScalaReplJLine.value) - classOf[org.jline.terminal.Terminal].getClassLoader // the JLineLoader - else classOf[Compilers].getClassLoader // the SbtInterfaceClassLoader + val topLoader = if (!useScalaReplJLine.value) { + // the JLineLoader contains the SbtInterfaceClassLoader + classOf[org.jline.terminal.Terminal].getClassLoader + } else classOf[Compilers].getClassLoader // the SbtInterfaceClassLoader + + // Scala 2.10 shades jline in the console so we need to make sure that it loads a compatible + // jansi version. Because of the shading, console does not work with the thin client for 2.10.x. + if (scalaVersion.value.startsWith("2.10.")) new ClassLoader(topLoader) { + override protected def loadClass(name: String, resolve: Boolean): Class[_] = { + if (name.startsWith("org.fusesource")) throw new ClassNotFoundException(name) + super.loadClass(name, resolve) + } + } + else topLoader }, scalaInstance := scalaInstanceTask.value, crossVersion := (if (crossPaths.value) CrossVersion.binary else CrossVersion.disabled), @@ -1163,16 +1173,6 @@ object Defaults extends BuildCommon { state: State, topLoader: ClassLoader, ): ScalaInstance = { - // Scala 2.10 shades jline in the console so we need to make sure that it loads a compatible - // jansi version. Because of the shading, console does not work with the thin client for 2.10.x. - val jansiExclusionLoader = if (version.startsWith("2.10.")) new ClassLoader(topLoader) { - override protected def loadClass(name: String, resolve: Boolean): Class[_] = { - if (name.startsWith("org.fusesource")) throw new ClassNotFoundException(name) - super.loadClass(name, resolve) - } - } - else topLoader - val classLoaderCache = state.extendedClassLoaderCache val compilerJars = allCompilerJars.filterNot(libraryJars.contains).distinct.toArray val docJars = allDocJars @@ -1181,7 +1181,7 @@ object Defaults extends BuildCommon { .toArray val allJars = libraryJars ++ compilerJars ++ docJars - val libraryLoader = classLoaderCache(libraryJars.toList, jansiExclusionLoader) + val libraryLoader = classLoaderCache(libraryJars.toList, topLoader) val compilerLoader = classLoaderCache(compilerJars.toList, libraryLoader) val fullLoader = if (docJars.isEmpty) compilerLoader From 5490385303a5f5bfa90071ca650f3e0c1c4e81ed Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Thu, 29 Apr 2021 14:06:00 +0200 Subject: [PATCH 15/23] Remove topLoader hack for dotty --- main/src/main/scala/sbt/Main.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 45ff32133..f9771b134 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -974,9 +974,6 @@ object BuiltinCommands { st => setupGlobalFileTreeRepository(addCacheStoreFactoryFactory(st)) ) val s4 = s3.put(Keys.useLog4J.key, Project.extract(s3).get(Keys.useLog4J)) - // This is a workaround for the console task in dotty which uses the classloader cache. - // We need to override the top loader in that case so that it gets the forked jline. - s4.extendedClassLoaderCache.setParent(Project.extract(s4).get(Keys.scalaInstanceTopLoader)) addSuperShellParams(CheckBuildSources.init(LintUnused.lintUnusedFunc(s4))) } From 181052fddb06fa8bccb01d8548fa61b71d131502 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 11 Jul 2021 02:04:48 -0400 Subject: [PATCH 16/23] launcher 1.3.3 Fixes https://github.com/sbt/sbt/issues/6587 --- launch/src/main/input_resources/sbt/sbt.boot.properties | 2 -- project/Dependencies.scala | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/launch/src/main/input_resources/sbt/sbt.boot.properties b/launch/src/main/input_resources/sbt/sbt.boot.properties index f13961dcf..2aa284ef8 100644 --- a/launch/src/main/input_resources/sbt/sbt.boot.properties +++ b/launch/src/main/input_resources/sbt/sbt.boot.properties @@ -12,8 +12,6 @@ [repositories] local - local-preloaded-ivy: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext] - local-preloaded: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/} maven-central sbt-maven-releases: https://repo.scala-sbt.org/scalasbt/maven-releases/, bootOnly sbt-maven-snapshots: https://repo.scala-sbt.org/scalasbt/maven-snapshots/, bootOnly diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 76ed2003e..3d398e5bb 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -22,7 +22,7 @@ object Dependencies { private val libraryManagementCore = "org.scala-sbt" %% "librarymanagement-core" % lmVersion private val libraryManagementIvy = "org.scala-sbt" %% "librarymanagement-ivy" % lmVersion - val launcherVersion = "1.3.2" + val launcherVersion = "1.3.3" val launcherInterface = "org.scala-sbt" % "launcher-interface" % launcherVersion val rawLauncher = "org.scala-sbt" % "launcher" % launcherVersion val testInterface = "org.scala-sbt" % "test-interface" % "1.0" From d24605992563e46657ff55e40650e5e51752c928 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 11 Jul 2021 02:04:48 -0400 Subject: [PATCH 17/23] launcher 1.3.3 Fixes https://github.com/sbt/sbt/issues/6587 --- launch/src/main/input_resources/sbt/sbt.boot.properties | 2 -- project/Dependencies.scala | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/launch/src/main/input_resources/sbt/sbt.boot.properties b/launch/src/main/input_resources/sbt/sbt.boot.properties index f13961dcf..2aa284ef8 100644 --- a/launch/src/main/input_resources/sbt/sbt.boot.properties +++ b/launch/src/main/input_resources/sbt/sbt.boot.properties @@ -12,8 +12,6 @@ [repositories] local - local-preloaded-ivy: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext] - local-preloaded: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/} maven-central sbt-maven-releases: https://repo.scala-sbt.org/scalasbt/maven-releases/, bootOnly sbt-maven-snapshots: https://repo.scala-sbt.org/scalasbt/maven-snapshots/, bootOnly diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 76ed2003e..3d398e5bb 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -22,7 +22,7 @@ object Dependencies { private val libraryManagementCore = "org.scala-sbt" %% "librarymanagement-core" % lmVersion private val libraryManagementIvy = "org.scala-sbt" %% "librarymanagement-ivy" % lmVersion - val launcherVersion = "1.3.2" + val launcherVersion = "1.3.3" val launcherInterface = "org.scala-sbt" % "launcher-interface" % launcherVersion val rawLauncher = "org.scala-sbt" % "launcher" % launcherVersion val testInterface = "org.scala-sbt" % "test-interface" % "1.0" From ba73f87296a342955c252aef8101345c74940b99 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Fri, 9 Jul 2021 08:41:24 +0200 Subject: [PATCH 18/23] add scaladoc in EvalReporter --- .../src/main/scala/sbt/compiler/EvalReporter.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main-actions/src/main/scala/sbt/compiler/EvalReporter.scala b/main-actions/src/main/scala/sbt/compiler/EvalReporter.scala index 485e8c7c2..7ae284231 100644 --- a/main-actions/src/main/scala/sbt/compiler/EvalReporter.scala +++ b/main-actions/src/main/scala/sbt/compiler/EvalReporter.scala @@ -12,7 +12,15 @@ import scala.reflect.internal.util.Position import scala.tools.nsc.Settings import scala.tools.nsc.reporters.{ ConsoleReporter, FilteringReporter } +/** + * Reporter used to compile *.sbt files that forwards compiler diagnostics to BSP clients + */ abstract class EvalReporter extends FilteringReporter { + + /** + * Send a final report to clear out the outdated diagnostics. + * @param sourceName a *.sbt file + */ def finalReport(sourceName: String): Unit } From c9ca2d4afa6a81203da68bc8ca4cbbf0487eaa29 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Mon, 12 Jul 2021 14:20:17 +0200 Subject: [PATCH 19/23] Use `-Dsbt.script` to start sbt server In order to start the sbt server we must use the sbt script because it is the only way to load the .sbtopts and the .jvmopts file properly. To do so the sbt script can pass a -Dsbt.script prop to the java server. It is used in the NetworkClient to start the server, and it is replicated in the BuildServerConnection file (.bsp/sbt.json). --- .../sbt/internal/client/NetworkClient.scala | 15 +++-- .../internal/bsp/BuildServerConnection.scala | 60 ++++++++++++------- sbt | 11 ++++ 3 files changed, 61 insertions(+), 25 deletions(-) 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..d9514a323 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -1045,7 +1045,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 +1068,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 +1091,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..9ee2da80c 100644 --- a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala +++ b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala @@ -7,12 +7,13 @@ 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 } + object BuildServerConnection { final val name = "sbt" final val bspVersion = "2.0.0-M5" @@ -21,26 +22,45 @@ 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 sbtLaunchJar = classPath - .split(File.pathSeparator) - .find(jar => SbtLaunchJar.findFirstIn(jar).nonEmpty) - .map(_.replaceAllLiterally(" ", "%20")) - val argv = - Vector( - s"$javaHome/bin/java", - "-Xms100m", - "-Xmx100m", - "-classpath", - classPath, - "xsbt.boot.Boot", - "-bsp" - ) ++ sbtLaunchJar.map(jar => s"--sbt-launch-jar=$jar") + val argv = Option(System.getProperty("sbt.script")) + .map(_.replaceAllLiterally("%20", " ")) + .orElse(sbtScriptInPath) match { + case Some(sbtScript) => + Vector(sbtScript, "-bsp", s"-Dsbt.script=${sbtScript.replaceAllLiterally(" ", "%20")}") + case None => + // 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 javaHome = System.getProperty("java.home") + val classPath = System.getProperty("java.class.path") + val sbtLaunchJar = classPath + .split(File.pathSeparator) + .find(jar => SbtLaunchJar.findFirstIn(jar).nonEmpty) + .map(_.replaceAllLiterally(" ", "%20")) + + Vector( + s"$javaHome/bin/java", + "-Xms100m", + "-Xmx100m", + "-classpath", + classPath, + "xsbt.boot.Boot", + "-bsp" + ) ++ sbtLaunchJar.map(jar => s"--sbt-launch-jar=$jar") + } 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 envPath = Option(System.getenv("PATH")).getOrElse("") + val allPaths = envPath.split(File.pathSeparator).map(Paths.get(_)) + allPaths.map(_.resolve("sbt")).find(Files.exists(_)).map(_.toString) + } } diff --git a/sbt b/sbt index 8adff2117..990515d6c 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 From c31503cca291c5c07211e85f0cf4d89b65814ffc Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Wed, 14 Jul 2021 10:23:44 +0200 Subject: [PATCH 20/23] Warn when using -sbt-launch-jar --- .../scala/sbt/internal/client/NetworkClient.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 d9514a323..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 _ => From c4e6cf54d29857d9dcf40c085ebfdaf11887976c Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Wed, 14 Jul 2021 10:24:52 +0200 Subject: [PATCH 21/23] Use java to start BSP client --- .../internal/bsp/BuildServerConnection.scala | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala index 9ee2da80c..593d925e3 100644 --- a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala +++ b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala @@ -13,6 +13,7 @@ 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" @@ -23,34 +24,34 @@ object BuildServerConnection { private[sbt] def writeConnectionFile(sbtVersion: String, baseDir: File): Unit = { val bspConnectionFile = new File(baseDir, ".bsp/sbt.json") - val argv = Option(System.getProperty("sbt.script")) - .map(_.replaceAllLiterally("%20", " ")) - .orElse(sbtScriptInPath) match { - case Some(sbtScript) => - Vector(sbtScript, "-bsp", s"-Dsbt.script=${sbtScript.replaceAllLiterally(" ", "%20")}") - case None => - // 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 javaHome = System.getProperty("java.home") - val classPath = System.getProperty("java.class.path") - val sbtLaunchJar = classPath - .split(File.pathSeparator) - .find(jar => SbtLaunchJar.findFirstIn(jar).nonEmpty) - .map(_.replaceAllLiterally(" ", "%20")) + val javaHome = System.getProperty("java.home") + val classPath = System.getProperty("java.class.path") - Vector( - s"$javaHome/bin/java", - "-Xms100m", - "-Xmx100m", - "-classpath", - classPath, - "xsbt.boot.Boot", - "-bsp" - ) ++ sbtLaunchJar.map(jar => s"--sbt-launch-jar=$jar") - } + 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", + "-Xms100m", + "-Xmx100m", + "-classpath", + classPath, + "xsbt.boot.Boot", + "-bsp" + ) ++ sbtScript.orElse(sbtLaunchJar) val details = BspConnectionDetails(name, sbtVersion, bspVersion, languages, argv) val json = Converter.toJson(details).get IO.write(bspConnectionFile, CompactPrinter(json), append = false) @@ -61,6 +62,9 @@ object BuildServerConnection { // As a fallback we try to find the sbt script in $PATH val envPath = Option(System.getenv("PATH")).getOrElse("") val allPaths = envPath.split(File.pathSeparator).map(Paths.get(_)) - allPaths.map(_.resolve("sbt")).find(Files.exists(_)).map(_.toString) + allPaths + .map(_.resolve("sbt")) + .find(file => Files.exists(file)) + .map(_.toString.replaceAllLiterally(" ", "%20")) } } From 2287f3e3844271d8391075198b1dff5452df6aa3 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Wed, 14 Jul 2021 10:25:20 +0200 Subject: [PATCH 22/23] Find sbt.bat in windows $PATH --- .../main/scala/sbt/internal/bsp/BuildServerConnection.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala index 593d925e3..85c2bd3cb 100644 --- a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala +++ b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala @@ -60,11 +60,12 @@ 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 = Option(System.getenv("PATH")).getOrElse("") val allPaths = envPath.split(File.pathSeparator).map(Paths.get(_)) allPaths - .map(_.resolve("sbt")) - .find(file => Files.exists(file)) + .map(_.resolve(fileName)) + .find(file => Files.exists(file) && Files.isExecutable(file)) .map(_.toString.replaceAllLiterally(" ", "%20")) } } From 966633aa8192ef285fe3dac05b318a174867b08e Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Wed, 14 Jul 2021 10:32:02 +0200 Subject: [PATCH 23/23] Fix duplicated -Dsbt.script --- sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbt b/sbt index 990515d6c..7fb865514 100755 --- a/sbt +++ b/sbt @@ -300,7 +300,7 @@ addDefaultMemory() { } addSbtScriptProperty () { - if [[ "${java_args[@]}" == -Dsbt.script=* ]]; then + if [[ "${java_args[@]}" == *-Dsbt.script=* ]]; then : else sbt_script=$0