I have been using Bazel extensively for my personal projects of late, being familiar with the whole paradigm from my work at Google. It’s quite refreshing to use compared to other build systems like CMake, Gradle and Make which are now looking positively ancient.

Sure, the comfort of using something I already know and use every day does factor into my bias towards Bazel. But this was the case even when Bazel was not released and I was using Blaze within Google: it was light-years better than any other build system that I had come across, including Maven/Gradle (while futzing around in Android) and CMake (from my days playing around with ITK/VTK and Boost).

But Bazel isn’t entirely without its quirks. Unlike at Google, which has a monorepo with all dependencies well specified, and with well-staffed languages/libraries teams, Bazel as an open-source tool is somewhat poorly staffed in comparison, and has to deal with the exponentially greater complexity and variability of software development across the ecosystem.

This state of affairs should not be surprising: the team cannot deliver upon all the functionality expected for all languages and toolchains and use cases at once; they need to prioritize and deliver on a roadmap, which means that some subset of features will be immature at any given time. As a result, Bazel has some frustratingly annoying limitations and inconsistencies in feature parity that need to be worked around. I’m taking the view that this is a temporary situation; and over time, Bazel will achieve better stability and iron out these small annoying inconsistencies.

That said, here are my impressions.

Good: The Starlark Language

Starlark is far more expressive than any other build language I have come across, allowing fairly complex logic to be specified in an intuitive fashion. The fact that it is so powerful means that Bazel rules don’t have to rely on auxiliary scripts to get complicated things done, which does wonders for separation of concerns. Additionally, if you do need auxiliary tools, they themselves can be built via Starlark rules, and then subsequently executed from other Starlark rules - which makes it super convenient.

Good: Bazel comes with its own Toolchains

I absolutely love the fact that Bazel doesn’t rely on the presence of a specific toolchain being present on my machine; I don’t need to have Golang installed on my machine in order to build and run go binaries. The minimum required is a C/C++ toolchain and a handful of shell tools, which can be expected to be present in almost all *nix systems (and Windows of late). For Python, Bazel uses the system-installed Python binary, but Pip dependencies are not installed system-wide, and instead are managed as imported repositories. This makes it easy to maintain a relatively clean base system, and let Bazel manage all the complex dependencies.

Good: The WORKSPACE Model

The WORKSPACE file as defining the root of a buildable tree is an absolute boon for making projects modular and composable. It makes managing dependencies an absolute pleasure.

The WORKSPACE acts as a single place where all external dependencies a project might have for its build is clearly specified; and internal BUILD rules depend upon the WORKSPACE definitions. As long as a project has a WORKSPACE file at its root level, any other project can import it directly using a git_repository or http_repository rule with its own alias for the project, and be confident that all BUILD rules that it has visibility to can be referenced as a dependency, and Bazel will transparently figure out how to build and resolve the dependency.

This modularity is fantastic: as long as an arbitrary Git repository has a WORKSPACE file and a tagged release, I can simply import it into my workspace via a git_repository rule, and be assured that it will mostly work as expected.

Good: Predictability of Artifact Paths

Bazel is similar to other, more mature build systems in this one: any artifact that’s built using Bazel is present in a predictable location, which makes locating build artifacts while resolving dependencies extremely straightforward. Even if your rule isn’t working because a dependency isn’t resolving correctly, the predictability makes it easy to reason why the dependency isn’t being generated correctly and fix it as necessary.

Good: The Build Artifact Ecosystem

There are build artifacts for pretty much anything you want to build. A protocol buffer? Sure, there’s a Bazel-team-blessed version. GRPC, why have one when you can have two? Python? You’ve got two options: either a py_binary, or a par_binary. And if there’s a build artifact that isn’t present, I can always author my own rules in Starlark.

Good: Crosstools support

While I haven’t played around with it yet, I found that Bazel has support for crosstools, making targeting different architectures reasonably straightforward with a slight change in configuration options; my BUILD rules will work as-is. It also seems that I can define my own custom toolchain as well, and register it with Bazel. The fact that this support exists in the first place is wonderful - it’s so much better than mucking about with crosstools directly on the commandline.

Bad: Transitive Dependencies are not Automatically Resolved

This was a major annoyance I had to deal with. Consider this situation: your project involves multiple independent pieces, each of them in a separate repository, with a WORKSPACE file that declares its external dependencies at its root. Let’s sa y that one project depends upon another project via a local_repository or a http_repository rule. You’d be surprised to find that the transitive dependencies (ie. the dependencies of the imported WORKSPACE) aren’t automatically included in your WORKSPACE; you need to explicitly import them again.

Bazel recommends that you set up your dependencies in a separate deps rule and explicitly execute it both in your project’s WORKSPACE and any other dependent projects’ WORKSPACEs. I can’t figure out why this was ever thought to be a good idea - what’s the point of a WORKSPACE that just won’t build because it’s dependencies aren’t present?

Bad: Golang Support is Wonky

Bazel runs headlong into Golang’s own packaging and build idiosyncracies, which results in them not playing nicely. In the Bazel team’s defense, I can see that they’ve done considerable work to make them get along, but there are still some annoying rough edges. Consider this: Golang expects all .go files in a given subdirectory to be under the same package. As a result, Bazel is forced to follow this convention when writing a BUILD rule for the subdirectory: all the files in the subdirectory is combined into a single rule called go_default_library, which is the only rule that you can import. So you can’t subdivide your package into separate BUILD rules all under the same directory; Bazel will complain if you do so.

Another annoyance is transitive dependencies (yes I covered it before, but this one is more insidious). For any go_library rule that depends upon a go_default_library from another project, you also need to list all the dependencies (both direct and transitive) as direct dependencies of your rule in the deps section. Yes - that’s right, all of them; there is no automated dependency resolution for imported go_default_library. This is super annoying, and completely goes against the whole “ease of use” paradigm. Bazel helpfully provides gazelle, a separate tool to help identify and automatically rewrite your rule to include these dependencies, but it seems more like a band-aid than a solution.

Bad: Documentation is All Over the Place

While the core Bazel documentation is pretty extensive and helpful in understanding the core Bazel rules, documentation for individual language rules are a bit more spread out. For example, documentation for go_binary and go_library rules are in the rules_go Github repository; py_binary and py_library in the rules_python repo; and GRPC’s is not provided directly by Bazel team, and instead in the rules_proto_grpc repository, which is derived from a 3P library stackb/rules_proto, even though GRPC itself is a project that’s directly sponsored by Google.

Bad: Build Rules have Idiosyncratic Assumptions

Bazel’s build rules, as varied and comprehensive as they are, come with their own idiosyncrasies; for example, a _library rule for a given language, doesn’t always behave in a similar manner to an _library for another one. To some extent this is due to constraints beyond Bazel (eg. see the Golang support note above), but it’s also due to differences in style between individual projects, and projects designing things with a view of only their limited set of use-cases without a sense of alignment with the larger ecosystem in the abstract sense. It may be that this is inevitable in an open-source project with community contributions; unfortunately, this idiosyncrasies lead to some frustration, especially when rules written in these projects have to depend upon each other. What’s worse is that the documentation doesn’t quite address adverse interactions between these projects - so you could run into situations where a rule refuses to execute since its dependencies conflict or its assumptions are violated by another build rule it depends on.

I personally ran into this when defining a proto_library and a grpc_library rule in the same project by importing rules_proto and rules_proto_grpc into the WORKSPACE; rules_proto_grpc implicitly includes a tagged version of rules_proto which means that you can’t have a different rules_proto version (that may have some feature you need) in the same project. I don’t think this is a situation that will be resolved unfortunately; it’s the cost of operating in an open-source environment.

So these are some of the wins and pitfalls I’ve come across while using Bazel. I’m sure I’ll uncover more of them as I continue using it, and as the project matures over the years; but on the whole, I’m super excited for the future of Bazel.