Skip to content
This repository has been archived by the owner on Aug 5, 2024. It is now read-only.

[feature] add the ability to use bloop as the bsp server #246

Merged
merged 9 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,44 @@ bazel run --stamp --define "maven_repo=file://$HOME/.m2/repository" //server/src
cs launch -r m2Local org.jetbrains.bsp:bazel-bsp:<your version> -M org.jetbrains.bsp.bazel.install.Install
```

### Using Bloop

By default Bazel BSP runs as a BSP server and invokes Bazel to compile, test and run targets.
This provides the most accurate build results at the expense of
compile/test/run latency. Bazel BSP can optionally be configured to use [Bloop](https://scalacenter.github.io/bloop/)
as the BSP server instead. Bloop provides a much lower latency with the trade-off that the Bloop model
may not perfectly represent the Bazel configuration.

#### Installing with Bloop

The instructions above will work in Bloop mode as well, simply pass ``--use_bloop`` as an installation option.
However, when using Bloop mode Bazel BSP can also install itself outside the source root directory. This can
be useful in large shared repositories where it's undesirable to keep the Bazel BSP projects inside the
repository itself.

In the examples below, we'll use ``~/src/my-repo`` as the "repository root" and ``~/bazel-bsp-projects`` as the
"Bazel BSP project root", however both can be any directory.

To install Bazel BSP outside the repository root:

1) Change directories into the repository root: ``cd ~/src/my-repo``
2) Invoke the Bazel BSP installer as described above (via Coursier or run the installer JAR directly), passing in:
1) ``--use_bloop``
2) ``-d ~/bazel-bsp-projects/my-repo-project``

For example, using Coursier:

```shell
cd ~/src/my-repository
cs launch org.jetbrains.bsp:bazel-bsp:2.1.0 -M org.jetbrains.bsp.bazel.install.Install \
--use_bloop \
-t //my-targets/... \
-d ~/bazel-bsp-projects/my-targets-project
```

This will create a set of BSP and Bloop projects in ``~/bazel-bsp-projects/my-targets-project`` which can then be opened
in IntelliJ or any other IDE that supports BSP.

## Project Views

In order to work on huge monorepos you might want to specify directories and targets to work on. To address this issue,
Expand Down
2 changes: 2 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ maven_install(
"com.fasterxml.jackson.core:jackson-databind:2.13.3",
"com.fasterxml.jackson.module:jackson-module-kotlin:2.13.3",
"io.vavr:vavr-jackson:0.10.3",
"ch.epfl.scala:bloop-config_2.13:1.5.0",
"org.scala-lang:scala-library:2.13.8",
],
fetch_sources = True,
repositories = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package org.jetbrains.bsp.bazel.bazelrunner

import org.apache.logging.log4j.LogManager
import org.jetbrains.bsp.bazel.bazelrunner.outputs.AsyncOutputProcessor
import org.jetbrains.bsp.bazel.bazelrunner.outputs.OutputCollector
import org.jetbrains.bsp.bazel.commons.Format
import org.jetbrains.bsp.bazel.commons.Stopwatch
import org.jetbrains.bsp.bazel.logger.BspClientLogger
Expand All @@ -14,17 +13,13 @@ class BazelProcess internal constructor(
) {

fun waitAndGetResult(): BazelProcessResult {
val stdoutCollector = OutputCollector()
val stderrCollector = OutputCollector()
val outputProcessor = AsyncOutputProcessor()
val outputProcessor = AsyncOutputProcessor(process, logger::message, LOGGER::info)
val stopwatch = Stopwatch.start()
outputProcessor.start(process.inputStream, stdoutCollector, logger::message, LOGGER::info)
outputProcessor.start(process.errorStream, stderrCollector, logger::message, LOGGER::info)
val exitCode = process.waitFor()
outputProcessor.shutdown()

val exitCode = outputProcessor.waitForExit()
val duration = stopwatch.stop()
logCompletion(exitCode, duration)
return BazelProcessResult(stdoutCollector, stderrCollector, exitCode)
return BazelProcessResult(outputProcessor.stdoutCollector, outputProcessor.stderrCollector, exitCode)
}

private fun logCompletion(exitCode: Int, duration: Duration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ class BazelRunner private constructor(
fun commandBuilder(): BazelRunnerCommandBuilder = BazelRunnerCommandBuilder(this)

fun runBazelCommandBes(command: String, flags: List<String>, arguments: List<String>): BazelProcess {
fun besFlags() = listOf(
"--bes_backend=grpc://localhost:${besBackendPort!!}",
"--build_event_publish_all_actions")
fun besFlags() = listOf("--bes_backend=grpc://localhost:${besBackendPort!!}")
abrams27 marked this conversation as resolved.
Show resolved Hide resolved

return runBazelCommand(command, flags = besFlags() + flags, arguments)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,67 @@ import java.io.InputStream
import java.io.InputStreamReader
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicBoolean

class AsyncOutputProcessor {
class AsyncOutputProcessor(
private val process: Process,
vararg loggers: OutputHandler
) {
private val executorService = Executors.newCachedThreadPool()
private val runningProcessors = mutableListOf<Future<*>>()
private val isRunning = AtomicBoolean(true)

fun start(inputStream: InputStream, vararg handlers: OutputHandler) {
val stdoutCollector = OutputCollector()
val stderrCollector = OutputCollector()

init {
start(process.inputStream, stdoutCollector, *loggers)
start(process.errorStream, stderrCollector, *loggers)
}

private fun start(inputStream: InputStream, vararg handlers: OutputHandler) {
val runnable = Runnable {
try {
BufferedReader(InputStreamReader(inputStream)).use { reader ->
var prevLine: String? = null

while (!Thread.currentThread().isInterrupted) {
val line = reader.readLine() ?: return@Runnable
if (line == prevLine) continue
prevLine = line
handlers.forEach { it.onNextLine(line) }
if (isRunning.get()) {
handlers.forEach { it.onNextLine(line) }
} else {
break
}
}
}
} catch (e: IOException) {
if (Thread.currentThread().isInterrupted) return@Runnable
throw RuntimeException(e)
}
}

executorService.submit(runnable).also { runningProcessors.add(it) }
}

fun shutdown() {
runningProcessors.forEach { it.get() }
fun waitForExit(): Int {
val exitCode = process.waitFor()
shutdown()
return exitCode
}

private fun shutdown() {
isRunning.set(false)
runningProcessors.forEach {
try {
it.get(500, TimeUnit.MILLISECONDS)
} catch (_: TimeoutException) {
// it's cool
}
}
executorService.shutdown()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public class Constants {
public static final String DOT_BSP_DIR_NAME = ".bsp";
public static final String BAZELBSP_JSON_FILE_NAME = "bazelbsp.json";
public static final String SERVER_CLASS_NAME = "org.jetbrains.bsp.bazel.server.ServerInitializer";
public static final String BLOOP_BOOTSTRAP_CLASS_NAME =
"org.jetbrains.bsp.bazel.server.bloop.BloopExporterInitializer";
public static final String BLOOP_SETTINGS_JSON_FILE_NAME = "bloop.settings.json";
public static final String CLASSPATH_FLAG = "-classpath";
public static final String BAZELBSP_TRACE_JSON_FILE_NAME = "bazelbsp.trace.json";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.jetbrains.bsp.bazel.install

import ch.epfl.scala.bsp4j.BspConnectionDetails
import io.vavr.control.Try
import org.jetbrains.bsp.bazel.commons.Constants
import java.lang.IllegalArgumentException
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path

class BloopBspConnectionDetailsCreator(bazelBspPath: Path) {
abrams27 marked this conversation as resolved.
Show resolved Hide resolved
private val coursierDestination = bazelBspPath.resolve("cs")

private fun downloadCoursier(): Try<Void> =
if (Files.isRegularFile(coursierDestination) && Files.isExecutable(coursierDestination)) {
Try.success(null)
} else if (Files.exists(coursierDestination)) {
Try.failure(IllegalArgumentException("file already exists: $coursierDestination, but was not executable"))
} else {
val url = System.getenv("FASTPASS_COURSIER_URL") ?: "https://git.io/coursier-cli"
Try.run {
Files.copy(
URL(url).openStream(),
coursierDestination
)
coursierDestination.toFile().setExecutable(true)
}
}

fun create(): Try<BspConnectionDetails> =
downloadCoursier().map {
BspConnectionDetails(
Constants.NAME,
listOfNotNull(
coursierDestination.toString(),
"launch",
"ch.epfl.scala:bloop-launcher-core_2.13:1.5.0",
"--ttl",
"Inf",
"--",
"1.5.0"
),
Constants.VERSION,
Constants.BSP_VERSION,
Constants.SUPPORTED_LANGUAGES
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.jetbrains.bsp.bazel.install

import io.vavr.control.Try
import org.jetbrains.bsp.bazel.commons.Constants
import org.jetbrains.bsp.bazel.install.cli.CliOptions
import org.jetbrains.bsp.bazel.installationcontext.InstallationContext
import java.nio.file.Path
import java.nio.file.Paths

private data class WorkspaceSettings(
val refreshProjectsCommand: List<String>
)

private data class ProjectSettings(
val targets: List<String>?
)

class BloopEnvironmentCreator(
private val cliOptions: CliOptions,
private val installationContext: InstallationContext
) : EnvironmentCreator(cliOptions.workspaceRootDir) {

private val projectRootDir = cliOptions.workspaceRootDir
private val launcherArgumentCreator = LauncherArgumentCreator(installationContext)

override fun create(): Try<Void> = createDotBazelBsp()
.flatMap { bazelBspPath ->
createProjectSettings(bazelBspPath).flatMap {
BloopBspConnectionDetailsCreator(bazelBspPath).create()
}
}
.flatMap { createDotBsp(it) }
.flatMap { createDotBloop() }

private fun createProjectSettings(bazelBspPath: Path): Try<Void> {
val projectSettings = ProjectSettings(
cliOptions.projectViewCliOptions?.targets
)
val settingsFile = bazelBspPath.resolve("project.settings.json")
return writeJsonToFile(settingsFile, projectSettings)
}

private fun createDotBloop(): Try<Void> =
createDir(projectRootDir, ".bloop")
.flatMap(::createBloopConfig)

private fun createBloopConfig(path: Path): Try<Void> =
refreshProjectArgs().flatMap {
val settings = WorkspaceSettings(it)
val bloopSettingsJsonPath = path.resolve(Constants.BLOOP_SETTINGS_JSON_FILE_NAME)
writeJsonToFile(bloopSettingsJsonPath, settings)
}


private fun refreshProjectArgs(): Try<List<String>> {
val pwd = Paths.get("").toAbsolutePath()
steveniemitz marked this conversation as resolved.
Show resolved Hide resolved
return launcherArgumentCreator.classpathArgv().map {
listOfNotNull(
launcherArgumentCreator.javaBinaryArgv(),
Constants.CLASSPATH_FLAG,
it,
launcherArgumentCreator.debuggerConnectionArgv(),
Constants.BLOOP_BOOTSTRAP_CLASS_NAME,
pwd.toString(),
launcherArgumentCreator.projectViewFilePathArgv()
)
}
}
}
14 changes: 14 additions & 0 deletions install/src/main/java/org/jetbrains/bsp/bazel/install/Install.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,27 @@ object Install {

if (cliOptions.helpCliOptions.isHelpOptionUsed) {
cliOptions.helpCliOptions.printHelp()
} else if (cliOptions.bloopCliOptions.useBloop) {
createBloopEnvironmentAndInstallBloopBspServer(cliOptions)
.onSuccess { printInCaseOfSuccess(cliOptions) }
.onFailure(::printFailureReasonAndExit1)
} else {
createEnvironmentAndInstallBazelBspServer(cliOptions)
.onSuccess { printInCaseOfSuccess(cliOptions) }
.onFailure(::printFailureReasonAndExit1)
}
}

private fun createBloopEnvironmentAndInstallBloopBspServer(cliOptions: CliOptions): Try<Void> =
InstallationContextProvider.parseProjectViewOrGenerateAndSaveAndCreateInstallationContext(cliOptions)
.flatMap { createBloopEnvironment(it, cliOptions) }

private fun createBloopEnvironment(installationContext: InstallationContext, cliOptions: CliOptions): Try<Void> {
val environmentCreator = BloopEnvironmentCreator(cliOptions, installationContext)

return environmentCreator.create()
}

private fun createEnvironmentAndInstallBazelBspServer(cliOptions: CliOptions): Try<Void> =
InstallationContextProvider.parseProjectViewOrGenerateAndSaveAndCreateInstallationContext(cliOptions)
.flatMap(::createBspConnectionDetails)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ data class ProjectViewCliOptions internal constructor(
val importDepth: Int?,
)

data class BloopCliOptions internal constructor(
val useBloop: Boolean
)

data class CliOptions internal constructor(
val helpCliOptions: HelpCliOptions,
val workspaceRootDir: Path,
val projectViewFilePath: Path?,
val projectViewCliOptions: ProjectViewCliOptions?,
val bloopCliOptions: BloopCliOptions,
)
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ class CliOptionsProvider(private val args: Array<String>) {
)
.build()
cliParserOptions.addOption(importDepthOption)

val useBloopOption = Option.builder(USE_BLOOP_SHORT_OPT)
.longOpt("use_bloop")
.desc("Use bloop as the BSP server rather than bazel-bsp.")
.build()
steveniemitz marked this conversation as resolved.
Show resolved Hide resolved
cliParserOptions.addOption(useBloopOption)
}

fun getOptions(): Try<CliOptions> {
Expand All @@ -150,6 +156,7 @@ class CliOptionsProvider(private val args: Array<String>) {
workspaceRootDir = workspaceRootDir(cmd),
projectViewFilePath = projectViewFilePath(cmd),
projectViewCliOptions = createProjectViewCliOptions(cmd),
bloopCliOptions = createBloopCliOptions(cmd),
)

private fun workspaceRootDir(cmd: CommandLine): Path =
Expand All @@ -166,6 +173,8 @@ class CliOptionsProvider(private val args: Array<String>) {

private fun isHelpOptionUsed(cmd: CommandLine): Boolean = cmd.hasOption(HELP_SHORT_OPT)

private fun useBloop(cmd: CommandLine): Boolean = cmd.hasOption(USE_BLOOP_SHORT_OPT)

private fun printHelp() {
val formatter = HelpFormatter()
formatter.width = 160
Expand All @@ -182,6 +191,9 @@ class CliOptionsProvider(private val args: Array<String>) {
)
}

private fun createBloopCliOptions(cmd: CommandLine): BloopCliOptions =
BloopCliOptions(useBloop = useBloop(cmd))

private fun createProjectViewCliOptions(cmd: CommandLine): ProjectViewCliOptions? =
if (isAnyGenerationFlagSet(cmd))
ProjectViewCliOptions(
Expand Down Expand Up @@ -247,6 +259,7 @@ class CliOptionsProvider(private val args: Array<String>) {
private const val DIRECTORIES_SHORT_OPT = "r"
private const val DERIVE_TARGETS_FLAG_SHORT_OPT = "v"
private const val IMPORT_DEPTH_SHORT_OPT = "i"
private const val USE_BLOOP_SHORT_OPT = "u"

const val INSTALLER_BINARY_NAME = "bazelbsp-install"
}
Expand Down
Loading