Skip to main content
Goal: By the end of this lesson, you should have a rough mental model of what Bazel is doing behind the scenes as it performs your test and build commands.

Bazel Evaluation Model

Bazel’s runtime is a series of phases. Developers won’t typically interact with these phases. If you ask Bazel to run a test, it automatically performs the prerequisite phases. Phases are executed as a partially ordered pipeline - a phase may begin before the previous one has finished. For example, starting in Bazel 7, execution can start before analysis is complete, thanks to Skymeld.
We’ll describe two “pseudo” phases first. Bazel doesn’t document them, but it’s a more natural mental model.

1. Configuration pseudo-phase

Read all the source files and update the BUILD files. In this exercise, you will run the Aspect CLI’s gazelle task (aspect gazelle), which is configured to use a pre-compiled “Gazelle” with an easy extension model. Be aware that most repositories use a Go source distribution instead, so developers compile a gazelle_binary and then run that. This phase is manual. Bazel doesn’t do it for you because it may take longer than Bazel is willing to spend on a “no-op” build. In Bazel terminology, it’s not even regarded as a “phase,” but it is the first step in the process, so it’s modeled this way in the course. Developers are expected to “configure” when they get an error message that BUILD files are out-of-date. It’s also possible to automate this in other ways, such as with editor extensions, autogazelle, etc.
Try it out in the examples repo:
  1. Modify a BUILD file to make it outdated. For example, comment out a line in the deps of the server_lib target in logger/backend/cmd/server/BUILD.bazel
  2. Run aspect gazelle. All the source files for enabled languages are parsed, to infer their dependency graph.
  3. Observe that the BUILD file is updated.
Many Gazelle extensions already exist for other languages. Aspect’s Extension Language lets you add them easily in Starlark. We’ll learn about that in the 150 course.

2. Fetching pseudo-phase

Download external resources that are required by the requested targets. This phase is implicit. During the “Loading” phase, any references that walk outside the sources in the monorepo automatically trigger fetching to occur. Bazel’s dependency graph should always determine which dependencies are fetched lazily as needed. For example, a third-party package not used anywhere in the repository should never be fetched. If you see something fetched that shouldn’t be, raise an issue with your DevInfra team! As a result, you ought to be able to turn off WiFi, unplug the network, or run Bazel with --nofetch, and still perform the remaining phases.
In the logger folder of the example repo, try:
  1. Run bazel fetch cli/... to download all the Python dependencies from PyPI.
  2. Run bazel fetch frontend/... to download all the JavaScript dependencies from npm.
  3. Fetch some third-party packages directly, for example:

Caching fetches: the repository cache

Fetching external dependencies can be slow. In a big monorepo, you’ll download many large files for hermetic toolchains. Bazel caches these in the $(bazel info repository_cache) folder, taking the following steps:
  • Caches the downloaded files.
  • Always give the integrity hash, that’s the key
In Bazel 8, there’s no cache for external repositories created by repository rules!! This is a frequently encountered and slow de-optimization. It should be thought of as a “downloader cache”. Bazel 9 adds --repository_content_cache: A true repository cache for Bazel

3. Loading phase

Load and evaluate all extensions and all BUILD files that are needed for the build. This triggers fetching as well. The execution of the BUILD files instantiates rules (each time a rule is called, it gets added to a graph). This is where macros are evaluated.
For example, you can trigger loading to occur by performing a query.
  1. Run bazel query 'somepath(client, @@zlib~1.3//:zlib.h)' to see why the Java application depends on zlib.
  2. As a result, Bazel constructs the “Dependency Graph” (where the nodes are “targets” and the edges are dependencies between them).

4. Analysis phase

The implementation function for each rule is executed, and actions are instantiated. An “action” describes how to generate a set of outputs from a set of inputs, such as “run gcc on hello.c to get hello.o”. You must list explicitly which files will be generated before executing the actual commands. In other words, the analysis phase takes the graph generated by the loading phase and generates an action graph. You can think of this as a “dry run” of the build. Bazel will figure out all the build steps (“actions”) that it needs to perform. 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.
Try this in the examples:
  1. Run analysis: bazel build --nobuild //backend/...
  2. As a result, you know the build has been defined without any errors.

5. Execution phase

Actions are executed, but only if at least one of their outputs is requested by the user. The bazel-out folder is up-to-date at the end of the execution phase. Finally you have everything in-place for Bazel to spawn sub-processes which do the actual work of the build.
Try it out with: bazel build //...