Using Bazel Build Attributes to Enable/Disable Compiler Warnings
I’ve started working on a new pet project recently called Chunked. It is an app which hopefully will help me (and others) streamline my study by integrating flashcards with exercises and specimen exam papers in one place.
All the code base is built with Bazel and the server-side components are written
in Haskell. With Cabal, you can use the option --pedantic
to transform all
warnings into errors to easily make sure you got rid of all of them.
Unfortunately, this is not something that is supported out-of-the-box by rules_haskell but I use build attributes instead to reproduce it in a slightly different way.
The solution presented here applies to any much more general problem of the form
"the behaviour of a Bazel target can be changed by passing an option to the
bazel
command". If that’s you are looking for, then read on.
Here’s the more specific problem. I want three levels of "strictness" with respect to warnings:
-
pedantic
means (almost) all warnings are enabled and they are treated as errors. -
relaxed
means no warnings stop compilation. This is useful during development when you want to ignore some compilation issues and get the code to compile so you can test something more specific. -
The default has some warnings turned into errors, e.g.
-Wmissing-methods
since it is a big red flag.
With the following solution, I can build the API with bazel build //api
most
of the time, with bazel build //api --define strictness=relaxed
when I am
debugging something and with bazel build //api --define strictness=pedantic
when building for production.
Configurable build attributes
The documentation does a better job at explaining what they are than I could but if you want to save one click, this section is a very condensed version of what you need to know for the rest.
Configurable attributes, commonly known as select()
, is a Bazel feature that lets users toggle the values of build rule attributes at the command line.
Bazel provides two functions that are used here: config_setting
and select
.
The first — config_setting
— declares a target which represents a set of
conditions on command-line arguments. There is also contraint_value
which
provides a way to match on the build platform.
The second — select
— is a function whose returned value depends on whether
such targets match.
The solution
Most of the code resides in bazel/haskell.bzl
. This contains one function
meant to be called in WORKSPACE
to set up the attributes and wrappers around
`rules_haskell’s rules which add the appropriate GHC options.
bazel/haskell.bzl
load(
"@rules_haskell//haskell:defs.bzl",
rules_haskell_haskell_binary = "haskell_binary",
rules_haskell_haskell_library = "haskell_library",
rules_haskell_haskell_test = "haskell_test",
)
def transform_haskell_kwargs(kwargs):
relaxed_options = [
"-Wmissing-deriving-strategies",
]
default_options = relaxed_options + [
"-Werror=missing-methods",
]
pedantic_options = default_options + [
"-Werror",
]
kwargs["compiler_flags"] = (kwargs.get("compiler_flags") or []) + select({
"//:pedantic": pedantic_options,
"//:relaxed": relaxed_options,
"//conditions:default": default_options,
})
def haskell_config():
native.config_setting(
name = "pedantic",
define_values = {
"strictness": "pedantic",
},
)
native.config_setting(
name = "relaxed",
define_values = {
"strictness": "relaxed",
},
)
def haskell_library(**kwargs):
transform_haskell_kwargs(kwargs)
rules_haskell_haskell_library(**kwargs)
def haskell_binary(**kwargs):
transform_haskell_kwargs(kwargs)
rules_haskell_haskell_binary(**kwargs)
def haskell_test(**kwargs):
transform_haskell_kwargs(kwargs)
rules_haskell_haskell_test(**kwargs)
Calling haskell_config
in WORKSPACE
declares the targets //:pedantic
and
//:relaxed
which are then used by select
in transform_haskell_kwargs
to
select the appropriate GHC options.
WORKSPACE
# ...
load("//bazel:haskell.bzl", "haskell_config")
haskell_config()
# ...
Finally, we can use these custom rules in place of the ones from rules_haskell
as we would have done before. Notice we load our .bzl
file here.
api/BUILD.bazel
load("//bazel:haskell.bzl", "haskell_library")
haskell_library(
name = "api",
src = glob(["src/**"]),
# ...
)
That’s it. We can know compile the API with
bazel build //:api --define strictness=pedantic
for example.