Why CMake sucks?
CMake has become a de facto industry standard as a natural ancestor superseding autotools. But is it actually an improvement? Personally, after spending signifficant amount of time with CMake projects, I’m inclined to conclude that no, not really. Below, I present why CMake sucks and why you shouldn’t use it for any of your projects.
Just as a disclaimer, I’m presenting my personal views here, which are very subjective. You’re entitled to have your own opinions. You have been warned :).
Autotools hell
CMake took the world by storm, because compared to autotools, it’s been significantly simpler to deal with and faster. Can you imagine that prior to CMake and a single CMakeLists.txt file, defining all the build rules you’d have to perform the following:
|
|
This is insane and I didn’t even mention any file editing required. Try to memorise all of that. After all these shenanigans here’s a list of all new files that autotools created:
|
|
Exceptionally… bad. Compared to all of that, CMake indeed is a blessing. You’ve got a single text file and no massive boilerplate required to achieve most fundamental stuff so, why do I complain?
CMake language is bad
Yes, it’s like a vimscript of build system languages. Inconsistent, confusing and without a reference manual at hand all the time, impossible to memorise (at least for me).
Weird statements
Does declaring conditional statements which resembles function invocation looks intuitive to anybody?
|
|
Same applies to function definitions:
|
|
Weird function/macro invocation
What the hell is this (?):
|
|
Oh boy, so many questions. Amongst many:
- Where do I place the keywords, i.e.
DESTINATION
, can it be anywhere, is its position well defined? - Where are the commas, are they needed at all, what’s the argument delimiter, are new lines important?
- What’s the set of accepted keywords (how do I even check that without having to fallback to documentation or inspect the source code myself for a given function/macro)?
- Does a given keyword accept a single argument or multiple ones?
- Which keywords are mandatory and which are optional?
- Does the order matter?
- What types am I expected to use after any given keyword?
I’m not sure what the motivation was during the design phase but this is simply unbearable. In big projects with lots of custom functions and macros it literally brings the development to a halt without having to constantly check examples (if any), documentation (if available) or attempted usage somewhere else. Many times, you have to deal with a cobweb of custom functions like that and before you even get to the code itself, you all of a sudden notice that you’ve wasted an entire day trying to understand the basics of how it gets built.
Weird function definitions
The invocation is a mess but wait, there’s more. Defining a function is even less appealing. Following CMake’s documentation to actually be able to use arguments in a function like my_install
, used as an example above, the arguments would have to be parsed, otherwise you’d be just dealing with unstructured set of values so, a typical function preamble looks the following way:
|
|
On first glance, it’s impossible to decrypt that without, again, spending some time with the documentation. After a while, it becomes apparent that MY_INSTALL
is a variable prefix and cmake_parse_arguments
will parse “options” (which in this case are value-less keywords or nullary keywords or whatever you want to call a set of flags), “one value arguments” - name is self explaining, in case of this example it’ll be i.e DESTINATION
, and multi value keywords like a list of files or targets. But wait… there’s more, it’s not returning a structured set of parsed data in a form of a dictionary or something similar, like you’d expect from any sane language, instead it’ll define a set of MY_INSTALL_
prefixed variables containing parsed values in the current scope. Yes, they’ve introduced a prefix for the sake of macros, since unlike functions, they don’t define their own scope. It feels like a workaround introduced as early as the design stage! So, quoting the documentation, after invoking my_install
the following way:
|
|
You’ll land up with these new variables within your function scope (or global scope in case we’re talking of macro()
):
|
|
Lovely!
CMake errors suck
So, imagine you’re getting a package from a 3rd party that you wish to integrate with the rest of your system. You try to build it and:
|
|
or:
|
|
So, again you have to go the online manual and decrypt the meaning. It seems that the semantics of CMake language have changed in regards to variables dereferencing. CMake, by default, is still maintainig the old behaviour and warns about the change. Seems good on first glace, doesn’t it? Well, kind of. The fact that the language semantics changed is a bad thing in the first place. Even though the old behaviour is still maintained, it is now deprecated, which means that new versions of CMake are very likely to remove it. Yeah, that’s fine you may say. You’ve got time to upgrade. Well, no I don’t - I’m not the maintainer of this package and licensing restrictions may forbid me from touching it whatsoever, which means that if newer CMake removes deprecated behaviour, I’ll have to have TWO versions of CMake in my system. The new one (whatever it’s going to be) and the old one, just for the purpose of building that one particular package.
Weird dependencies
CMake comes with its own curl
implementation - cmcurl
. It wouldn’t be that strange, since it provides ways to deal with remote source code repos. The consequence of that is all of a sudden, your build system requires openssl as a dependency:
|
|
Using cmake sucks
Do I really have to type:
cmake -DCMAKE_INSTALL_PREFIX=... .
instead of just --prefix
like a normal person? Yes, autotools do suck immensely but there were some good parts, like a reasonable variable names to name one :).
Anatomy of a CMakeLists.txt file
I’m gonna try to prove that CMake
is not necessarily the panacea we’ve been all looking for. Let’s work with an example. I’ve got a very simple library that I wish to build with cmake:
|
|
Simple stuff. One header file, one c++ source file. I need a CMakeLists.txt
. I’ll create one with the following content:
|
|
Nice and simple. But this’ll just build the library. I still need to install it. Okay, fair enough:
|
|
But, this only installs the target and I still want to install the headers. No problems there:
|
|
Cool. Job done! Well, no, it’s not. How will the client code find my library? I can just add a pkg-config file but CMake
comes with CMake
packages instead so, it must be better? Sure, let’s try that. First thing first, the target installation has to be slightly altered:
|
|
Here I’ve defined a package export. Clients of my library will be able to find it as a package and all targets that it exports. Cool, am I done? No… The target itself has to be installed as well:
|
|
You might have noticed that I’m using these cool variables CMAKE_INSTALL_LIBDIR
to denote paths in the system. Well, they don’t come for free. I had to include a package for that:
|
|
Am I finally done? Nah :). I still need to create package config files so, the clients can just find_package(lib1 0.1 EXACT)
. But wait! How to incorporate the version information? I just need to add some cmake magic:
|
|
Am I finally done? Well, no! There are references to a strange file Config.cmake.in
. Yeah, skeleton for this one has to be provided as well:
|
|
It must be over now, right? No, it’s not. I’m installing the headers, but the clients have no idea about these paths so, these have to be exported as well:
|
|
There you go! To bring a simple library to a production quality which other people can actually use (or even yourself, as part of a bigger project), I had to write 48 lines of CMake code. Here’s the file in its entirety:
|
|
… and the funny thing is that some of you will still be able to find many things wrong with this code.
Dealing with more complex projects
This is the biggest issue, in my opinion. Software stack is rarely comprised of one project. There’s a set of projects, interdependent on each other and there’s a need to express that dependency. All of a sudden you can’t rely on CMake
package files to find dependencies in one of your projects because the dependencies themselves are not yet built and the package files are missing. Consider the following:
|
|
I’ve got a very simple project which uses lib1
. The CMakeLists.txt
looks the following way:
|
|
My stack has two projects lib1
and exec
. exec
directly depends on lib1
. lib1
has to be built first then. Here’s the “stack” overview:
|
|
Of course this is greatly simplified and contrived example. In reality there would be a lot of libraries each coming with their dedicated unit tests and potentially interdependent on one another.
If you build lib1
yourself manually and then follow to exec
then obviously, everything will be fine. Nobody wants to do that though and it’s obvious that the desire is to have a global CMakeLists.txt
to manage all dependencies at once. So, I’ve got a global CMakeLists.txt
where:
|
|
Seems logical and one could assume that CMake
will figure out the dependencies between the targets itself but, it will not. CMake
build model involves three stages:
- configuration stage
- generation stage
- build stage
During the configuration stage, CMake
goes through all CMakeLists.txt
and tries to determine dependencies, configure file templates etc. Since global CMakeLists.txt
acts as a single super project aggregator, the implication is that it’s impossible to enforce build order that way for projects so, the dependencies can’t be incrementally satisfied, since during the configuration stage, all package exports and their files would have to be already present. This is a major bummer.
During the configuration stage all package files have to be installed, otherwise exec
will complain (rightly so) that lib1
cannot be found. So, to actually make it work you have to resort to hacks like:
|
|
Where exec
is build in two modes. When it’s a standalone package, built out of tree, it tries to locate all its dependencies by looking for the package files. When it’s built as part of the entire stack, it is just directly linking lib1
target. In other words, you have to simulate a mode when all targets are built as part of a single project. This is a terrible hack! It can be achievable if you’re working alone and don’t depend at all on any 3rd party components. If you do, you’ll have to patch them all, just to manage the dependencies.
Other option is to create a set of custom scripts with build order defined but that’s even worse. You won’t be able to rebuild quickly since the change detection has been compromised that way (you’ll be going through all projects all the time).
UPDATE 2024-02-12
ExternalProject is a mitigation to solve that problem in a cleaner way. I’ve wrote a short post about it which you might be interested in.
UPDATE 2024-02-23
Recently I’ve been playing around with conan as a package manager in my cmake projects. I was positively impressed with it. You can find out more in this post.
What’s the alternative?
This is gonna be a short advice. For the love of God! Do yourself a favour and don’t use CMake! I strongly suggest to transition to meson, or bazel but that’s a topic for another post. Hope to see you there.