Rules Tutorial
Starlark is a Python-like
configuration language originally developed for use in Bazel and since adopted
by other tools. Bazel’s BUILD and .bzl files are written in a dialect of
Starlark properly known as the “Build Language”, though it is often simply
referred to as “Starlark”, especially when emphasizing that a feature is
expressed in the Build Language as opposed to being a built-in or “native” part
of Bazel. Bazel augments the core language with numerous build-related functions
such as glob
, genrule
, java_binary
, and so on.
See the Bazel and Starlark documentation for more details.
The empty rule
To create your first rule, create the file foo.bzl
:
def _foo_binary_impl(ctx):
pass
foo_binary = rule(
implementation = _foo_binary_impl,
)
When you call the rule
function, you
must define a callback function. The logic will go there, but you
can leave the function empty for now. The ctx
argument
provides information about the target.
You can load the rule and use it from a BUILD file.
Create a BUILD file in the same directory:
load(":foo.bzl", "foo_binary")
foo_binary(name = "bin")
Now, the target can be built:
$ bazel build bin
INFO: Analyzed target //:bin (2 packages loaded, 17 targets configured).
INFO: Found 1 target...
Target //:bin up-to-date (nothing to build)
Even though the rule does nothing, it already behaves like other rules: it has a
mandatory name, it supports common attributes like visibility
, testonly
, and
tags
.
Evaluation model
Before going further, it’s important to understand how the code is evaluated.
Update foo.bzl
with some print statements:
def _foo_binary_impl(ctx):
print("analyzing", ctx.label)
foo_binary = rule(
implementation = _foo_binary_impl,
)
print("bzl file evaluation")
and BUILD:
load(":foo.bzl", "foo_binary")
print("BUILD file")
foo_binary(name = "bin1")
foo_binary(name = "bin2")
ctx.label
corresponds to the label of the target being analyzed. The ctx
object has
many useful fields and methods; you can find an exhaustive list in the
API reference.
Query the code:
$ bazel query :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
//:bin2
//:bin1
Make a few observations:
- “bzl file evaluation” is printed first. Before evaluating the BUILD file, Bazel evaluates all the files it loads. If multiple BUILD files are loading foo.bzl, you would see only one occurrence of “bzl file evaluation” because Bazel caches the result of the evaluation.
- The callback function
_foo_binary_impl
is not called. Bazel query loads BUILD files, but doesn’t analyze targets.
To analyze the targets, use the cquery
(“configured
query”) or the build
command:
$ bazel build :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin1
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin2
INFO: Analyzed 2 targets (0 packages loaded, 0 targets configured).
INFO: Found 2 targets...
As you can see, _foo_binary_impl
is now called twice - once for each target.
Some readers will notice that “bzl file evaluation” is printed again, although
the evaluation of foo.bzl is cached after the call to bazel query
. Bazel
doesn’t reevaluate the code, it only replays the print events. Regardless of
the cache state, you get the same output.
Creating a file
To make your rule more useful, update it to generate a file. First, declare the file and give it a name. In this example, create a file with the same name as the target:
ctx.actions.declare_file(ctx.label.name)
If you run bazel build :all
now, you will get an error:
The following files have no generating action:
bin2
Whenever you declare a file, you have to tell Bazel how to generate it by
creating an action. Use ctx.actions.write
,
to create a file with the given content.
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello\n",
)
The code is valid, but it won’t do anything:
$ bazel build bin1
Target //:bin1 up-to-date (nothing to build)
The ctx.actions.write
function registered an action, which taught Bazel
how to generate the file. But Bazel won’t create the file until it is
actually requested. So the last thing to do is tell Bazel that the file
is an output of the rule, and not a temporary file used within the rule
implementation.
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello!\n",
)
return [DefaultInfo(files = depset([out]))]
Look at the DefaultInfo
and depset
functions later. For now,
assume that the last line is the way to choose the outputs of a rule.
Now, run Bazel:
$ bazel build bin1
INFO: Found 1 target...
Target //:bin1 up-to-date:
bazel-bin/bin1
$ cat bazel-bin/bin1
Hello!
You have successfully generated a file!
Attributes
To make the rule more useful, add new attributes using
the attr
module and update the rule definition.
Add a string attribute called username
:
foo_binary = rule(
implementation = _foo_binary_impl,
attrs = {
"username": attr.string(),
},
)
Next, set it in the BUILD file:
foo_binary(
name = "bin",
username = "Alice",
)
To access the value in the callback function, use ctx.attr.username
. For
example:
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello {}!\n".format(ctx.attr.username),
)
return [DefaultInfo(files = depset([out]))]
Note that you can make the attribute mandatory or set a default value. Look at
the documentation of attr.string
.
You may also use other types of attributes, such as boolean
or list of integers.
Dependencies
Dependency attributes, such as attr.label
and attr.label_list
,
declare a dependency from the target that owns the attribute to the target whose
label appears in the attribute’s value. This kind of attribute forms the basis
of the target graph.
In the BUILD file, the target label appears as a string object, such as
//pkg:name
. In the implementation function, the target will be accessible as a
Target
object. For example, view the files returned
by the target using Target.files
.
Multiple files
By default, only targets created by rules may appear as dependencies (e.g. a
foo_library()
target). If you want the attribute to accept targets that are
input files (e.g. source files in the repository), you can do it with
allow_files
and specify the list of accepted file extensions (or True
to
allow any file extension):
"srcs": attr.label_list(allow_files = [".java"]),
The list of files can be accessed with ctx.files.<attribute name>
. For
example, the list of files in the srcs
attribute can be accessed through
ctx.files.srcs
Single file
If you need only one file, use allow_single_file
:
"src": attr.label(allow_single_file = [".java"])
This file is then accessible under ctx.file.<attribute name>
:
ctx.file.src
Create a file with a template
You can create a rule that generates a .cc file based on a template. Also, you
can use ctx.actions.write
to output a string constructed in the rule
implementation function, but this has two problems. First, as the template gets
bigger, it becomes more memory efficient to put it in a separate file and avoid
constructing large strings during the analysis phase. Second, using a separate
file is more convenient for the user. Instead, use
ctx.actions.expand_template
,
which performs substitutions on a template file.
Create a template
attribute to declare a dependency on the template
file:
def _hello_world_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".cc")
ctx.actions.expand_template(
output = out,
template = ctx.file.template,
substitutions = {"{NAME}": ctx.attr.username},
)
return [DefaultInfo(files = depset([out]))]
hello_world = rule(
implementation = _hello_world_impl,
attrs = {
"username": attr.string(default = "unknown person"),
"template": attr.label(
allow_single_file = [".cc.tpl"],
mandatory = True,
),
},
)
Users can use the rule like this:
hello_world(
name = "hello",
username = "Alice",
template = "file.cc.tpl",
)
cc_binary(
name = "hello_bin",
srcs = [":hello"],
)
If you don’t want to expose the template to the end-user and always use the same one, you can set a default value and make the attribute private:
"_template": attr.label(
allow_single_file = True,
default = "file.cc.tpl",
),
Attributes that start with an underscore are private and cannot be set in a
BUILD file. The template is now an implicit dependency: Every hello_world
target has a dependency on this file. Don’t forget to make this file visible
to other packages by updating the BUILD file and using
exports_files
:
exports_files(["file.cc.tpl"])
Going further
- Take a look at the reference documentation for rules.
- Get familiar with depsets.
- Check out the examples repository which includes additional examples of rules.