Skip to main content

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:

  1. pedantic means (almost) all warnings are enabled and they are treated as errors.

  2. 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.

  3. 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.