C++ build systems: our transition from CMake To meson
How we got to this decision and what we got out of it.
We develop our main product, the Cranmera MPC engine, in C++20. (Why we chose Modern C++ and not, for example, Rust, is a topic for another blogpost.) Because C++ doesn’t come with a default build system like Rust’s Cargo, it means that it is up to the programmer to choose an appropriate build system. (Strictly speaking, I should say ‘build system generator’, as Ninja is the actual build system that we use, but let’s not get into that today.)
This choice is of course not just a disadvantage; in that you actually have a choice. As CMake is quite popular in the C++ community these days, I had been using CMake for several years now. (And before that I used GNU’s Automake.) Anyway, in the beginning, I was satisfied with my choice for CMake, although I never had the feeling that I really mastered CMake.
"I want to have that ultimate control also over my build system"
In my previous role as academic researcher, I mainly wrote scientific code for which the build system played a minor role. I liked to view the build system as a part that you ‘mess around with it until it works, and then keep your hands off it’, which served my purpose well enough at the time. Now that I have the role of CTO in a privacy-technology company, I bear much more responsibility for the quality and robustness of the developed codebase.
Even before you have to choose a build system, there is the choice of the programming language for a new project. In a nutshell: choose the right tool given the problem. For us, that choice happens to be C++, which gives us ultimate control. (There is a price to pay for this level of control, namely that you become responsible for managing all resources within the program, and for protecting yourself against the footguns that any language with low-level features happily offers you.
The purpose of having all that control is that it should ultimately pay off in terms of better performance: smaller memory footprint, faster code, etc., though it comes with the price of extra cognitive load during development.)
I want to have that ultimate control also over my build system, but as I already indicated above, with CMake it never felt like I had that level of control.
Coming from CMake
Recently, we have migrated from CMake to Meson, and in the rest of this blogpost I want to tell the story of how we got to this decision and what we got out of it.
The main reason for migrating our build system, is that I wanted to prevent the scenario where some parts of the codebase, of which you actually know that those are a bit messy (in this case, our CMake scripts), suddenly start playing up at a very bad moment, for example just before the deadline of shipping a new product.
If you have some engineering experience, you will agree that the more subtle kind of technical problems like to manifest themselves in atypical situations or corner cases. What I mean, in terms of a build system, is that you won’t notice the actual quality of your build system scripts until you start doing special things, like porting to other OSes, architectures and cross-compiling. At that point you will be penalized for every ‘hack’, where you did not follow the idiom prescribed by the makers of the (build) system. And where are those hacks actually located? In your own build scripts, or in the “FindXXX.cmake” scripts you found somewhere on GitHub?
Then there is the topic of “Modern CMake”, caused by the paradigm shift that took place a few years ago in CMake, due to which we now have the “old” (read: “wrong”) directory-based style, versus the “modern” (and recommended) target-based style. “Modern CMake” is probably coined to make us think of “Modern C++”, the latter being the novel style of C++ that encourages the use of updated and less error-prone language and standard-library constructs.
With respect to the language C++, I value the backwards compatibility guarantees that C++ offers, and I agree that those backwards-compatibility benefits make up for the ugly parts of its syntax. With respect to a build system, however, I am not convinced of the value of such backwards compatibility, and in my opinion, a clean syntax and well-thought-out structure is much to be preferred.
By the way, googling for “modern CMake” yields many blog posts in which authors explain the intricacies of Modern CMake. One could view the abundance of such stories as a positive thing, though I can’t help regarding it as an indicator for the poor style of CMake’s own documentation. Compared to that, Meson’s documentation, together with its design, simply feels ‘right’.
Before deciding to switch our build system, I have tried to convert our CMake scripts to the “Modern CMake” idiom and polish them up. After putting some work in it, meaning reading CMake docs and changing scripts, I was still not satisfied with the result. Probably it was my own fault; I still did not yet fully understand CMake’s new idioms, and as a consequence our build system setup still felt brittle. On the other hand, given that our build system requirements are not that complex, why should I invest more time to become an expert in a system (CMake) that I anyway appreciate less the more I read about it... What about alternatives?
Before choosing Meson, I also considered build2 by Boris Kolpackov / Code Synthesis. Build2 is famous for being one of the earliest build systems to support C++20 Modules, and based on its descriptions and the author’s talks (e.g. at CPPCon) this system seems to be well-designed, more advanced than most other build systems, and to have a lot of potential. However, given my limited amount of time, I was unable to quickly figure out from its documentation how I could accomplish common tasks, like linking with libs that are not included in the related cppget.org repository, such as Boost. I think improving documentation-for-the-impatient is something where Build2 could improve. BTW, I am curious to see how Meson’s approach to dealing with C++20 modules will develop; we do not yet use them in our project.
I do not want claim that our build system requirements are very complex, though we do need some special setup:
- we need multi-compiler support (Clang for debug and GCC for release builds);
- depending on the use of Address Sanitizer, or Valgrind, we need differently compiled versions of some Boost libs (Boost.Context and Boost.Fiber); and
- we use a some generated code, which originates from Cap’n Proto, a protobuf-like library for object serialization.
With Meson, all these were easy to accomplish (for the third I use a little helper Python script), and, maybe even more importantly, in such a way that I fully understand what is going on under the hood.
Furthermore, some personal favourites about Meson are:
- the concept of a dependency, which lets you bundle libraries with include paths, and the different ways to obtain such dependency (automatic vs. manually)
- one does not have to specify header files in the executable target to make IDEs happy
- integration with pkg-config and CMake, to locate system-installed libs
- easy-to-manage configuration options
- out-of-the-box support common stuff (sanitizer support, coverage builds, stripping binaries, etc.)
- easy-to-extend while maintaining transparency
- parallel unit tests
A key benefit of a good and transparent build system is that it will save you time, and that it makes it much easier to use helper tools that will improve code quality, like Clang’s static analyzer, Address Sanitizer, coverage builds, etcetera. We are very happy with Meson, thanks Jussi Pakkanen!