When age fell upon the world, and wonder went out of the minds of men; when grey cities reared to smoky skies tall towers grim and ugly, in whose shadow none might dream of the sun or of spring's flowering meads; when learning stripped earth of her mantle of beauty, and poets sang no more save of twisted phantoms seen with bleared and inward-looking eyes; when these things had come to pass, and childish hopes had gone away forever, there was a man who travelled out of life on a quest into the spaces whither the world's dreams had fled. — H.P. Lovecraft
I spent the better part of my off-hours last year rewriting Halide's CMake build.
I knew CMake had a polarizing reputation, but I needed to make Halide work easily on Windows. The existing build didn't work right in CLion, it couldn't find its dependencies (except on CI, somehow), and it didn't produce usable packages. I figured I'd roll up my sleeves and get to work, and so I started where anyone else would: by Googling "CMake tutorial".
I was nearly stricken blind.
There is so much bad information about CMake out there. It's pervasive. It's high in the search results. Just about every StackOverflow answer is out of date, wrong, or both. Heeding any of this advice will send you and your project careening down a road to madness, paved into the earth by the sweat and tears of those who have tried to port a project that hard-codes a library path.
If you don't want your builds to break, and your crops to die, you should learn to use CMake properly. This is the first in a series of blog posts that will attempt to teach you to use CMake effectively. My earlier post about whether CMake is a build system could be considered part 0 of this series.
So without further ado, let's talk about the most basic decision to make: what version of CMake to use in the first place.
If you're writing an open source project, you most likely want to make your code available to as many users as possible. So you might assume that you want to use a very old CMake version to build your project. This is nonsense. Recent versions of CMake are available absolutely everywhere. Your build's users are technical: C++ developers, not laypeople. They can upgrade CMake if for some reason they haven't yet. For every major platform, there are easy ways to get a recent CMake version installed and kept up to date. Don't believe me? See the table below.
OS | Arch | Source | Version | Update Process |
---|---|---|---|---|
Windows 10 | x86, amd64 | Visual Studio 2019 | 3.19 | Updated occasionally through VS installer |
Windows 10 | x86, amd64 | Chocolatey | newest | choco upgrade |
Windows 10 | x86, amd64 | Kitware MSI | newest | Manual |
Windows 10 | x86, amd64 | Kitware ZIP | newest | Manual (no installer) |
macOS 10.14+ | universal | Homebrew | newest | brew upgrade |
macOS 10.10+ | universal | Kitware DMG | newest | Manual |
macOS 10.10+ | universal | Kitware TGZ | newest | Manual (no installer) |
Ubuntu 16.04+, many other distros | x86, amd64, aarch64, armhf, ppc64el, s390x | snap | newest | Fully automatic |
Ubuntu 16.04+ | x86, amd64 | Kitware APT | newest | sudo apt upgrade |
Ubuntu 20.04+ | x86, amd64, aarch64, armhf | Kitware APT | newest | sudo apt upgrade |
Ubuntu 20.04 LTS | x86, amd64, aarch64, armhf, ppc64el, s390x | Ubuntu APT | 3.16.3 | sudo apt upgrade (security only) |
Linux (Generic) | amd64, aarch64 | Kitware TGZ | newest | Manual (no installer), only depends on glibc6 |
ALL | x86, amd64, aarch64, armhf, ppc64el, s390x | pip | newest | pip install -U cmake |
I can't stress this enough: Kitware's portable tarballs and shell script installers do not require administrator access. CMake is perfectly happy to run as the current user out of your downloads directory if that's where you want to keep it. Even more impressive, the CMake binaries in the tarballs are statically linked and require only libc6 as a dependency. Glibc has been ABI-stable since 1997. It will work on your system.
We on the Halide team use the CMake 3.20.2 tarballs from Kitware on a variety of aging and new ARM hardware for our build infrastructure. We used to build CMake from scratch, which was a little painful, but since upstream started providing ARM binaries, it's been trivial.
There are good reasons for using modern CMake versions, too. Beyond broader compiler and platform compatibility, newer CMake versions offer many more features to help keep your builds simple and expressive. One of the best examples is CMake's CUDA support. It has gone through several evolutions from a find module to a full first-class language. Working with CUDA prior to CMake 3.17 is about as much fun as eating glass. The move away from package variables to targets with transitive, propagating properties has turned ugly, error-prone build scripts into simple, declarative build descriptions. We will touch on many of these features in the next few parts.
So there is no problem with taking a minimum version of 3.20 (the latest at time of writing). Maybe it's worth taking a minimum of 3.16 just because Ubuntu 20.04 LTS is such a hold-out, but anything earlier than that is plain masochism.
Another hard requirement is that you must never use a version of CMake older than your compiler. Older versions of CMake won't somehow know how to work with a compiler that was released later in time, and the command line defaults for GCC, Clang, and other major compilers changes frequently. The most basic example of this is the default language version and set of supported language versions. Other changes include the wording of errors and warnings that CMake matches to detect compiler capabilities.
Thus, if you intend to use C++17 on Linux, you will need to use at least Clang 5 (released Sep 7, 2017) or GCC 7 (released May 2, 2017), so you therefore cannot use a minimum CMake version prior to 3.9.3 (released Sep 20, 2017), and versions prior to 3.8 (released April 10, 2017) didn't even understand 17
as a possible value of the CXX_STANDARD
target property, so there was no correct way to enable it. Rather than doing this tedious and ultimately pointless work of determining the oldest potentially compatible versions, just use the newest.
No matter what minimum version you pick for whatever reason, it would be a major mistake to simply set cmake_minimum_required(VERSION 3.X)
and call it a day. You must also test with the actual CMake 3.X release on your local development machine and on CI.
Why? Simply because the policy mechanism ensures backwards compatibility, not forwards compatibility. If you use a more recent CMake version, nothing will stop you from using a feature that is too new for the declared minimum version. This is very, very common, too. Here are three examples off the top of my head that have bitten me:
lib64
directories when using HINTS
arguments to find_library
, but older ones don't. So code for old versions have to check CMAKE_SIZEOF_VOID_P
and add those paths to HINTS
manually. I don't think this is documented; I bisected to find that version number.So another basic rule is to never declare a minimum version lower than the one you actually test your build against. I have seen projects in the wild that claim compatibility with ridiculously old versions of CMake, like 2.6. Not only is it extremely unlikely that those builds actually work with 2.6, newer versions of CMake are soon dropping compatibility with versions before 2.8.12. So this "increased" compatibility will in fact cost you users who are doing the right thing by keeping up to date.
If you're setting up a CI pipeline, you should test your build with both the absolute newest version of CMake, and the minimum required version. This will allow you to very quickly catch backwards compatibility bugs and make upgrading the version a breeze. I do this on GitHub Actions with the jwlawson/actions-setup-cmake
action. You can see such a workflow here on tinyxml2
, whose CMake build I recently helped modernize.
These are the most important lessons from this post:
In part 2, we'll talk about the contract between a CMake build and its many consumers.
Until then, join the conversation here on Reddit!
Since publishing this article, I have heard from several readers that they cannot upgrade their minimum versions because some particular Linux distribution (e.g. Ubuntu 18.04 LTS, RHEL 7, etc.) packages an older version of CMake, and so they must accept that version.
I stand by what I wrote. On the one hand, if the maintainers independently want to include your package, then it's up to them to figure out how to use a newer CMake version in their build process. If that means bootstrapping a newer CMake version from source, so be it.
On the other hand, if you want to ask the maintainers to include your package, and they won't let you use a newer version, you should instead ask yourself why your package needs to be included in the base distribution. There are many viable distribution methods on Linux these days. You could host your own APT or RPM repository; you could release your package on pip or snap. Agreeing to downgrade for the sake of one distro harms all of your users.
Lowest common denominator thinking is toxic to the progress of the C and C++ ecosystems. Distribution maintainers should periodically update CMake, even on LTS releases. CMake is incredibly backwards compatible, but when there are issues, there are also many recourses for a distribution maintainer: they can package multiple CMake versions, they can patch a problematic package (and maybe upstream the patch, which is better for everyone), or they can patch their distribution of CMake. The vcpkg team occasionally has to rewrite entire build systems for projects.
At the beginning of the article, I complained that there are no good learning resources. Fortunately, this isn't quite true. So far as I know, the best places to get high quality advice for writing CMake code are these:
#cmake
channel on the CppLang Slack. This is a very friendly community for CMake users to think through build issues, ask beginner to intermediate level questions, and share wisdom.If you don't want to buy Professional CMake or can't afford it, here are some good free resources on the web:
IMPORTED_GLOBAL
on targets created by find_package
calls. These talks are particularly valuable for showing the old, painful way of doing things next to the new(er), less-painful way.Unless otherwise stated, all code snippets are licensed under CC-BY-SA 4.0.