Configuring BUILD files
To configure Bazel, you'll need to author BUILD
files, and sometimes improve how they are written.
By the end of this section, you'll have written a simple BUILD
file.
Concepts
A "Bazel package" is a filesystem tree rooted at a BUILD
or BUILD.bazel
file.
root/ # This is the "//" package
├── BUILD.bazel
├── ...
├── animations # Here is the "//animations" package
│ ├── BUILD.bazel
│ ├── browser # Here is "//animations/browser"
│ │ ├── BUILD.bazel
│ │ └── ...
| | ...
│ ├── src
│ │ └── index.ts # This file is in the `animations` package, since there's no BUILD here
│ ├── test
│ │ ├── BUILD.bazel
│ │ └── ...
Packages are encapsulated.
glob
doesn't cross them (!!)- Sources in a package aren't visible outside without
exports_files
- Outputs must be written within the same package
We prefer BUILD.bazel
over BUILD
to be more explicit,
to allow for tooling to select with *.bazel
,
and to avoid colliding with a directory named build
on case-insensitive systems.
However in text it's easier to abbreviate them by writing just BUILD
to mean either file.
Labels
A "Label" is a string identifier that refers to a source file, an output file, or a target.
Example:
┌ package name ┐
v v
@angular//animations/utils:draw_circle
^ ^
| └ target
└ repository name (optional)
Label shorthand
If the working directory is in the same workspace, //animations/util:draw_circle
//
means the root of that workspace.- On the command line, labels can be relative to the working directory
- Each package has a default label, named after the package
You can usually use this shorthand to save typing.
For example you could just cd backend; bazel run devserver
rather than
bazel run //backend/devserver:devserver
.
Every package should have a nice default target, to save typing and make an ergonomic experience
for developers interacting with Bazel in your project.
You can use alias
to introduce an indirection, for example if you'd like users to be able to
bazel run backend
from the repository root, then you'd add an alias
:
alias(
name = "backend", # the default target for the backend package
actual = "//backend/devserver",
)
Run bazel help target-syntax
Starlark
Starlark is a python-ish language used by Bazel, buck, tilt, and many other tools. There are Java, Go, and Rust implementations of the interpreter.
The spec is surprisingly readable, and explains things like how the execution model is guaranteed to allow parallel evaluation. Read: https://github.com/bazelbuild/starlark/blob/master/spec.md
BUILD.bazel
files are written in a subset of Starlark.
Bazel extensions, written in *.bzl
files, use the full Starlark language.
Anatomy of a BUILD file
load
statements
load
statements should appear at the top of the file.
They import symbols into file scope.
The first argument is a label of a .bzl
source file, and following arguments are symbols to load.
load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files")
You can alias a symbol on load, to avoid collisions:
load("@npm//:typescript/package_json.bzl", typescript_bin = "bin")
package
statement
Optionally, you can define defaults for all targets in the BUILD
file:
package(default_visibility = ["//visibility:public"])
Target declarations
A "rule" is like a constructor, creating a target. These are "bare facts": they only describe the source files and their dependencies. They do not say what to do. The rule implementation is responsible for instructing Bazel what build steps are required.
ts_project(
name = "compile",
srcs = ["index.ts"],
tsconfig = "//src:tsconfig",
data = ["my.json"],
deps = [":node_modules/color"],
)
The arguments to the ts_project
rule are called "attributes". Some are common for most rules:
name
is always required; we'll use this in a label to refer to the targetsrcs
typically means files in the source tree which are grouped togetherdeps
typically means other targets, either 1p or 3p, needed at build timedata
is likedeps
but is only needed at runtime
Other attributes are particular to the rule implementation.
tsconfig
is an attribute specific tots_project
which tells Bazel where the configuration file is
Writing BUILD files
The majority of content in the BUILD file should be generated by bazel configure
(Aspect CLI only)
or by building a Gazelle binary and running it.
These things can be automated:
srcs
contains all files in the current package with the conventional extension.deps
can be determined by looking at all theimport
statements in thesrcs
and mapping them to labels which provide that symbol.- tests can be separated out by a file convention
The glob
function allows you to skip file listing, for example with srcs = glob(["*.ts"])
.
However, the glob must be evaluated every time Bazel loads the file, and so it incurs a performance penalty, especially as the number of files in the package grows.
It also doesn't descend into sub-packages, so it's easy to omit files by accident.
Finding rules
The first approach is just to learn more APIs which are already available. If the problem you're solving isn't novel, then other engineers probably ran into it before, and may have provided a solution you can simply adopt.
There is a vibrant ecosystem of rules for Bazel, and you should be able to find documentation for those.
- bazel_skylib has many utilities like
copy_file
- aspect-bazel-lib builds on top of
bazel-skylib
with many more utilities - Recommended rules from the Bazel team
You should skim through these to form a rough memory of what's available.
This way when you encounter an interesting problem while writing a BUILD
file, you can search on this site
to find that useful nugget.
You can also look through example repositories like aspect bazel-examples to find solutions to problems similar to yours. If you don't find a solution, consider donating a Feature Bounty on our OpenCollective and we can add it for you.
Key building block: run_binary
This rule is an "adapter" from an executable (something you could bazel run
) to an action (something you can bazel build
).
The executable (called a "tool" here) is run in a single action which spawns that executable given some declared inputs, and produces some declared, default outputs.
Bazel's built-in genrule
looks a lot like run_binary
, but it's best to avoid it.
- Arbitrary bash one-liner, commonly non-hermetic
- Bash dependency hurts portability
- Subtly different semantics for
expand_location
,stamp
, etc.
Here's a sample usage, which runs my_tool
with three arguments to produce a folder called dir_a
:
- The path to
some.file
which is the only input - An
--outdir
flag, which we know from reading the CLI documentation for my_tool.- We're always required to predict what path the tool will write to. If you get it wrong, Bazel will error that the "output was not produced".
- A syntax-sugar shorthand for "the output folder Bazel assigns for this action"
load("@aspect_bazel_lib//lib:run_binary.bzl", "run_binary")
run_binary(
name = "dir_a",
srcs = ["some.file"],
args = ["$(execpath some.file)", "--outdir", "$(@D)"],
out_dirs = ["dir_a"],
tool = ":my_tool",
)
The js_run_binary
rule takes it a step further, adding the ability to:
- capture stdout/stderr/exit code as "outputs"
chdir
to a specific working directory- throw away log-spam on success
Follow https://github.com/bazel-contrib/bazel-lib/issues/575
Making tools work
The tool
in run_binary
can be any executable.
However some tools don't work the way Bazel expects.
This can usually be fixed without having to change the tool, which is good since most tools are
written by third-parties who don't care about your Bazel migration problems!
Google engineers got in the habit of rewriting everything to work with Blaze. Do not follow their lead! Changing more than one thing at a time makes your migration riskier.
You can make most tools work under Bazel by asking: "How can the tool tell that it's running under Bazel?"
There are three ways to make the tool think it's still running outside Bazel:
- "Monkey-patch" the runtime
- Node.js
--require
flag to run - JVM has a classpath, you can inject a shadowing class
- Node.js
- In-process wrapper
- Peel one layer off the tool's CLI
- Write your own CLI that calls its entry point
- Parent process wrapper
- Often a short Bash script
Comparing Rules and Macros
So far, we've used features that ship with Bazel or with rulesets. What do we do when we need something more?
First, recall that an Action is a transformation from some inputs to some outputs, by spawning a tool.
A "Rule" extends Bazel to understand how to produce an action sub-graph from the user's dependency graph.
Features:
- Output Groups: Multiple named sets of outputs
- Can run multiple actions. Which actions run depends on which outputs are requested.
- Interoperability API with other rules: "Providers"
- Walk the dependency graph: "Aspects"
We'll learn to write a custom rule, however they are an advanced topic and not needed in many cases.
Macros are significantly easier to write than custom rules.
As a product engineer who rarely needs to interact with Bazel internals, it's likely not worth your time to learn how to write a custom rule.
However you can nearly always accomplish your goal with a macro instead.
So, we'll learn about the more usable alterative in the next section: Macros.