234 lines
9.5 KiB
Markdown
234 lines
9.5 KiB
Markdown
+++
|
|
template = "article.html"
|
|
title = "Why Bazel?"
|
|
date = 2019-11-02T18:00:00+11:00
|
|
description = "An overview of Bazel's core concepts, from hermetic builds and reproducibility to extensibility and its three-phase build system."
|
|
|
|
[taxonomies]
|
|
tags = ["bazel"]
|
|
+++
|
|
|
|
In this post, we'll cover what [Bazel](https://bazel.build) is, how to use it,
|
|
and why I chose to use it.
|
|
|
|
## What is Bazel?
|
|
|
|
Bazel is a build-system released by Google in 2015. It actually is derived from
|
|
the internal build-system Google uses internally for most of its own code-base,
|
|
called
|
|
[Blaze](https://mike-bland.com/2012/10/01/tools.html#blaze-forge-srcfs-objfs).
|
|
|
|
### Building at scale
|
|
|
|
Bazel has a huge focus on hermetic builds, and reproducibility. Every build step
|
|
is, from a really broad perspective, defined as a list of inputs, tools, and
|
|
outputs. This allows for efficient and robust caching (if no inputs nor tools
|
|
changed, then this target doesn't need to be rebuilt, and this cascades through
|
|
the whole build graph). Let's see a sample definition of a C++ library, as well
|
|
as a C++ binary depending on it:
|
|
|
|
*BUILD*
|
|
|
|
```python
|
|
cc_library(
|
|
name = "my_feature"
|
|
srcs = [
|
|
"feature_impl.cpp",
|
|
"utils.cpp",
|
|
],
|
|
hdrs = [
|
|
"feature.hpp",
|
|
"utils.hpp",
|
|
],
|
|
)
|
|
|
|
cc_binary(
|
|
name = "my_app",
|
|
srcs = ["main.cpp"],
|
|
deps = [
|
|
":my_feature",
|
|
],
|
|
)
|
|
```
|
|
|
|
`cc_library` and `cc_binary` are both depending an implicit dependency on a C++
|
|
toolchain (I won't enter into any language-specific features in this post, but
|
|
if you don't tell Bazel to use a specific C++ toolchain, it will try to use your
|
|
system compiler - which is convenient, but loses a bit of hermeticity and
|
|
reproducibility). Everything else is pretty obvious here: we defined two
|
|
different build targets, one of them being a library called `my_feature`, and
|
|
the other one a binary called `my_app`, depending on `my_feature`. If we build
|
|
`my_app`, Bazel will automatically build `my_feature` first as you would expect,
|
|
and then proceed to build `my_app`. If you change the `main.cpp` and re-build
|
|
`my_app`, it will skip the compilation of `my_feature` entirely, as nothing
|
|
changed.
|
|
|
|
Bazel's cache handling is really reliable. During the past few months, I've done
|
|
a lot of diverse things (writing my own rules, compiling a bunch of different
|
|
languages, depending on third-party libraries and rules...), and never had a
|
|
single time to run `bazel clean`. Now I didn't use a lot of other build systems
|
|
in the recent past, but from someone who has been using
|
|
[Gradle](https://gradle.org/) for Android previously, this feels really weird.
|
|
|
|
### Integrating tools and other languages
|
|
|
|
Another great aspect of Bazel is its extensibility. It works with rules defined
|
|
in a language called [Starlark](https://github.com/bazelbuild/starlark), which
|
|
syntax is a subset of Python's. It comes without a lot of standard Python
|
|
features, as I/O, mutable collections, or anything that could affect build
|
|
hermeticity. While this isn't the focus of this article (I will cover the
|
|
writing of a rule to run a simple tool in a later article), here is what an
|
|
example rule can look like (from
|
|
[Bazel's samples](https://github.com/bazelbuild/examples/blob/master/rules/shell_command/rules.bzl)):
|
|
|
|
*rules.bzl*
|
|
|
|
```python
|
|
def _convert_to_uppercase_impl(ctx):
|
|
# Both the input and output files are specified by the BUILD file.
|
|
in_file = ctx.file.input
|
|
out_file = ctx.outputs.output
|
|
ctx.actions.run_shell(
|
|
outputs = [out_file],
|
|
inputs = [in_file],
|
|
arguments = [in_file.path, out_file.path],
|
|
command = "tr '[:lower:]' '[:upper:]' < \"$1\" > \"$2\"",
|
|
)
|
|
# No need to return anything telling Bazel to build `out_file` when
|
|
# building this target -- It's implied because the output is declared
|
|
# as an attribute rather than with `declare_file()`.
|
|
|
|
convert_to_uppercase = rule(
|
|
implementation = _convert_to_uppercase_impl,
|
|
attrs = {
|
|
"input": attr.label(
|
|
allow_single_file = True,
|
|
mandatory = True,
|
|
doc = "The file to transform",
|
|
),
|
|
"output": attr.output(doc = "The generated file"),
|
|
},
|
|
doc = "Transforms a text file by changing its characters to uppercase.",
|
|
)
|
|
```
|
|
|
|
Once it's defined, it's re-usable to define actual build targets in a simple way:
|
|
|
|
*BUILD*
|
|
|
|
```python
|
|
load(":rules.bzl", "convert_to_uppercase")
|
|
|
|
convert_to_uppercase(
|
|
name = "foo_but_uppercase",
|
|
input = "foo.txt",
|
|
output = "upper_foo.txt",
|
|
)
|
|
```
|
|
|
|
As a result of this simple extensibility, while Bazel ships only with C++ and
|
|
Java support (which are actually getting removed and rewritten in Starlark, to
|
|
decouple them from Bazel itself), a lot of rules have been written either by the
|
|
Bazel team or by the community, to integrate languages and tools. You can find
|
|
rules for [NodeJS](https://github.com/bazelbuild/rules_nodejs),
|
|
[Go](https://github.com/bazelbuild/rules_go),
|
|
[Rust](https://github.com/bazelbuild/rules_rust),
|
|
[packaging](https://github.com/bazelbuild/rules_pkg) (generating debs, zips...),
|
|
[generating Docker images](https://github.com/bazelbuild/rules_docker),
|
|
[deploying stuff on Kubernetes](https://github.com/bazelbuild/rules_k8s), and a
|
|
bunch of other things. And if there are no rules to run/build what you want, you
|
|
can write your own!
|
|
|
|
### A three-steps build
|
|
|
|
Bazel runs in
|
|
[three distinct phases](https://docs.bazel.build/versions/master/guide.html#phases).
|
|
Each of them has a specific role, and specific capabilities.
|
|
|
|
#### Loading
|
|
|
|
The loading phase is parsing and evaluating all the `BUILD` files required to
|
|
build the requested target(s). This is typically the step during witch any
|
|
third-party dependency would be fetched (just downloaded and/or extracted,
|
|
nothing more yet).
|
|
|
|
#### Analysis
|
|
|
|
The second phase is validating any involved build rule, to generate the actual
|
|
build graph. Note that both of those two first phases are entirely cached, and
|
|
if the build graph doesn't change from one build to another (e.g. you just
|
|
changed some source files), they will be skipped entirely.
|
|
|
|
#### Execution
|
|
|
|
This is the phase that checks for any out-of-date output (either non-existent,
|
|
or its inputs changed), and runs the matching actions.
|
|
|
|
### Great tooling
|
|
|
|
Bazel comes with some really cool tools. Without spending too much time on that,
|
|
here's a list of useful things:
|
|
|
|
- [ibazel](https://github.com/bazelbuild/bazel-watcher) is a filesystem-watcher
|
|
that will rebuild a target as soon as its inputs files or dependencies
|
|
changed.
|
|
- [query](https://docs.bazel.build/versions/master/query-how-to.html) is a
|
|
built-in sub-command that helps to analyse the build graph. It's incredibly
|
|
feature-packed.
|
|
- [buildozer](https://github.com/bazelbuild/buildtools/tree/master/buildozer) is
|
|
a tool to edit `BUILD` files at across a whole repository. It can be used to
|
|
add dependencies to specific targets, changing target visibilities, adding
|
|
comments...
|
|
- [unused_deps](https://github.com/bazelbuild/buildtools/blob/master/unused_deps/README.md)
|
|
is detecting unused dependencies for Java targets, and displays `buildozer`
|
|
commands to remove them.
|
|
- Integration [with](https://github.com/bazelbuild/intellij)
|
|
[different](https://github.com/bazelbuild/vscode-bazel)
|
|
[IDEs](https://github.com/bazelbuild/vim-bazel).
|
|
- A set of APIs for remote caching and execution, with
|
|
[a](https://gitlab.com/BuildGrid/buildgrid)
|
|
[few](https://github.com/bazelbuild/bazel-buildfarm)
|
|
[implementations](https://github.com/buildbarn), as well as an upcoming
|
|
service on Google Cloud called Remote Build Execution, leveraging GCP to build
|
|
remotely. The loading and analysis phases are still running locally, while the
|
|
execution phase is running remotely.
|
|
|
|
## Choosing a build system
|
|
|
|
At the time I started thinking about working on this blog again, I had a small
|
|
private repository with a bunch of stuff, all compiled with Bazel. I also
|
|
noticed a [set of Starlark rules](https://github.com/stackb/rules_hugo)
|
|
integrating [Hugo](https://gohugo.io/). While I didn't need a build system,
|
|
Bazel seemed to be interesting for multiple aspects:
|
|
|
|
- I could leverage my existing CI system
|
|
- While Hugo comes with a bunch of features to e.g. pre-process Sass files, it
|
|
has some kind of lock-in effect. What if I eventually realise that Hugo
|
|
doesn't fill my need? What's the cost of migrating to a new static site
|
|
generator? The less I rely on Hugo-specific features, the easier this would be
|
|
- I could integrate some custom asset pipelines. For example, I could have a
|
|
diagram written with [PlantUML](http://plantuml.com/) or
|
|
[Mermaid](https://mermaidjs.github.io/) and have it part of the Bazel graph,
|
|
as a dependency of this blog
|
|
- Bazel would be able to handle packaging and deployment
|
|
- It sounded stupid enough to be a fun experiment? (Let's be honest, that's the
|
|
only real reason here.)
|
|
|
|
## Closing thoughts
|
|
|
|
Bazel is quite complex, and this article only scratches the surface. The goal
|
|
was not to teach you how to use Bazel (there are a lot of existing resources for
|
|
that already), but to give a quick overview of the core ideas behind it.
|
|
|
|
If you found it interesting, here are some useful links:
|
|
|
|
- Bazel's
|
|
[getting started](https://docs.bazel.build/versions/master/getting-started.html)
|
|
- A [list of samples](https://github.com/bazelbuild/examples) using different
|
|
languages as well as defining some rules
|
|
- A (non-exhaustive)
|
|
[list of rules](https://docs.bazel.build/versions/master/rules.html), as well
|
|
as the documentation of all the built-in rules
|
|
|
|
In the next article, we'll see how to build a simple Kotlin app with Bazel, from
|
|
scratch all the way to running it.
|