Skip to main content

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.

Aspect CLI only

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

  • 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.