Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: add custom bsp as possible build tool #5791

Merged
merged 5 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 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,19 @@ class BspConnector(
}
}

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment when this would be different?

}
.foreach(buildTool =>
tables.buildTool.chooseBuildTool(buildTool.executableName)
)

private def sbtMetaWorkspaces(root: AbsolutePath): List[AbsolutePath] = {
def recursive(
p: AbsolutePath,
Expand Down Expand Up @@ -277,7 +298,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
Loading