Skip to content

Commit

Permalink
improvement: add custom bsp as possible build tool (#5791)
Browse files Browse the repository at this point in the history
  • Loading branch information
kasiaMarek authored Dec 18, 2023
1 parent 72692d2 commit 73706fa
Show file tree
Hide file tree
Showing 25 changed files with 417 additions and 238 deletions.
49 changes: 38 additions & 11 deletions metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import scala.concurrent.Future

import scala.meta.internal.bsp.BspConfigGenerationStatus._
import scala.meta.internal.builds.BuildServerProvider
import scala.meta.internal.builds.BuildTool
import scala.meta.internal.builds.BuildTools
import scala.meta.internal.builds.SbtBuildTool
import scala.meta.internal.builds.ScalaCliBuildTool
import scala.meta.internal.builds.ShellRunner
import scala.meta.internal.metals.BloopServers
import scala.meta.internal.metals.BuildServerConnection
Expand All @@ -18,6 +20,7 @@ import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.StatusBar
import scala.meta.internal.metals.Tables
import scala.meta.internal.metals.UserConfiguration
import scala.meta.internal.metals.scalacli.ScalaCli
import scala.meta.internal.semver.SemVer
import scala.meta.io.AbsolutePath

Expand All @@ -43,13 +46,9 @@ class BspConnector(
* Resolves the current build servers that either have a bsp entry or if the
* workspace can support Bloop, it will also resolve Bloop.
*/
def resolve(): BspResolvedResult = {
def resolve(buildTool: Option[BuildTool]): BspResolvedResult = {
resolveExplicit().getOrElse {
if (
buildTools
.loadSupported()
.exists(_.isBloopDefaultBsp) || buildTools.isBloop
)
if (buildTool.exists(_.isBloopInstallProvider) || buildTools.isBloop)
ResolvedBloop
else bspServers.resolve()
}
Expand All @@ -61,7 +60,10 @@ class BspConnector(
else
bspServers
.findAvailableServers()
.find(_.getName == sel)
.find(buildServer =>
(ScalaCli.names(buildServer.getName()) && ScalaCli.names(sel)) ||
buildServer.getName == sel
)
.map(ResolvedBspOne)
}
}
Expand All @@ -74,19 +76,20 @@ class BspConnector(
* of the bsp entry has already happened at this point.
*/
def connect(
projectRoot: AbsolutePath,
buildTool: Option[BuildTool],
workspace: AbsolutePath,
userConfiguration: UserConfiguration,
shellRunner: ShellRunner,
)(implicit ec: ExecutionContext): Future[Option[BspSession]] = {
val projectRoot = buildTool.map(_.projectRoot).getOrElse(workspace)
def connect(
projectRoot: AbsolutePath,
bspTraceRoot: AbsolutePath,
addLivenessMonitor: Boolean,
): Future[Option[BuildServerConnection]] = {
def bspStatusOpt = Option.when(addLivenessMonitor)(bspStatus)
scribe.info("Attempting to connect to the build server...")
resolve() match {
resolve(buildTool) match {
case ResolvedNone =>
scribe.info("No build server found")
Future.successful(None)
Expand Down Expand Up @@ -131,6 +134,7 @@ class BspConnector(
.map(Some(_))
case ResolvedBspOne(details) =>
tables.buildServers.chooseServer(details.getName())
optSetBuildTool(details.getName())
bspServers
.newServer(projectRoot, bspTraceRoot, details, bspStatusOpt)
.map(Some(_))
Expand Down Expand Up @@ -165,6 +169,7 @@ class BspConnector(
)
)
_ = tables.buildServers.chooseServer(item.getName())
_ = optSetBuildTool(item.getName())
conn <- bspServers.newServer(
projectRoot,
bspTraceRoot,
Expand All @@ -180,7 +185,10 @@ class BspConnector(
possibleBuildServerConn match {
case None => Future.successful(None)
case Some(buildServerConn)
if buildServerConn.isBloop && buildTools.isSbt =>
if buildServerConn.isBloop && buildTool.exists {
case _: SbtBuildTool => true
case _ => false
} =>
// NOTE: (ckipp01) we special case this here since sbt bsp server
// doesn't yet support metabuilds. So in the future when that
// changes, re-work this and move the creation of this out above
Expand All @@ -196,6 +204,23 @@ class BspConnector(
}
}

/**
* Looks for a build tool matching the chosen build server, and sets it as the chosen build server.
* Only for `bloop` there will be no matching build tool and the previously chosen one remains.
*/
private def optSetBuildTool(buildServerName: String): Unit =
buildTools.loadSupported
.find {
case _: ScalaCliBuildTool if ScalaCli.names(buildServerName) => true
case buildTool: BuildServerProvider
if buildTool.buildServerName.contains(buildServerName) =>
true
case buildTool => buildTool.executableName == buildServerName
}
.foreach(buildTool =>
tables.buildTool.chooseBuildTool(buildTool.executableName)
)

private def sbtMetaWorkspaces(root: AbsolutePath): List[AbsolutePath] = {
def recursive(
p: AbsolutePath,
Expand Down Expand Up @@ -277,7 +302,9 @@ class BspConnector(
BspConnectionDetails,
]] = {
if (
bloopPresent || buildTools.loadSupported().exists(_.isBloopDefaultBsp)
bloopPresent || buildTools
.loadSupported()
.exists(_.isBloopInstallProvider)
)
new BspConnectionDetails(
BloopServers.name,
Expand Down
11 changes: 6 additions & 5 deletions metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ final class BspServers(
config: MetalsServerConfig,
userConfig: () => UserConfiguration,
)(implicit ec: ExecutionContextExecutorService) {
private def customProjectRoot =
userConfig().getCustomProjectRoot(mainWorkspace)

def resolve(): BspResolvedResult = {
findAvailableServers() match {
Expand Down Expand Up @@ -153,7 +155,7 @@ final class BspServers(
* may be a server in the current workspace
*/
def findAvailableServers(): List[BspConnectionDetails] = {
val jsonFiles = findJsonFiles(mainWorkspace)
val jsonFiles = findJsonFiles()
val gson = new Gson()
for {
candidate <- jsonFiles
Expand All @@ -172,17 +174,16 @@ final class BspServers(
}
}

private def findJsonFiles(
projectDirectory: AbsolutePath
): List[AbsolutePath] = {
private def findJsonFiles(): List[AbsolutePath] = {
val buf = List.newBuilder[AbsolutePath]
def visit(dir: AbsolutePath): Unit =
dir.list.foreach { p =>
if (p.extension == "json") {
buf += p
}
}
visit(projectDirectory.resolve(".bsp"))
visit(mainWorkspace.resolve(".bsp"))
customProjectRoot.map(_.resolve(".bsp")).foreach(visit)
bspGlobalInstallDirectories.foreach(visit)
buf.result()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ final class BloopInstall(
override def toString: String = s"BloopInstall($workspace)"

def runUnconditionally(
buildTool: BuildTool,
buildTool: BloopInstallProvider,
isImportInProcess: AtomicBoolean,
): Future[WorkspaceLoadedStatus] = {
if (isImportInProcess.compareAndSet(false, true)) {
Expand Down Expand Up @@ -121,7 +121,7 @@ final class BloopInstall(
// notifications. This method is synchronized to prevent asking the user
// twice whether to import the build.
def runIfApproved(
buildTool: BuildTool,
buildTool: BloopInstallProvider,
digest: String,
isImportInProcess: AtomicBoolean,
): Future[WorkspaceLoadedStatus] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import scala.meta.io.AbsolutePath
/**
* Helper trait for build tools that have a Bloop plugin
*/
trait BloopInstallProvider { this: BuildTool =>
trait BloopInstallProvider extends BuildTool {

/**
* Method used to generate the necesary .bloop files for the
* Method used to generate the necessary .bloop files for the
* build tool.
*/
def bloopInstall(
Expand All @@ -22,4 +22,6 @@ trait BloopInstallProvider { this: BuildTool =>
* Args necessary for build tool to generate the .bloop files.
*/
def bloopInstallArgs(workspace: AbsolutePath): List[String]

override val isBloopInstallProvider = true
}
25 changes: 25 additions & 0 deletions metals/src/main/scala/scala/meta/internal/builds/BspOnly.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package scala.meta.internal.builds

import java.security.MessageDigest

import scala.meta.internal.builds.Digest
import scala.meta.internal.mtags.MD5
import scala.meta.io.AbsolutePath

/**
* Build tool for custom bsp detected in `.bsp/<name>.json` or `bspGlobalDirectories`
*/
case class BspOnly(
override val executableName: String,
override val projectRoot: AbsolutePath,
pathToBspConfig: AbsolutePath,
) extends BuildTool {
override def digest(workspace: AbsolutePath): Option[String] = {
val digest = MessageDigest.getInstance("MD5")
val isSuccess =
Digest.digestJson(pathToBspConfig, digest)
if (isSuccess) Some(MD5.bytesToHex(digest.digest()))
else None
}
override val forcesBuildServer = true
}
39 changes: 16 additions & 23 deletions metals/src/main/scala/scala/meta/internal/builds/BuildTool.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,12 @@ import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption

import scala.concurrent.Future

import scala.meta.io.AbsolutePath

trait BuildTool {

/**
* Export the build to Bloop
*
* This operation should be roughly equivalent to running `sbt bloopInstall`
* and should work for both updating an existing Bloop build or creating a new
* Bloop build.
*/
def bloopInstall(
workspace: AbsolutePath,
systemProcess: List[String] => Future[WorkspaceLoadedStatus],
): Future[WorkspaceLoadedStatus]

def digest(workspace: AbsolutePath): Option[String]

def version: String

def minimumVersion: String

def recommendedVersion: String

protected lazy val tempDir: Path = {
val dir = Files.createTempDirectory("metals")
dir.toFile.deleteOnExit()
Expand All @@ -40,15 +20,16 @@ trait BuildTool {

def executableName: String

def isBloopDefaultBsp = true

def projectRoot: AbsolutePath

val forcesBuildServer = false

val isBloopInstallProvider = false

}

object BuildTool {

case class Found(buildTool: BuildTool, digest: String)
def copyFromResource(
tempDir: Path,
filePath: String,
Expand All @@ -62,4 +43,16 @@ object BuildTool {
outFile
}

trait Verified
case class IncompatibleVersion(buildTool: VersionRecommendation)
extends Verified {
def message: String = s"Unsupported $buildTool version ${buildTool.version}"
}
case class NoChecksum(buildTool: BuildTool, root: AbsolutePath)
extends Verified {
def message: String =
s"Could not calculate checksum for ${buildTool.executableName} in $root"
}
case class Found(buildTool: BuildTool, digest: String) extends Verified

}
45 changes: 36 additions & 9 deletions metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import scala.meta.internal.io.PathIO
import scala.meta.internal.metals.BloopServers
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.UserConfiguration
import scala.meta.internal.metals.scalacli.ScalaCli
import scala.meta.io.AbsolutePath

import ujson.ParsingFailedException
Expand Down Expand Up @@ -54,6 +55,7 @@ final class BuildTools(
def isBloop: Boolean = bloopProject.isDefined
def isBsp: Boolean = {
hasJsonFile(workspace.resolve(".bsp")) ||
customProjectRoot.exists(root => hasJsonFile(root.resolve(".bsp"))) ||
bspGlobalDirectories.exists(hasJsonFile)
}
private def hasJsonFile(dir: AbsolutePath): Boolean = {
Expand Down Expand Up @@ -121,16 +123,34 @@ final class BuildTools(
)
def isBazel: Boolean = bazelProject.isDefined

private def customProjectRoot =
userConfig().customProjectRoot
.map(relativePath => workspace.resolve(relativePath.trim()))
.filter { projectRoot =>
val exists = projectRoot.exists
if (!exists) {
scribe.error(s"custom project root $projectRoot does not exist")
private def customBsps: List[BspOnly] = {
val bspFolders =
(workspace :: customProjectRoot.toList).distinct
.map(_.resolve(".bsp")) ++ bspGlobalDirectories
val root = customProjectRoot.getOrElse(workspace)
for {
bspFolder <- bspFolders
if (bspFolder.exists && bspFolder.isDirectory)
buildTool <- bspFolder.toFile
.listFiles()
.collect {
case file
if file.isFile() && file.getName().endsWith(".json") &&
!knownBsps(file.getName().stripSuffix(".json")) =>
BspOnly(
file.getName().stripSuffix(".json"),
root,
AbsolutePath(file.toPath()),
)
}
exists
}
.toList
} yield buildTool
}

private def knownBsps =
Set(SbtBuildTool.name, MillBuildTool.name) ++ ScalaCli.names

private def customProjectRoot = userConfig().getCustomProjectRoot(workspace)

private def searchForBuildTool(
isProjectRoot: AbsolutePath => Boolean
Expand Down Expand Up @@ -187,6 +207,7 @@ final class BuildTools(
mavenProject.foreach(buf += MavenBuildTool(userConfig, _))
millProject.foreach(buf += MillBuildTool(userConfig, _))
scalaCliProject.foreach(buf += ScalaCliBuildTool(workspace, _, userConfig))
buf.addAll(customBsps)

buf.result()
}
Expand All @@ -208,6 +229,11 @@ final class BuildTools(
Some(MavenBuildTool.name)
else if (isMill && MillBuildTool.isMillRelatedPath(path))
Some(MillBuildTool.name)
else if (
path.isFile && path.filename.endsWith(".json") &&
path.parent.filename == ".bsp"
)
Some(path.filename.stripSuffix(".json"))
else None
}

Expand All @@ -221,6 +247,7 @@ final class BuildTools(
val before = lastDetectedBuildTools.getAndUpdate(_ + buildTool)
!before.contains(buildTool)
}

}

object BuildTools {
Expand Down
Loading

0 comments on commit 73706fa

Please sign in to comment.