86 comments

[ 0.23 ms ] story [ 154 ms ] thread
The author points out the lack of emperor's clothes when he says that the choice of build system is primarily political and not based on technical merit, since politics elevate CMake above Qbs, even though the latter is a superior build system (syntax/UX x performance x correctness).
Somehow I could never get to intermediate level in cmake. I read the book, and still clueless. Getting Cuda to run was arbitrary luck. How does one understand (modern) cmake? Read the source code?
I kinda learned CMake on the fly by trying to (and sometimes failing to) use other external C++ libraries. (I’m still not an expert in it though.) Some of the packages had perfect CMake integration (find_package just works), some had to be manually compiled to a library (via add_library), and some used a custom script for installation such that I needed to write a FindXX.cmake file from scratch. You basically learn by haphazardly throwing yourself against the wall multiple times. “Modern” CMake tutorials will help to a certain extent, but the examples they use only use perfectly modern-CMake-ready libraries; real life is often more cruel than that! (especially when you’re doing 3D graphics and need to deal with all sorts of esoteric legacy systems).
Point 3 downplays just how awful the CMake scripting language is. It's not just ugly, it's needlessly non-standard and unintuitive, full of footguns, and can be an infuriating timesink. You should still probably use CMake, but make no mistake, sometimes it can be absolutely miserable to work with.

I rambled about this recently: https://news.ycombinator.com/item?id=24203172

I always think CMake is so simple, there's nothing to it. It is maybe awkward to write compared to python or whatever, and kinda ugly, but it is very easy to learn, you can do anything with it (eventually), multiplatform, and has several back-ends, like make, ninja or IDEs.

The issue you linked to was changing the environment after running the "configure" step, but the availability of the header that had been newly installed was cached to "not available", so re-running cmake used the cached "not available" instead of checking the system again. Or something like that. Seems like a classic trade off of speed vs correctness. Something like SCons would always check everything and ran super slow, whereas CMake opts for not checking things that are less likely to change and caching the result. That shouldn't be used as a reason to throw away all the other good stuff CMake has :-)

I recently experimented with C++17/20 in CLion & CMake. I tried to organize my project that different namespaced folders could have their own cmake-file and depend on different external libraries (dlls).

I gave up. CMake is just too frustrating to work with. Hard to debug, different API versions, many different ways to do the same thing, hard to understand the abstraction of CMake itself. Total time sink. I will never use that again.

> many different ways to do the same thing

This is a huge problem with too many things - CMake included. Backwards compatibility is often being praised too highly.

Totally understand you. I once tried to use CMake by creating a hierarchy of a bunch of git submodules for my dependencies with their own CMakeLists.txt, and it failed epically. One of the main problems arose when two different submodules depended on different versions of another submodule, which really confused the hell out of CMake.
For me the biggest hurdle is the disconnect between the way CMake and Visual Studio see the world. I still use it but I have my own batch files for preparing the environment, installing the dependencies, and building the CMake files in a directory outside my source tree.

CMake you run to generate files that will build your thing in a single configuration (platform, runtime settings, etc).

Visual Studio wants to have a solution where you can switch between many configurations (platforms, runtime etc).

Yes, I have looked into latest Visual Studio CMake integration but it doesn't suit my needs because it puts the build files in some arbitrary hidden random location on disk.

I've got a CMake project that builds multiple libraries that depend on different libraries.
If I remember correctly I never figured out how to get the target in a subfolder, you need the target to your dependencies.
I don't understand. To do that you just need to pass the subdir with your CMakeLists.txt to add_subdirectory(). Exactly which problems did you experienced?
Looking at it now, I wanted to copy files into the build folder after build step was done with add_custom_command() with copy_if_different and with target TARGET <project> POST_BUILD, but when I would put that in a subdirectory it wouldn't work because there was no target.

   add_custom_command(TARGET project POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
        some.dll
        $<TARGET_FILE_DIR:project>)
> I recently experimented with C++17/20 in CLion & CMake. I tried to organize my project that different namespaced folders could have their own cmake-file and depend on different external libraries (dlls).

What's hard about that? You have a subproject per folder, in each subproject you specify the build targets and dependencies, and then you just specify build precedence in downstream libraries when need.

Exactly which problems did you experienced getting this to work?

CMake punishes you whenever you choose to do anything which in the configure/build step that is not blindigly obvious, adding friction to all the crazy stunts one might dream up.

This a language an evil genius might come up with while trying to do good.

Of course it sucks when you actually need something nontrivial.

Saying this as a heavy CMake user with a strong love and stronger hate relationship with it.

I tried learning CMake and it seemed so simple at first compared to, say, autotools, or even writing one’s own Makefile. Then when you want to do anything other than a simple project, it gets crazy. It’s like it was initially just an informational file format that had scripting bolted on in a haphazard manner so as not to break anything.

Doesn’t CMake not even have a concept of arrays? I remember reading that functions to generate arrays were really just fancy string concatenaters with quote escaping.

Yes, which leads to beautiful things like `LIST_SEPARATOR` [1]. Lists in CMake are just strings with ';' as item separator.

All this is just a gentle slap on the wrist for programming in your build system. Or doing something weird like optimizing a CMake-language JSON parser for performance [2].

1: https://cmake.org/cmake/help/v3.16/module/ExternalProject.ht...

2: https://github.com/sbellus/json-cmake/pull/3

> All this is just a gentle slap on the wrist for programming in your build system

That's an awfully polite way of saying Yes, the CMake scripting language is indeed terrible.

It's meant to be used for complex package-detection logic. CMake Find modules can run hundreds of lines. It's not unreasonable to expect proper support for arrays, proper functions that can return values, a sane syntax, etc.

> Then when you want to do anything other than a simple project, it gets crazy.

I've been using CMake for around a decade and I have to say I never experienced anything remotely similar to what you described.

Do you mind sharing a real-world example of something that you struggled with while working with CMake?

What would you use instead? What does a bleeding-edge modern C++ build system look like? conan + meson?
I'm not suggesting it's a good idea to drop CMake for one of its obscure rivals, that's why I put You should still probably use CMake. I don't want to have to learn an obscure build-system to work on someone's project, and I won't ask others to do the same for me.

To revisit the points I made in the old thread [0]:

We pay a price for fragmentation; it's better we all agree on one system. (Although I think it's fair to say that autotools has had its time in the sun at this point, I don't see a big problem in using it for Unix-only projects.) CMake can work very well, with impressive portability, if a project's CMake scripts are rock-solid. I plan to continue to use CMake, despite its considerable shortcomings.

If CMake did the following things, I could probably forgive the atrocious scripting language:

1. Overhaul the documentation. Every unit of documentation should be accompanied by an illustrative example; dry rambling essays are not the right format. Do what Microsoft do with their .Net documentation.

2. Official design-patterns for common tasks like Find modules. They shouldn't each have a character of their own. These scripts ought to be very consistent, very boilerplate-driven, and very boring.

3. Rework the officially bundled CMake scripts so that they all follow the current best-practices to the letter. As it stands, there's nowhere to go to see what real CMake scripts are meant to look like.

I would suggest a linter/improved errors and warnings, but I don't know if that's practical. I would suggest replacing the entire scripting language with something less awful, but it wouldn't do to throw out compatibility. Stability is a must-have for this kind of thing.

[0] https://news.ycombinator.com/item?id=24209573

CMake might be the best thing we have in C++, but god does that fact tell you what we have to work with. I've never delved far into CMake, but I regularly make two observations that make my shy away:

1) Every time some "guide" gets posted on the programming communities, there's people pointing out mistakes, deprecated functions or alternatives.

2) If you look at the CMakeLists.txt files of various popular libraries, they all look different. There's some commonality but much less than what you would desire from a more or less standard build system.

Maybe what we need is a CMake linter with rules for some strong default opinions.
What we need is CMakeCMake. A special CMake for building CMake rules with its own special meta-language.
There is a cmake linter. I use it for some of the projects I inherited which still use cmake.

But cmake is a joke really. Overly simplistic, but for anything close to a real project compared to autotools, autotools is much much better. With autotools you get tons of constantly improving standard probes and fixes. Cmake is only for really trivially small projects.

I just added cross-compilation support for my aarch64 phones. It was doable with cmake, but it's a huge mess. With autotools it would have needed no changes at all. For cmake I need seperate CmakeCFG files. Not even talking about adding GPU support. Or valgrind. Or proper testing.

1) always comes up in HN and it’s totally hilarious. Seriously I do not know (and do not care) what is the “right” way to write CMake anymore. It’s kinda like C++ where everyone has their own standards for writing code (think of Orthodox C++ vs Modern C++) but the consensus is even more scattered out in the case of CMake.
> CMake might be the best thing we have in C++

CMake is not the best build tool for C++, in a technical sense (pretty much every hash-DAG based tool like Qbs or Bazel is better), but it is the most common tool, which has little to do with merit.

(comment deleted)
The real reason is that CMake became a dramatical better tool just before version 3.0, and it has been improving since.

That's what people call "modern CMake".

The scripting language remains awful, though.

Modern CMake still doesn’t have local variables or any sort of scope, functions cannot return values, and there is no thing such as a dictionary datatype. You gradually accept the shortcomings of the language though, in a typical 5-stages-of-grief fashion.
It's more of a sixth stage of grief where you discover your tragically deceased beloved relatives left you a very nice sum in their will.

The language is terrible, but there are things CMake does pretty well.

(comment deleted)
CMake is a hell for medium/large projects.

Builds are not hermetic/reproducible. When doing deep change of CMake files, the only reliable way to verify it is correct is to clean and rebuild everything.

CMake is very very slow. A minor change in CMake file results in CMake re-evaluation which is very slow for large projects.

CMake language is just pain.

And CMake does not have cache. CCache helps a little, but it’s still much worse than cache of proper modern build system.

I would probably recommend Bazel for building C++. Works like magic. Although if you hit Bazel limitations (likely you won’t but still), you don’t have many options how to deal with it. You never need to clean Bazel cache whatever you are doing (unless you are intentionally trying to break it of course).

> CMake is a hell for medium/large projects.

I agree. And if you let me, I want to add that for small projects it is ridiculous overkill.

This leaves no reasonable use case for cmake (except maybe building itself solipsistically for all eternity). And I like it that way.

CMake was a big improvement over make and autoconf years ago, but now it’s time to admit that it was a step to a wrong direction, dead end, cannot be fixed. CMake model is wrong. CMake just need to retire.
What is the good model?
Bazel’s:

* each build file is independent (unlike CMake where the project is one big script with global scope and side effects)

* build script evaluation is hermetic (cannot read time for example) and strictly reproducible

* target is rebuilt not by timestamp changes (which is unreliable for several reasons), but instead all dependencies must be declared, and target is rebuilt when the hash of dependencies change

> each build file is independent (unlike CMake where the project is one big script with global scope and side effects)

I don't understand your point. CMake projects specify build targets per module, and it's up to the developer to choose what he thinks should be a module. What value do you see in specifying build targets per file?

I'm not saying about specifying build target per file.

I say when cmake file includes another cmake file, all variables are inherited, and in certain cases cmake file can define variable in outer cmake file scope.

Thus build files are dependent on each other. Inclusion order is important, and when project refactoring/restructuring, it's a common error, that resulting project is incorrect because for example, some file relied on the fact that when it is included, certain variables or macros are already set by someone else. This makes large project maintenance quite inconvenient. Especially because CMake does not guarantee that error will be revealed unless it is full clean rebuild.

> I say when cmake file includes another cmake file, all variables are inherited, and in certain cases cmake file can define variable in outer cmake file scope.

This is not true at all. You only "inherit" variables that you intentionally want to pass to downstream projects. You need to explicitly and intentionally declare your variables with PARENT_SCOPE to access variables from other files. That's an explicit decision from the developer.

> You only "inherit" variables that you intentionally want to pass to downstream projects

Yep, most project variables, except for some locals.

> You need to explicitly and intentionally declare your variables with PARENT_SCOPE to access variables from other files.

No you don't. PARENT_SCOPE is needed to write variables into parent scope, but not read.

> That's an explicit decision from the developer.

Even so. Yes, when working very carefully, when you are a single developer writing on a small projects, and enforce code style, it is possible to keep the mess manageable.

When at least 100 people work on the project, if a project is several years old, if half of the team changed, CMake project is always a mess. (Source: I worked in two very different large companies which were using CMake; both of them eventually migrated away from CMake).

Someone might blame developers for making that mess. I'd prefer blaming CMake, as I see the same developer keep projects in good shape when using Bazel (or another build system with similar model).

> No you don't. PARENT_SCOPE is needed to write variables into parent scope, but not read.

You're talking about an entirely different thing then. Subprojects do inherit scope, but that is only relevant if you intentionally, and for some reason, want to reuse variables that were inherited. This is often used to propagate config settings from the project root to all submodules, but you only do this if that's something you explicitly want to.

> Even so. Yes, when working very carefully, when you are a single developer writing on a small projects, and enforce code style, it is possible to keep the mess manageable.

It really isn't. If you're dealing with a problem where random team members break stuff without oversight or awareness of what they're doing, and if this happens so often that you start to deflect blame at tooling, then you're not dealing with a build system problem. You're dealing with poor software engineering practices that not only goes way deeper than the build system but also cannot be fixed by replacing it.

> When at least 100 people work on the project, if a project is several years old, if half of the team changed, CMake project is always a mess.

It really isn't. What's always a mess under those circumstances is unmaintained code developed without any care or oversight, which just builds crust by accretion. The choice of build system has no influence on that outcome, not does the problem stay circumscribed to the build system.

As the saying goes, bad workers always blame their tools. Well, we can extend that saying to badly managed teams.

> The choice of build system has no influence on that outcome

Yes it does. When build system prevents shooting in the foot (by being very strict about side effects of submodules in particular, and forcing to explicitly declare dependencies), the result is better project quality.

> As the saying goes, bad workers always blame their tools.

Sure. Just let's not jump to conclusion that everyone who criticise tools is bad worker.

> CMake was a big improvement over make and autoconf years ago

I would not even grant this, and I spent a lot of time wrangling both CMake and autotools many years ago.

What is the model of CMake. Probably I could use it better if I knew (I couldnt figure it out).
Basically it is one big script (with globals and side effects) which generates makefiles.

(Even if it does not generate makefiles, the project model is makefiles: targets, file names, timestamps.)

So the globals and side effects are the problem or the generating makefiles part?
Both are the problems.

Because of side effects, when single cmake file modified, the whole project need to be regenerated.

Also, because of side effects, project files become too frigile, for example, changing the order of two includes may result in incorrect project definition.

And makefile generation part is problematic: using timestamps to check if something need to be rebuilt is unreliable (may result in not rebuilding something when project is actually changed).

Isnt the makefile generation the only reason one uses cmake?
It is also used to generate project files for various ides. Also, (but I’m not sure), cmake have some built in make which does the build like mske would do but without generating makefiles and invoking real make.
>I agree. And if you let me, I want to add that for small projects it is ridiculous overkill.

I don't think it is. On Windows at least, it's still easier to build a VS project links with libraries and everything linked by editing a text file than it is to go through the GUI, and VS is worth using as a debugger if nothing else.

I concede that cmake makes windows/VS portability easier. But that's probably the only advantage I can see, to be weighted against all the other shitty stuff. Anyhow, for C programmers this is a non-issue since VS does not compile C.
Spinning up JVM just to build a C/C++ project? No thanks. At least CMake is lean enough and native.
Is this is your only argument against Bazel, I consider your comment a praise of that build system.
Is it possible to use Graal's ahead-of-time compilation with Bazel? Or perhaps something like Nailgun? ( http://www.martiansoftware.com/nailgun/background.html )
It already does that. On first run, Bazel spawns a background process, which does all the work. In particular, it caches all the state (e. g. file hashes, parsed build files etc.)
Thanks for the reply. How does its package-detection stack up against CMake? One of CMake's advantages is that there's a Find module for most well-known libraries.
CMake really need beginner proof documentation with clear information about what should someone care about when they start, and what is more advanced. I have experience with a lot of build systems in various languages, but to be honest I had a hard time trying to learn how to use CMake for simple projects that depends on a small number of libraries. I started writing down what I’m learning, and the result is a huge document with way too many details I shouldn’t be introduced to before I’m doing something way more complicated.

And that was both time consuming and frustrating.

QMake no longer exists and the replacement is CMake. But the important thing here is that it is the only option. No other build system is a viable alternative for C ++ developers, because:

- It is the industry standard

- Dependency managers like vcpkg and conan use it

- Is what Qt selected after killing Qbs

We can discuss the many problems of CMake but in the short and medium term, the best solution is to focus on improving CMake

On a similar note, I used to loathe Maven, coming from a well behaved use of Ant + Ivy, and nowadays exactly for those reasons (and not suffering from XML allergy) it has become my Java build system when it is me calling the shots.

Point is, stick to a tool and improve it, as you say, we don't need fashion in build systems as well.

I've been programming C++ for 25 years and never had to use CMake in any professional context.

I don't think there's any point in using CMake if you're not forced to support Windows.

QMake hasn't actually been killed off, they're still maintaining it but there are no real updates or anything AFAIK.
I really recommend people look into using Hunter to manage CMake dependencies. It's just a much more correct solution than the alternatives b/c your code and all your dependencies get built reproducibly with the exact same toolchain file. You also get things like namespaces and you eliminate weird issues with multiple versions of dependencies

I've compared it to other alternatives (though I wrote this ages ago and it needs an update): https://geokon-gh.github.io/hunterintro.html

I haven't used vcpkg - but the fact it's forcing its own toolchain file is a huge red flag.

CMake is actually really not bad as long as you stick to a subset of the language (ha! sounds like C++) and just don't deal with the whole installer side of things and generator functions. That said 99% of CMake files are written incorrectly. CMakeLists.txt should be system agnostic and compiler/toolchain agnostic. It just coordinates the linking of libraries and where to find headers. All system specific magic needs to be in the toolchain files. Once you break that rule things quickly spin out of control and you get crazy CMake configuration that are a nightmare to maintain

> That said 99% of CMake files are written incorrectly.

I see this repeated over and over since years. At some point if everybody is doing it wrong, either the tool is the actual issue, or there is a clear problem of miscommunication somewhere.

In CMake, there are many ways to do it, most of them deprecated, and most of them poorly documented.

If I recall correctly, even the official Find... modules rely on deprecated functionality.

In any long lived project eventually you discover a better way and regret the past. New tools that don't have a history don't have this problem, but new tools eventually become old and have some dark corner they regret.
Package managers like Hunter are no use for me if external dependencies that aren’t in the package manager registry (which is typical for my field) can’t be integrated easily. This is precisely the problem your blog post describes at the end.
Yeah, that's the part I need to update :) It looks kinda intimidating but it actually ended up being not bad at all. The Hunter docs are just not great unfortunately. They're similar to the CMake docs - very terse and to the point and they totally make sense once you already know everything

Basically you just fork Hunter and add your own package with it's own URL and hash and then it works. Just take a look at how the other packages are organized and it'll quickly make sense. You point your project at your fork instead of the "official" Hunter repo and voila. I ended up having to do it for a couple packages and there was no problem

I think there was a bit of a mental block for me at first b/c it feels like you will need to "maintain" your own fork. But this is dependency management - there is fundamentally no maintenance. You only update things when you decide to update dependencies. This is always a manual and deliberate process. It's just like how your Hunter version doesn't just magically grab the latest version of Hunter. It's also pinned to a certain release version number

The system is very well thought out..

> Also, many people hate MOC. This is the pre compiler that compiles your Qt C++ into regular C++.

MOC is not a pre-compiler but a code generator; all code written by the developer is compiled as is, but it can include macros known to MOC for which additional code files are generated. This goes without notice when e.g. using qmake or cmake. No reason to hate it.

Would be nice if they will keep the main logic but rework the language syntax, parsing rules, etc.
Every time I needed to look at adding something to the build system at my old job, in CMake, I booked a day. What is incredulous to me is the numerous answers on the forum and stackoverflow are never ever complete enough to be useable. All they do is give you a hint. If you want to do something, say, add some existing external library with partial cmake support built in, you check endless posts with hints until, at the end of a wasted day, you add one or two lines to your CMake files and the job is done.
(comment deleted)
(comment deleted)
This article completely glosses over the shortcomings of CMake. It follows a fundamentally flawed design and the sooner the C++ community manages to migrate (probably to Bazel?) the better off we will all be.
CMake is horrendous.

There was a reddit AMA when someone asked what the dev would change about CMake if back-compat wasn't and issue, and the dev said that hadn't thought about it. To me, that's the nail in the coffin: all engineers should think about how things ought to work, even if they cannot immediately fix it.

Just use Meson and leave this POS behind.

> To me, that's the nail in the coffin: all engineers should think about how things ought to work, even if they cannot immediately fix it.

This has always been a bone of contention between me and my peers or leads: I always intended to drill down to the root of the problem to understand how a system should behave, while most of the time they considered it a waste of time and frustrating. "Patch with a workaround and call it a day" mentality is ubiquitous nowadays, from what I see.

Well, it depends where you work, though perhaps I am a bit of an Oasis.
CMake is used by LLVM, and I wish it would use something with a better syntax.
I am a heavy CMake user (as well as Ninja, Maven, SCon, make, Gradle, premake, MakeMe, Buck, Meson, Ant and parallelized bash build tool),

I’ve used CLion and PyCharm as my primary IDE but never used CMake on them anymore after a few aborted attempts. While not the IDE’s fault, CMake is that bad, starting with a “build” subdirectory. The bug report I filed with JetBrain/Idea (IDE creator of PyCharm/CLion) to make it work with Make/autotool is now the oldest bug over there.

I’ll take Make and autotool/autoscan any day and StackExchange for package identification of weird dependencies.

I use QMake and it's painful as hell for certain things. If you want to set certain options globally (e.g. where to look for external libraries that you don't want everything to link against), you end up having to include files all over the place just to avoid duplicating things.

Building multiple executables is not possible in a single .pro file, so if you've got a folder full of unit tests, which are separate applications, you have to create a .pro file with subdirs as the author here does, and then create another pro file for each test executable.