Skip to content
This repository has been archived by the owner on Mar 20, 2024. It is now read-only.
Alex Eagle edited this page Dec 13, 2017 · 10 revisions

Bazel user guide [incomplete draft]

Using Bazel

Welcome to Bazel! This is the build tool used at Google for nearly all projects, including all Frontend work. We like it a lot, so much so that ex-googlers have started clones like Pants and Buck, and probably a bunch of others, before Bazel was open-sourced. Now you don’t need to clone it, and can share the same build tooling as the Angular developers at Google.

We find a few key advantages from using Bazel, and all of them are benefits to engineering productivity.

  • Can parallelize a build across workers in the cloud, without touching the Build definitions

  • Builds are highly incremental - typically the work required to re-build is proportional to what you changed

  • Almost never need to “clean” the output tree, so you can stay incremental

  • A CI system can run all the tests affected by a change across a wide range of interdependent packages, and can also be incremental

  • Plugins, known as rules, are highly composable. You can generally take two different rules written without knowledge of each other, and just plug them together without surprising broken interactions between them

This guide assumes you are already familiar with JS/TypeScript and/or Angular development.

Installing

Bazel will install its own toolchain for builds, so you don’t need much installed even on a new computer.

Bazel

ibazel (iterative Bazel, aka. watch mode)

Install it with your package manager, eg. npm install --save-dev @bazel/ibazel or yarn add -D @bazel/ibazel

Note: we don’t recommend global install, because it’s so non-hermetic: who knows which version you or your teammate are running?

To use it, just replace bazel with ibazel in any command. This assumes ibazel is on your path: see https://github.com/bazelbuild/bazel-watcher/issues/45 for comments about how to set this up.

graphviz (optional)

This is a useful tool for visualizing the dependency graph.

  • Mac: brew install graphviz
  • Others: TODO

Configuring Bazel

Bazel takes options on the command line, but any option can be placed in a unix-style RC file instead.

We recommend adding a tools/bazel.rc file in your workspace root, which will be checked into version control and shared with your team and CI. If you have personal preferences for this project, you can add these to the .bazelrc file in the project root, and add that file to your .gitignore. If you have personal preferences that you'd like to apply globally, put them in the .bazelrc file in your home directory. See bazelrc for the full docs.

Here is a template tools/bazel.rc file to get you started:

###############################
# Directory structure         #
###############################

# Don't create bazel-* symlinks in the WORKSPACE directory.
# These require .gitignore and may scare users.
# Also, it's a workaround for https://github.com/bazelbuild/rules_typescript/issues/12
# which affects the common case of having `tsconfig.json` in the WORKSPACE directory.
#
# Instead, you should run `bazel info bazel-bin` to find out where the outputs went.
build --symlink_prefix=/

# Another good choice is to create a dist/ directory. Then you can use
#
# build --symlink_prefix=dist/
#
# to get folders like dist/bin
# But be aware, this will still create a bazel-out symlink in your project directory.
# You may still need to exclude that, eg. from the editor's search path.

###############################
# Output                      #
###############################

# A more useful default output mode for bazel query
# Prints eg. "ng_module rule //foo:bar" rather than just "//foo:bar"
query --output=label_kind

# Don't print every dependency in :node_modules, for example
query --noimplicit_deps

# By default, failing tests don't print any output, it goes to the log file
test --test_output=errors

# Show which actions are run under workers,
# and print all the actions running in parallel.
# Helps to demonstrate that bazel uses all the cores on the machine.
build --experimental_ui
test --experimental_ui

###############################
# Typescript / Angular / Sass #
###############################
# Make TypeScript and Angular compilation fast, by keeping a few copies of the compiler
# running as daemons, and cache SourceFile AST's to reduce parse time.
build --strategy=TypeScriptCompile=worker --strategy=AngularTemplateCompile=worker

# Enable debugging tests with --config=debug
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results

Querying the build graph

Using the graphviz optional dependency, you'll have a program dot which you can use with bazel query:

$ bazel query --output=graph ... | dot -Tpng > graph.png

Bazel Rules for Frontend development

"Rules" are like plugins for Bazel. Many rule sets are available. This guide documents the ones maintained by the Angular team at Google.

Rules are used in BUILD.bazel files, which are markers for the packages in your workspace. Each BUILD.bazel file declares a separate package to Bazel, though you could have more coarse-grained distributions so the packages you publish to eg. npm might be made up of many Bazel packages. Note that Bazel also allows these files to be called BUILD, though you should be careful not to have collisions with directories named build since case-insensitive filesystems will do The Wrong Thing.

In the BUILD.bazel file, each rule must first be imported, using the load statement. Then the rule is called with some attributes, and the result of calling the rule is that you've declared to Bazel how it can derive some outputs given some inputs and dependencies. Then later, when you run a bazel command line, Bazel will load all the rules you've declared to determine an absolute ordering of what needs to be run. Note that only the rules needed to produce the requested output will actually be executed.

JavaScript tooling rules

Setup

For these rules to work, Bazel needs to partner with your package manager (Yarn or npm) so that dependencies are available to the toolchain. For example, the @bazel/typescript package on NPM provides the TypeScript bazel rules used below. That means we need to install the dependencies using the package manager before running the first build.

Then we need to tell Bazel how to find the rules, using a WORKSPACE file in the root of your workspace (typically that's the root of your repository), and then we need to declare a filegroup rule that tells Bazel how to find the installed dependencies in the node_modules directory.

See the Installation instructions for how to set up these prerequisites.

If you want to run bazel commands from the package.json scripts, you must take care not to lock the Bazel server in the initial bazel run @yarn... command. Otherwise the script will hang trying to run Bazel again underneath Bazel -- it is not re-entrant. See https://github.com/angular/tsickle/commit/d5ab3942c99ed991fa51fc4b21f9e4627afefeb8 for a workaround using the --script_path argument to bazel run.

Usage

The NodeJS rules (rules_nodejs) allow us to run JavaScript under Bazel, either as a tool that runs as part of the toolchain, or as a test or a binary program the users asks Bazel to execute.

nodejs_binary makes a runnable program based on some JS files and an entry point path, relative to the output root. To provide extra inputs which must be available to be read at runtime, put these in the data attribute. You can call this rule to produce a target that the user can run:

BUILD.bazel

nodejs_binary(
    name = "hello_world",
    ...
)
$ bazel run :hello_world
Hello, world!

jasmine_node_test runs some JavaScript spec files through the Jasmine test framework. This is used with the bazel test command.

Details about these rules can be found in the README of the rules_nodejs repository.

TypeScript rules

TypeScript is a popular typed language that is a superset of the JavaScript you already know, and compiles down to plain JavaScript. The TypeScript rules live at rules_typescript.

Setup

To set up the TypeScript rules, add them to your WORKSPACE file:

git_repository(
    name = "build_bazel_rules_typescript",
    remote = "https://github.com/bazelbuild/rules_typescript.git",
    tag = "0.6.1",
)

load("@build_bazel_rules_typescript//:defs.bzl", "ts_repositories")

ts_repositories()

Finally, be sure you added the --strategy settings above to your bazelrc file to make the TypeScript compiler run faster.

Compiling TypeScript

ts_library compiles one package of TypeScript code. Each library is compiled independently, using the .d.ts declaration files from the dependencies. That means a package is only re-built when an API it depends on is changed.

Attributes:

  • srcs are some TypeScript files (.ts and .d.ts)
  • deps are any rules that produce .d.ts outputs, typically other ts_library rules.
  • tsconfig points to your tsconfig.json file, which is important so the editor uses the same TypeScript settings as Bazel.

Outputs:

  • default: .d.ts file for each input .ts file.
  • in the same output directory with the .d.ts outputs, there is an ES5 (devmode) .js file, intended for consumption by rules that depend (maybe transitively) on this one.

We recommend few tsconfig.json files, ideally just one for your whole repository, or even your whole company if it’s not too late (we do that at Google).

Note that Bazel controls parts of the tsconfig, namely those related to locating inputs and outputs, managing dependencies on typings, and producing JavaScript output that’s readable by downstream tooling. (Currently this format is un-bundled UMD modules, wrapping both named (non-anonymous) AMD modules and commonjs modules. Bazel may introduce new requirements on your TS code, for example, we always use –declarations to produce .d.ts outputs needed by dependent rules, and your code may have some errors which only manifest under that rule.

If you find this rule is running slowly, consider breaking it into multiple rules, then declare the dependencies between them (you may have to do some refactoring to find a non-cyclical partitioning of the code, otherwise Bazel errors)

Running a development server

The ts_devserver rule brings up a development server from your application sources. It's intended to be used with bazel run (or ibazel run in watch mode).

Attributes:

  • deps are your application sources, typically ts_library or ng_module targets
  • scripts are other sources which are not dependencies of your deps, but which should be included in the bundled JavaScript
  • serving_path is what URL the server exposes for the bundled JavaScript. This is optional; if not specified the bundle is served at /_/ts_scripts.js
  • static_files are other files, such as images or HTML, which should be served. Note that the devserver only serves files from the package where the ts_devserver rule is declared.
  • entry_module is the AMD module name of the application entry point. It should start with your workspace name, eg. my_workspace/path/to/main for an input path/to/main.js

Notes:

  • In development mode, the ts_library rule produces JavaScript in a UMD format, with named AMD modules inside. This allows us to bundle them with a simple cat command, which is fast and simple.
  • The ts_devserver relies on ibazel to expose a livereload server. It will inject a livereload script into the page so you don't need to reload when the app is re-built.

Angular rules

The Angular rules run the Angular compiler (ngc) on individual packages. These live in the Angular bazel package. Issues can be filed in the main issue tracker, https://github.com/angular/angular/issues where we'll add a comp: bazel label.

Setup

As with the TypeScript rules, we first install the npm dependency, eg.

$ bazel run @yarn//:yarn -- add -dev @angular/bazel

and install the Bazel dependency in WORKSPACE:

local_repository(
    name="angular",
    path="node_modules/@angular/bazel"
)

We must also do an extra step to generate code for our dependencies. Since Angular libraries (including @angular/common) are distributed only with JavaScript and Angular Metadata, they are incomplete. In a non-Bazel setup, the ngc command compiles the whole world, including the libraries in node_modules. Since we only compile one package at a time in Bazel, we must bootstrap this by compiling the libraries first before any ng_module rules execute.

Currently we recommend doing this in your package.json as a postinstall script. We'll just run ngc like any standard non-Bazel application does, but with zero application sources. This will just leave the generated code in the library directories under node_modules. Any time you re-install your dependencies, this postinstall script makes sure they have generated code too.

Around the time of Angular 6 we expect this step can be removed, follow https://github.com/angular/angular/issues/18810

{
    "scripts": {
        "postinstall": "ngc -p angular.tsconfig.json",
    }
}

This requires an extra angular.tsconfig.json file inside your project, which can be short:

{
    "compilerOptions": {
        "lib": [
            "dom",
            "es2015"
        ],
        "experimentalDecorators": true,
        "types": []
    },
    "include": [
        "node_modules/@angular/**/*"
    ],
    "exclude": [
        "node_modules/@angular/bazel/**",
        "node_modules/@angular/compiler-cli/**",
        "node_modules/@angular/tsc-wrapped/**"
    ]
}

Usage

ng_module runs the Angular compiler on one package of Angular code. At Google, we make our packages small, typically 1:1 with an @NgModule declaration in the code, so the rule is named to be synonymous with this convention.

ng_module behaves exactly the same as ts_library, except that it accepts additional attributes:

  • assets point to .html and .css inputs. If you use SASS, the asset should be the label of the sass_binary rule. The angular compiler produces extra JavaScript output corresponding with these inputs.

Outputs:

  • same as for ts_library, with the addition of .ngsummary.json files which are for ngc what .d.ts files are for tsc
  • also produces .ngfactory.d.ts/.ngfactory.js files, but we plan to stop producing these for Angular 6

Coming soon

If you build the code using the rules above, you'll have .d.ts and .js files, but don't know how to use them.

Soon we'll have

  • a karma rule that runs unit tests
  • a protractor rule that runs end-to-end tests against real running server(s)
  • a production bundling/optimizing rule that generates a distribution of the app (or library)

End-to-end example

The repository https://github.com/alexeagle/angular-bazel-example contains the canonical example maintained by the Angular team at Google.

The master branch reflects what parts we suggest trying today. Everything in that branch is supported for registered early adopters of ABC, and is roughly "Beta" quality software (still subject to design changes, and not well documented).

Other branches of this repository demonstrate work-in-progress, such as a devserver, karma testing, and production bundling. These bits are experimental and not supported for use by anyone. During 2017Q4 we expect that these will mature to "beta" status, at which time usage will be demonstrated on the master branch.

Clone this wiki locally