Mezel is a Scala BSP implementation for Bazel. Mezel acts as a communication layer between Bazel and Scala BSP consumers such as Metals.
Mezel is work-in-progress. I daily drive it with Metals using NeoVim, but any Metals-enabled editor works. A non-exhaustive list of features that work:
- Semanticdb consumption from Bazel (code navigation).
- External dependencies (from maven for instance, but also locally built and imported jars).
- Presentation compiler support with plugins (type hints and such).
- Caching of semanticdb files (bazel clears the output directory on every build).
- Streamed diagnostics reporting for errors and warnings (diagnostics will appear as they are generated instead of waiting for the build to finish).
- Build change propagation (mezel always builds all targets so you get full diagnostics).
- Logging of all build events with performance traces.
- Build isolation, uses a custom
--output_base
to avoid destroying your build cache. - Custom build/query flags.
- Semi-automatic configuration generation.
- Minimal build target definitons using ijars over jars.
- Automatic reload on build change.
- IntelliJ support (lightly tested).
- BSP Task cancellation.
- Cancellation of stale builds, saving a file mid-build restarts the build.
Things that I will be working that Mezel doesn't support yet:
- Caching of output jars/ijars from local targets (improves DX on big refactorings since transitive jars will persist through bazel output extermination)
- Support for an add-it-all-to-classpath mode such that you won't need to add a dependency to your BUILD.bazel for it to be available in your IDE?
- Scala3 support *
Make sure that you are using version f9381414068466b9c74ff7681d204e1eb19c7f80
or newer of the bazel scala rules.
Failing to do so will cause issues with generation of diagnostics.
rules_scala_version = "f9381414068466b9c74ff7681d204e1eb19c7f80" # update this as needed
http_archive(
name = "io_bazel_rules_scala",
sha256 = "63be9ff9e5788621cbf087f659f4c4e8d2a328e6d77353e455a09d923b653b56",
strip_prefix = "rules_scala-%s" % rules_scala_version,
type = "zip",
url = "https://github.com/bazelbuild/rules_scala/archive/%s.zip" % rules_scala_version,
)
Your Scala toolchain needs to be configured to emit semanticdb files and diagnostics. I recommend having a separate toolchain for your LSP server, so that you can have different settings for it.
load("@io_bazel_rules_scala//scala:scala_toolchain.bzl", "scala_toolchain")
scala_toolchain(
name = "my_lsp_toolchain",
enable_diagnostics_report = True,
enable_semanticdb = True
# other settings...
# for the best editor experince I recommend not having fatal warnings and
# not having 'strict_deps_mode' and 'unused_dependency_checker_mode' set to error
)
toolchain(
name = "lsp_toolchain",
toolchain = "my_lsp_toolchain",
toolchain_type = "@io_bazel_rules_scala//scala:toolchain_type"
)
Furthermore I suggest omitting the fatal warnings flag when you are editing your code, since downstream targets won't build if their dependencies don't compile.
# flags.bzl
_flags = [
"-encoding",
"UTF-8",
"-Wconf:cat=other-implicit-type:s",
"-Wdead-code",
"-Wextra-implicit",
"-Wunused:explicits"
# and so on...
]
flags = _flags + select({
"//my_config:no_fatal_warnings": [],
"//conditions:default": ["-Xfatal-warnings"]
})
# myconfig/BUILD.bazel
config_setting(
name = "no_fatal_warnings",
define_values = {
"no_fatal_warnings": "true"
}
)
Mezel needs an aspect to work, so add the following to your WORKSPACE
file to get it into scope:
mezel_version = "0c3e96d02c82b875fd3f755f55329ade01cd8320"
http_archive(
name = "mezel",
sha256 = "a73da8680a824833b3c4711a9558e82b9a02fdb39a50bb39b269ba87fbd20743",
strip_prefix = "mezel-%s" % mezel_version,
type = "zip",
url = "https://github.com/valdemargr/mezel/archive/%s.zip" % mezel_version,
)
# loads the bsp binary
load("@mezel//rules:load_mezel.bzl", "load_mezel")
load_mezel()
The Mezel BSP program will expect the Mezel aspect to be at the label @mezel//aspects:aspect.bzl
, so make sure to name the http_achive
mezel
like in the example.
Add the following to your BUILD.bazel
(any location is fine) to create a bazel target for the Mezel binary:
#./BUILD.bazel
load("@mezel//rules:make_mezel_launcher.bzl", "make_mezel_launcher")
make_mezel_launcher(name = "mk_mezel")
Now we have a Mezel binary to run.
Create a config for Metals at .bsp/mezel.json
:
{
"argv":["bash", "-c", "bazel run //:mk_mezel -- /tmp/startmezel && exec /tmp/startmezel"],
"bspVersion":"2.0.0",
"languages":["scala"],
"name":"Mezel",
"version":"1.0.0"
}
And that's it. Start your editor and select Mezel
as your BSP server.
Running Mezel though Bazel has been causing issues with broken stdin/stdout streams (on a nix-emulated FHS system). To alleviate this issue, the Mezel binary is prepared by a Bazel rule and then run as a separate process.
External dependencies should work but have only been tested with rules_jvm_external.
When you import external dependencies you must ensure that you fetch their sources, such that there are sources to navigate to on "goto definition" type actions.
For rules_jvm_external you can flag this in your maven_install
as seen here.
I suggest checking your bsp file into VCS .bsp/mezel.json
so it works for other developers without any configuration.
A configuration example that uses a custom toolchain and set of configuration options for local development (semanticdb + diagnostics + no fatal warnings):
#./BUILD.bazel
load("@mezel//rules:make_mezel_launcher.bzl", "make_mezel_launcher")
make_mezel_launcher(
name = "mezel",
build_args = [
"--extra_toolchains=//toolchain:lsp",
"--define=no_fatal_warnings=true"
],
aquery_args = [
"--extra_toolchains=//toolchain:lsp",
"--define=no_fatal_warnings=true"
]
)
A bug with bazel build/aquery causes bazel to consider the convinience symlinks it creates to be considered build targets. To get around this, tell bazel to ignore the symlink when running bazel operations:
echo "bazel-$(basename $PWD)" >> .bazelignore
For larger projects, some operations can take longer than Metals (by default) will wait for a response before timing out.
Mezel is not supposed to deadlock or become stuck, if this ever occurs it is a bug.
I suggest turning off the reconnection feature of Metals and enabling debug logging:
-Dmetals.verbose=true
-Dmetals.askToReconnect=false
-Dmetals.loglevel=debug
-Dmetals.build-server-ping-interval=10h