Bazel has been available as open source software since March 2015 and has developed from Google’s internal tool Blaze. Blaze itself has been successfully used internally by Google for over 10 years. In the end, they didn’t want to withhold it from the community anymore and therefore published the essential parts of it. It follows that Bazel is already a very stable and mature tool.
How does Bazel help you?
Anyone who has ever tried to build or extend Angular source code and run some tests afterwards knows how tedious and lengthy this is. Bazel can solve this problem for you. It has reduced the test execution time from one hour to 15 minutes. How does Bazel do that? Having been developed by Google, it is naturally well-adapted to Google’s conditions, meaning it is fit to deal with huge code repositories where every application and its dependencies are built from actual source code. Bazel achieves the promised speed with the help of incremental builds, caching and parallelization of builds. It’s worth to mention that these builds can also be executed remotely in a so-called build cloud, where the load can be distributed across multiple build servers. The result can then be reused by all developers via caching mechanisms.
What’s so special about Bazel?
Getting started with Bazel, the learning curve can be quite steep at first. What is Bazel even? Is it something in the same line of thought as webpacks or maybe a substitute to a Jenkins build server? Well, actually it’s none of that. Bazel neither replaces webpacks nor a build server. Bazel is what we call a build tool. Opposite to it are the dev tools, including webpacks. Figure 1, having been shown by Alex Eagle (Mr. Bazel) during several talks about Bazel, helps enormously in understanding it.
For a couple of years so-called dev tools have gained a lot of popularity. For example, attempts have been made to map the entire build in a webpack configuration, rendering tools like Gulp or Grunt less and less important. The intention to save developers some tools is a noble and understandable one, but unfortunately, we are actually generating more complexity than originally intended. Why is that? Figure 2 illustrates the problem.
Do you see what’s going on? Each combination of tool and library now requires an own plug-in, so we end up in a plug-in jungle, where each plug-in uses a different set of configurations that now need to be mastered. Do you also have the urge to escape from such a configuration hell? With Bazel, each plug-in only needs to be developed exactly once. Bazel offers support for that by providing a readable language for configuring our builds. Through this unified language, Bazel manages to create uniform configurations that operate independently of the dev tools and programming languages used behind them. The inputs of a configuration and the outputs of a build are thus clearly defined, making it easier for different plug-ins to work together (Fig. 3). This can drastically decrease the number of plug-ins.
Starting a new project with Angular and Bazel
To create a new project with Angular and Bazel, Bazel must first be globally installed via npm. You can use the following command: npm i -g @angular/bazel. After that, use the Angular CLI command to create a new project with Bazel as build tool: ng new –collection=@angular/bazel. Looking at the workspace of the created project, at first glance you can’t see any difference compared to a normal Angular CLI project (Fig. 4), just as intended. This way we can use familiar commands like ng serve, ng build and ng test as we did before switching to Bazel.
Angular Builders, introduced in Angular 8, form an abstraction layer and hide the actual build tool that is used. You need to go to the angular.json file to find a reference to the Bazel command. If we now use ng serve to build our project for the first time, it will take some extra time. But for all of the following builds you won’t notice any difference to the usual build times because of the incremental build.
Adding Bazel to existing projects
Bazel can be added easily to an existing Angular CLI project: ng add @angular/bazel. Use a git status command to take a closer look at all the changes made to the project in this case. However, the specific Bazel configuration files are still hidden from us in this mode and only actually generated at runtime. But since we are most interested in these files when we want to get to know Bazel, we can extract them with the following command: ng build -leaveBazelFilesOnDisk. After that we should find files named WORKSPACE and BUILD.bazel in the root folder, just as we did with src/BUILD.bazel.
However, at the moment we can only successfully build an untapped Angular CLI project. As soon as we add a new dependency to the project, even if it is only an import to the FormsModule, we already have to adjust the Bazel configuration manually. Of course, the Angular CLI will do this at some point in the future, but it isn’t ready to take over these tasks yet. Nevertheless, it does not hurt to learn the basics of Bazel, and the manual changes are not too difficult anyway.
In order to be able to really “go bazeling”, we must first clarify a few terms. A Bazel workspace is identified by the WORKSPACE file and located in the main directory of the project, where the files to be built are located and Bazel stores its build results. This workspace can now be further divided by packages. Each directory containing a BUILD file represents a separate package. It makes sense to define several packages, because Bazel only rebuilds the packages where changes have actually been made. The BUILD file itself contains a description of how the package should be built. In Bazel’s language, these are called rules. The rules are first loaded into the BUILD files and then executed. A rule now describes how the desired output files are created from the defined input files. The goal of Bazel is that the BUILD files completely describe the inputs and outputs of the build process, which allows a more detailed analysis of the build artifacts and their changes.
Let us now take a closer look at an example rule located in the generated BUILD file in the src folder of our project and was created by switching to Bazel (Listing 1).
Listing 1: Bazel Rule
// 1. Rules must to be loaded first load("@npm_angular_bazel//:index.bzl", "ng_module") // 2. execution of the ng_module rule ng_module( name = "src", srcs = glob( include = ["**/*.ts"], // Input-Files exclude = [ "**/*.spec.ts", "main.ts", "test.ts", "initialize_testbed.ts", ], ), assets = glob([ "**/*.css", "**/*.html", ]) + ([":styles"] if len(glob(["**/*.scss"])) else ), deps = [ "@npm//@angular/core", "@npm//@angular/platform-browser", "@npm//@angular/router", "@npm//@types", "@npm//rxjs", // additional dependencies on libraries can be added here: // e.g.: @npm//@angular/forms ], )
Even though Bazel is designed for large monorepos like the ones used by Google, it can make sense to switch to Bazel even for smaller projects in order to reduce build and development times. However, this should only be considered once Bazel is fully integrated into the Angular CLI, thus minimizing the manual configuration effort. For those who want to delve even deeper into the subject, I recommend reviewing the Angular Bazel Example.