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

feature: Support Bazel as a build tool #3233

Merged
merged 32 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3519a46
Support Bazel as a build tool - wip
Oct 27, 2021
98f5624
Merge branch 'main' into bazel-bsp
tanishiking Jun 16, 2023
c73f1fa
Auto-install bazel-bsp
tanishiking Jun 19, 2023
84f4baf
Refresh workspace when build changed
tanishiking Jun 19, 2023
1c354d4
Merge remote-tracking branch 'origin/main' into bazel-bsp
jkciesluk Jun 27, 2023
dc18814
fix: Fix PathTrie with empty paths
jkciesluk Jul 18, 2023
1d58063
feat: Detect changes in Bazel-related files
jkciesluk Jul 18, 2023
bb878d8
improvement: Persist checksum on connecting to new build server
jkciesluk Jul 18, 2023
9d9e769
improvement: Add basic BazelBsp test
jkciesluk Jul 18, 2023
716ae39
improvement: Strip ANSI coloring from BazelBsp logs
jkciesluk Jul 18, 2023
855d9d6
Merge remote-tracking branch 'origin/main' into bazel-bsp
jkciesluk Jul 18, 2023
8e42fff
improvement: Add Bazel slow tests to CI
jkciesluk Jul 19, 2023
f8aa610
improvement: Add more test for BazelBsp
jkciesluk Jul 19, 2023
f6287e5
improvement: Apply review suggestions
jkciesluk Jul 27, 2023
2ed4e65
Merge remote-tracking branch 'origin/main' into bazel-bsp
jkciesluk Jul 27, 2023
74cdd81
fix: Fix memory leak in supportedBuildTool
jkciesluk Jul 27, 2023
7d7abff
improv: Apply review suggestions
jkciesluk Jul 27, 2023
39f4fff
Merge branch 'main' into HEAD
jkciesluk Sep 22, 2023
06c5552
bump bazel-bsp to 3.1.0
jkciesluk Sep 22, 2023
6026253
Merge branch 'main' into bazel-merge
jkciesluk Sep 22, 2023
644bc73
improvement: Bump bazelbsp to 3.1.0
jkciesluk Sep 29, 2023
d5157e2
Merge branch 'main' into bazel-merge
jkciesluk Nov 6, 2023
c81c453
Merge branch 'main' into bazel-merge
jkciesluk Dec 18, 2023
96071fa
Merge branch 'main' into bazel-merge
jkciesluk Dec 29, 2023
cb36ea8
Merge branch 'main' into bazel-merge
jkciesluk Jan 2, 2024
82af4be
bugfix: Ignore non scala or java build targets
jkciesluk Jan 5, 2024
a56ae7a
Merge branch 'main' into bazel-merge
jkciesluk Jan 10, 2024
f8b9ea5
improvement: Look for projectview in ijwb directory
jkciesluk Jan 10, 2024
c360dcf
improvement: Add diagnostics check to bazel tests
jkciesluk Jan 12, 2024
a2f451c
improvement: Warning message for no semanticdb in bazel projects
jkciesluk Jan 15, 2024
c631604
improvement: Download bazelbsp dependency
jkciesluk Jan 15, 2024
504f703
Merge branch 'main' into bazel-bsp
tgodzik Jan 15, 2024
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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
gradle,
scalacli,
mill,
bazel,
feature,
cross,
scalafmt,
Expand Down Expand Up @@ -87,6 +88,10 @@ jobs:
command: bin/test.sh 'slow/testOnly -- tests.mill.*'
name: Mill integration
os: ubuntu-latest
- type: bazel
command: bin/test.sh 'slow/testOnly -- tests.bazel.*'
name: Bazel integration
os: ubuntu-latest
- type: scalacli
command: bin/test.sh 'slow/testOnly -- tests.scalacli.*'
name: Scala CLI integration
Expand Down
39 changes: 37 additions & 2 deletions metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import scala.meta.internal.bsp.BspConfigGenerationStatus._
import scala.meta.internal.builds.BazelBuildTool
import scala.meta.internal.builds.BuildServerProvider
import scala.meta.internal.builds.BuildTools
import scala.meta.internal.builds.SbtBuildTool
Expand Down Expand Up @@ -45,13 +46,16 @@ class BspConnector(
*/
def resolve(): BspResolvedResult = {
resolveExplicit().getOrElse {
lazy val resolved = bspServers.resolve()
jkciesluk marked this conversation as resolved.
Show resolved Hide resolved
if (
buildTools
.loadSupported()
.exists(_.isBloopDefaultBsp) || buildTools.isBloop
)
ResolvedBloop
else bspServers.resolve()
// WORKSPACE file is found && bsp connection file for Bazel is not yet generated
else if (buildTools.isBazel && resolved == ResolvedNone) ResolvedBazel
else resolved
}
}

Expand Down Expand Up @@ -129,6 +133,34 @@ class BspConnector(
statusBar
.trackFuture("Connecting to sbt", connectionF, showTimer = true)
.map(Some(_))
// NOTE: (jkciesluk) This is a special case where .bazelbsp config is not yet generated.
// It can happen if `autoConnectToBuildServer` is called without check if build tool is auto-connectable
// eg. in ResetWorkspace or RestartBuildServer commands
case ResolvedBazel =>
jkciesluk marked this conversation as resolved.
Show resolved Hide resolved
BazelBuildTool
.writeBazelConfig(
shellRunner,
workspace,
userConfig().javaHome,
)
.flatMap { _ =>
bspServers
.findAvailableServers()
.collectFirst {
case details if details.getName == BazelBuildTool.bspName =>
tables.buildServers.chooseServer(BazelBuildTool.bspName)
bspServers
.newServer(
projectRoot,
bspTraceRoot,
details,
bspStatusOpt,
)
.map(Some(_))
}
.getOrElse(Future.successful(None))

}
case ResolvedBspOne(details) =>
tables.buildServers.chooseServer(details.getName())
bspServers
Expand Down Expand Up @@ -277,7 +309,10 @@ class BspConnector(
BspConnectionDetails,
]] = {
if (
bloopPresent || buildTools.loadSupported().exists(_.isBloopDefaultBsp)
bloopPresent ||
buildTools
.loadSupported()
.exists(_.isBloopDefaultBsp)
)
new BspConnectionDetails(
BloopServers.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import ch.epfl.scala.bsp4j.BspConnectionDetails
sealed trait BspResolvedResult extends Product with Serializable
case object ResolvedNone extends BspResolvedResult
case object ResolvedBloop extends BspResolvedResult

/** WORKSPACE file is found, but bsp connection not found. */
case object ResolvedBazel extends BspResolvedResult
case class ResolvedBspOne(details: BspConnectionDetails)
extends BspResolvedResult
case class ResolvedMultiple(md5: String, details: List[BspConnectionDetails])
Expand Down
171 changes: 171 additions & 0 deletions metals/src/main/scala/scala/meta/internal/builds/BazelBuildTool.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package scala.meta.internal.builds

import java.util.concurrent.TimeUnit

import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import scala.meta.internal.metals.JavaBinary
import scala.meta.internal.metals.Messages
import scala.meta.internal.metals.Messages.ImportBuild
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.Tables
import scala.meta.internal.metals.UserConfiguration
import scala.meta.internal.process.ExitCodes
import scala.meta.io.AbsolutePath

import coursierapi.Dependency
import coursierapi.Fetch
import org.eclipse.lsp4j.services.LanguageClient

case class BazelBuildTool(
userConfig: () => UserConfiguration,
projectRoot: AbsolutePath,
) extends BuildTool
with BuildServerProvider {

override def digest(workspace: AbsolutePath): Option[String] = {
BazelDigest.current(projectRoot)
}

def createBspFileArgs(workspace: AbsolutePath): Option[List[String]] =
Option.when(workspaceSupportsBsp)(composeArgs())

def workspaceSupportsBsp: Boolean = {
projectRoot.list.exists {
case file if file.filename == "WORKSPACE" => true
case _ => false
}
}

private def composeArgs(): List[String] = {
val classpathSeparator = java.io.File.pathSeparator
val classpath = Fetch
tgodzik marked this conversation as resolved.
Show resolved Hide resolved
.create()
.withDependencies(BazelBuildTool.dependency)
.fetch()
.asScala
.mkString(classpathSeparator)
List(
JavaBinary(userConfig().javaHome),
"-classpath",
classpath,
BazelBuildTool.mainClass,
) ++ BazelBuildTool.projectViewArgs(projectRoot)
}

override def minimumVersion: String = "3.0.0"

override def recommendedVersion: String = version

override def version: String = BazelBuildTool.version

override def toString: String = "Bazel"

override def executableName = BazelBuildTool.name

override def isBloopDefaultBsp = false

override val buildServerName: Option[String] = Some(BazelBuildTool.bspName)

}

object BazelBuildTool {
val name: String = "bazel"
val bspName: String = "bazelbsp"
val version: String = "3.1.0"

val mainClass = "org.jetbrains.bsp.bazel.install.Install"

private val dependency = Dependency.of(
"org.jetbrains.bsp",
"bazel-bsp",
version,
)

def projectViewArgs(projectRoot: AbsolutePath): List[String] = {
val hasProjectView =
projectRoot.list.exists(_.filename.endsWith(".bazelproject"))
if (hasProjectView) Nil
else // we default to all targets view
List(
"-t",
"//...",
)
}

def writeBazelConfig(
shellRunner: ShellRunner,
projectDirectory: AbsolutePath,
javaHome: Option[String],
)(implicit
ec: ExecutionContext
): Future[WorkspaceLoadedStatus] = {
def run() =
shellRunner.runJava(
dependency,
mainClass,
projectDirectory,
projectViewArgs(projectDirectory),
javaHome,
false,
)
run()
.flatMap { code =>
scribe.info(s"Generate Bazel-BSP process returned code $code")
if (code != 0) run()
else Future.successful(0)
}
.map {
case ExitCodes.Success => WorkspaceLoadedStatus.Installed
case ExitCodes.Cancel => WorkspaceLoadedStatus.Cancelled
case result =>
scribe.error("Failed to write Bazel-BSP config to .bsp")
WorkspaceLoadedStatus.Failed(result)
}
}

def maybeWriteBazelConfig(
shellRunner: ShellRunner,
projectDirectory: AbsolutePath,
languageClient: LanguageClient,
tables: Tables,
javaHome: Option[String],
forceImport: Boolean = false,
)(implicit
ec: ExecutionContext
): Future[WorkspaceLoadedStatus] = {
val notification = tables.dismissedNotifications.ImportChanges
if (forceImport) {
writeBazelConfig(shellRunner, projectDirectory, javaHome)
} else if (!notification.isDismissed) {
languageClient
.showMessageRequest(ImportBuild.params("Bazel"))
.asScala
.flatMap {
case item if item == Messages.dontShowAgain =>
notification.dismissForever()
Future.successful(WorkspaceLoadedStatus.Rejected)
case item if item == ImportBuild.yes =>
writeBazelConfig(shellRunner, projectDirectory, javaHome)
case _ =>
notification.dismiss(2, TimeUnit.MINUTES)
Future.successful(WorkspaceLoadedStatus.Rejected)

}
} else {
scribe.info(
s"skipping build import with status ${WorkspaceLoadedStatus.Dismissed}"
)
Future.successful(WorkspaceLoadedStatus.Dismissed)
}
}

def isBazelRelatedPath(
workspace: AbsolutePath,
path: AbsolutePath,
): Boolean =
path.toNIO.startsWith(workspace.toNIO) &&
path.isBazelRelatedPath &&
!path.isInBazelBspDirectory(workspace)
}
23 changes: 23 additions & 0 deletions metals/src/main/scala/scala/meta/internal/builds/BazelDigest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package scala.meta.internal.builds

import java.security.MessageDigest

import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.io.AbsolutePath

object BazelDigest extends Digestable {
override protected def digestWorkspace(
workspace: AbsolutePath,
digest: MessageDigest,
): Boolean = {
workspace.listRecursive.forall {
case file
if file.isBazelRelatedPath && !file.isInBazelBspDirectory(
jkciesluk marked this conversation as resolved.
Show resolved Hide resolved
workspace
) =>
Digest.digestFile(file, digest)
case _ =>
true
}
}
}
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 @@ -67,7 +67,7 @@ final class BloopInstall(
}

private def runArgumentsUnconditionally(
buildTool: BuildTool,
buildTool: BloopInstallProvider,
args: List[String],
javaHome: Option[String],
): Future[WorkspaceLoadedStatus] = {
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 Expand Up @@ -155,7 +155,7 @@ final class BloopInstall(

private def persistChecksumStatus(
status: Status,
buildTool: BuildTool,
buildTool: BloopInstallProvider,
): Unit = {
buildTool.digest(workspace).foreach { checksum =>
tables.digests.setStatus(checksum, status)
Expand All @@ -164,7 +164,7 @@ final class BloopInstall(

private def requestImport(
buildTools: BuildTools,
buildTool: BuildTool,
buildTool: BloopInstallProvider,
languageClient: MetalsLanguageClient,
digest: String,
)(implicit ec: ExecutionContext): Future[Confirmation] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ 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
* build tool.
* 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,
Expand Down
14 changes: 0 additions & 14 deletions metals/src/main/scala/scala/meta/internal/builds/BuildTool.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,10 @@ 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
Expand Down
Loading