Practical Bazel: Custom Bazel Make Variables
Practical Bazel bazel c c++
Published: 2023-03-03
Practical Bazel: Custom Bazel Make Variables

Many Bazel attributes support the use of predefined variables and functions such as @D for output directory or $(location //foo:bar) to get the path to a label. But what if you want to apply some sort of tranformation to these variables, or define your own custom make variables? This blog post explains how.

I was working on integrating some third-paty C software into my Bazel workspace. The project was relatively simple, so I decide that rather than call into the third-party project’s build system usuing rules_foreign_cc, I would instead write native cc_library() build rules.

I started by writing a basic cc_library() rule:

1
2
3
4
5
6
cc_library(
    name = "libexample",
    srcs = ["src/libexample.c"],
    hdrs = ["include/libexample.h"],
    strip_include_prefix = "include",
)

And received errors like the following:

external/libexample/src/libexample.c:11:10: fatal error: 'libexample.h' file not found
#include "libexample.h"
         ^~~~~~~~~~~~
1 error generated.

This error occurs because libexample.h lives in the include/ directory, but the compiler flags set by cc_library() requires all headers contained within the hdrs attribute to be included with angle brackets, instead of quotes. In other words, if the code had performed #include <libexample.h> instead of #include "libexample.h", the library would have compiled fine.

To fix this, I decided to add the appropriate compiler flags using copts. I wanted something like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
cc_library(
    name = "libexample",
    srcs = ["src/libexample.c"],
    hdrs = ["include/libexample.h"],
    strip_include_prefix = "include",
    copts = [
        # Add the directory which contains include/libexample.h to the
        # list of include directories
        "-I$(dirname $(location include/libexample.h))"
    ],
)

Unfortunately, the above code, doesn’t work – the $(dirname) function doesn’t exist. I did some research and realized that I could solve this problem by writing my own variable provider. By writing a rule which returns a platform_common.TemplateVariableInfo, and referencing it as a toolchain in cc_library(), you can define variables that can then be used in attributes like copts.

After some time I came up with the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# //:dirname_providing_rule.bzl: A rule that determines the dirname
# of a variable and provides it as a Bazel "make" variable
load("@bazel_skylib//lib:paths.bzl", "paths")

def _impl(ctx):
    return [
        platform_common.TemplateVariableInfo({
            ctx.attr.varname: paths.dirname(ctx.expand_location(ctx.attr.value, ctx.attr.data)),
        }),
    ]

dirname_providing_rule = rule(
    implementation = _impl,
    attrs = {
        "varname": attr.string(mandatory = True),
        "value": attr.string(mandatory = True),
        "data": attr.label_list(allow_files = True),
    },
)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# BUILD.bzl
load("//:dirname_providing_rule.bzl", "dirname_providing_rule")

dirname_providing_rule(
    name = "set_libexample_h_dirname",
    data = ["include/libexample.h"],
    value = "$(location include/libexample.h)",
    varname = "LIBEXAMPLE_H_DIRNAME",
)

cc_library(
    name = "libexample",
    srcs = ["src/libexample.c"],
    hdrs = ["include/libexample.h"],
    strip_include_prefix = "include",
    copts = [
        # Add the directory which contains include/libexample.h to the
        # list of include directories
        "-I$(LIBEXAMPLE_H_DIRNAME)"
    ],
    toolchains = [
        ":set_libexample_h_dirname",
    ]
)

Problem solved!