CMake vs Bazel

I have had the (mis?)fortune this week of reading and writing a lot with both Bazel and CMake, the most production-ready C++ build environments other than Make (which of course, has its issues). I have been using CMake for my galvASR project. Meanwhile, I used Bazel to add an existing Tensorflow op to the contrib/ of Tensorflow. (We shall see whether that code sees the open-source light of day; there are non-technical hold-ups.)

This post focuses on C++ mostly, although both of my use cases have used these build systems for Python as well.

Here is a collection of thoughts I’ve had while using both:

  • Bazel’s documentation is small. Exceedingly small and built on some fairly simple principles. This is a very nice thing for an experienced C++ developer. I understand that this can be troublesome if you’re not already experienced with build systems. Meanwhile, CMake is a monstrous mess of documentation, and I still don’t fully understand it. Still, it is nice that when you Google around, you will find an answer, and failing that, there is probably a CMakeLists.txt sitting on Github that you can adapt.

  • Bazel does not support ccache. Argh!!!!! I understand Google has its own very specific infrastructure for distributed C++ builds, but not supporting the common case (trying to speed up local builds as much as possible) really is disappointing. For that matter, I don’t think Bazel supports precompiled headers out-of-the-box (though I haven’t needed this).

  • Bazel does not allow you to take apart the various components of a dependency, like CMake does. i.e., in CMake, typically when you Find(MyLib), the variables ${MyLib_INCLUDE_DIRS} and ${MyLib_LIBRARIES} get defined. This means you can compile against that libraries headers, but choose not to link against them. When would you ever do this? The answer is never, until you need to do it. A rather pernicious bug in Tensorflow when creating your custom ops is caused by linking its libraries to your op’s shared object file, which causes specific global register constructors to be run twice. See here.

  • Bazel has a lot fewer examples than CMake. For example, the documentation on how to depend on non-Bazel external libraries (probably a very common case for those outside Google!) has one example for Java. There is not even an example for C++ and C! Oh dear! (Arguably, C/C++ is a more important use case than Java since the Java build ecosystem is quite mature at this point with Gradle.) Getting an external C++ library working in Bazel requires that it be pre-built (CMake, meanwhile, allows you to make the build process a part of your CMakeLists.txt) and requires an understanding that - this is confusing for noobies like me - your libfoo.so file should be declared as part of the “srcs” (sources) of your cc_library. This is not hard to figure out, but it’s harder than copy-pasta’ing some code.

  • Bazel tells you about headers you include that have not been declared as dependencies! This is the killer feature! I’m guessing the original motivation of this was to make it easy to know what files were required to do a build on a remote machine for distributed C++ builds, but it’s really great to see a cleaner way of documenting dependencies in C++ this way.

  • Bazel has static typing. In CMake, an undefined variable (e.g., foo_INCLUDEDIRS instead of foo_INCLUDE_DIRS) becomes an empty string. Bazel is the clear winner here.

Overall, I would recommend sticking with CMake, but I do think Bazel holds a lot of potential, and I’m happy that Google open-sourced it. I may consider using Bazel for a project in the future.