Project: /_project.yaml Book: /_book.yaml
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, and the Rules SIG template as a starting point for new rulesets.
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
.
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:
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._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.
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!
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.
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
.
By default, only targets created by rules may appear as dependencies (such as a foo_library()
target). If you want the attribute to accept targets that are input files (such as 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
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
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"])