Requesting Build Outputs
Now we'll finally build some code.
By the end of this section, you'll be able to get desired outputs with bazel build
and execute
some application with bazel run
for at least the language you picked.
Exercise: let's build something
Instructor chooses some code in the examples repo to demonstrate.
This may be where you discover errors in the BUILD
files that weren't evident earlier when you only analyzed, as this is the first time we're running the tools.
Make some minor change to the sources and build again. Can you get some cache hits in this second build?
Bazel prints the paths to the resulting output files.
You can predict where the outputs will be written, if a script wants to do something with them:
bazel help outputs
Actions
When Bazel needs to transform inputs to outputs, it does it by spawning an Action, which is just a subprocess invoking some tool.
The Action Graph
In the Analysis phase, the dependency graph is "lowered" to an action graph. In the action graph, each node is a subprocess to spawn (invoking some tool) and the edges are files and Providers which are output by one action and needed as inputs to another.
The graphs are NOT one-to-one!
For example, a ts_project
rule with a custom transpiler produces several actions.
Exercise: bazel analyze
Bazel doesn't actually have a command called analyze
.
It's spelled "build --nobuild" instead.
This command is rarely useful.
You might use it if you're making a big, breaking refactoring, so that you can resolve all the analysis failures first before attempting to build anything.
You could also use it to reason about what is the slow step in your CI pipeline.
bazel build --nobuild //...
Querying the action graph
This is a valuable skill when debugging a failure of some rule, especially when required inputs aren't declared.
You can run arbitrary starlark programs on the action graph with --output=starlark
which is a powerful tool.
Exercise: bazel aquery
- What are the declared input files to the compile action for a library target you've created?
- What providers are produced by the library target? (You'll need a tiny Starlark program)
The Action Cache
Earlier, we introduced the Repository Cache (actually a downloader cache). This is entirely separate from the Action Cache, because only actions are hermetic and know all their inputs, and therefore a correct cache key can be computed.
When a file in bazel-out
is replaced, then Bazel normally needs to run the action again to restore it.
You can have an "L1" cache locally on the machine to avoid this, using the --disk_cache
flag.
Prior to Bazel 7.4, the Disk Cache grows unbounded because there was no eviction, see https://github.com/bazelbuild/bazel/issues/5139
The cache key is typically dependent on the execution platform. This makes it very hard to share intermediate results between a macOS development machine and a Linux CI system. Non-determinism will cause cache misses, because the cache key changes anytime a dependency produces a different output. Hash ordering and timestamps are typical sources of non-determinism.
Spawn Strategies
Bazel can take several approaches to execute an action. See https://bazel.build/docs/user-manual#execution-strategy
sandboxed
causes commands to be executed inside a sandbox on the local machine.- This requires that all input files, data dependencies and tools are listed as direct dependencies in the
srcs
,data
andtools
attributes. - Bazel enables local sandboxing by default, on systems that support sandboxed execution.
- This requires that all input files, data dependencies and tools are listed as direct dependencies in the
local
causes commands to be executed as local sub-processes.worker
causes commands to be executed using a persistent worker, if available.docker
causes commands to be executed inside a docker sandbox on the local machine.- This requires that docker is installed.
remote
causes commands to be executed remotely; this is only available if a remote executor has been configured separately.dynamic
is a special value to try both remote and local execution, take the first to complete.
Inspecting the sandbox
How to find it: run Bazel with the --sandbox_debug
flag.
It will print lots of extra information, and buried in there you can find paths beneath the temporary sandbox folder.
This flag also causes Bazel to skip the cleanup and removal of the sandbox folder - leaving it available to inspect and understand.
Checking in generated files
Bazel output files are always written beneath the bazel-out
tree.
But other tools allow, or even expect, that files are present in the source tree.
We can un-break the situation if we relax the Bazel dogma around writing to the source tree.
For example, in the logger/schema
folder we should have a ts_proto_library
target
which produces a logger_pb.d.ts
interface definition file. The editor expects to read
that file in order to provide valuable completion (intellisense) to a developer navigating the API.
ts_proto_library
is a macro which automatically writes this file to the source tree, which we can
observe by deleting it and then running the tests in that package.
There are options to "teach the editor" instead. In Go for example, it's possible to use the GoPackagesDriver to teach the editor to look in
bazel-out
forlogger.pb.go
. But, this is a difficult, brittle configuration to setup on developer machines. And then you'll just have the next tool or language where thebazel-out
location breaks things.
Of course, bazel build
itself cannot result in files in the source tree, but we can get a close
approximation with a rule called
write_source_files
.
It uses a simple pattern:
- the build target writes to a file like
bazel-out/pkg/foo.pb.go
- a generated test target asserts that
pkg/foo.pb.go
in the source folder has the same content - a generated executable copies files from
bazel-out
back to the source tree - when the test fails, it prints an instruction how the developer can run the executable
This is the same pattern we use for "golden" or "snapshot" files which live in the source tree.
Exercise: Running programs
Running programs in Bazel is just a syntax sugar.
It really means "build this program, then spawn the resulting executable".
Run bazel help run
You should be able to run one of the programs you've built so far, in whichever language you have working.
Bazel's naming convention is that any *_binary
target should be executable, so you can bazel run
it.
Some other targets may be executable as well, so refer to the documentation, such as oci_load
from rules_oci.
Watch mode
It's really nice to have a "live" development server where code changes are immediately visible.
To make this possible, we need Bazel to run in "watch mode" where changes to sources are automatically
reflected in the bazel-out
tree without us having to manually run bazel
after every edit.
Bazel doesn't have a watch mode built-in, but bazel-watcher adds this capability.
That project releases a tool named ibazel
, short for "interactive Bazel".
Our example repo has this installed for you in the tools
folder.
You can read more about the technique used: https://blog.aspect.build/run-tools-installed-by-bazel
It can also be installed in these alternative ways:
- Mac:
brew install ibazel
- If you have npm installed:
npm install -g @bazel/ibazel
- Download a binary from the releases page
and install it as
ibazel
on your$PATH
.
We plan to include this feature directly in a future release of Aspect CLI, further improving out-of-the-box developer experience.
Exercise: A live devserver
Let's practice using it for the frontend, since JavaScript engineers are accustomed to having watch mode.
If you didn't write a BUILD
file for the frontend, you can checkout the packages
branch.
The target we run has to be aware of the Bazel-watcher protocol:
- The target must be tagged to indicate its awareness:
tags = ["ibazel_notify_changes"]
This tag prevents the binary being restarted every time the files change. - The program must read from
stdin
to find out when a build has finished. Bazel-watcher will write lines likeIBAZEL_BUILD_COMPLETED SUCCESS
js_run_devserver
is one rule that is aware of the protocol.
$ ./tools/ibazel run logger/frontend
Click the link to open the site in the browser, then make some edits to logger/frontend/index.ts
, for
example change the Get Server Logs
text that appears on the button.