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.
If you weren't able to create working BUILD.bazel
files in the previous section, you can skip ahead
by checking out the packages
branch, e.g. git reset --hard packages
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 - --disk_cache
is simple, but grows unbounded since there is no eviction so beware!
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.
Exercise: bazel build
Build some targets in the codelab repo. 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
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 and tools attributes.
- Bazel enables local sandboxing by default, on systems that support sandboxed execution.
local
causes commands to be executed as local subprocesses.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.
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 written to the source tree.
For example, a go_proto_library
target produces a foo.pb.go
file which the editor expects to
read in order to provide valuable completion (intellisense) to a developer navigating the API.
While it's recently possible to use the GoPackagesDriver to teach the editor to look in bazel-out
for this file, this is a difficult, brittle configuration to setup on developer machines.
And then you'll just have the next tool or language where the bazel-out
location breaks things.
We can un-break the situation if we relax the Bazel dogma around writing to the source tree.
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.
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.
Sadly, Bazel doesn't have a watch mode built-in, but bazel-watcher does. You can install it in a couple 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
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 like
IBAZEL_BUILD_COMPLETED SUCCESS
js_run_devserver is one rule that is aware of the protocol.
$ ibazel run frontend
Click the link to open the site in the browser, then make some edits to frontend/index.ts
, for
example change the Get Server Logs
text that appears on the button.
You may need to "Shift-refresh" in the browser to force a reload of the JavaScript code.
We have a recipe for making the browser hot-reload as well.