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:
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:
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:
# //: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),
},
)
# 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!