Some CMake tips

Sketch of John Barber's gas turbine, from his patent

I spent the past few weeks converting a bunch of Make and Autotools-based modules to use CMake instead. This was my first major outing with CMake. Maybe there will be a few blog posts on that subject!

In general I think CMake has a sound design and I quite want to like it. It seems like many of its warts are due to its long history and the need for backwards compatibility, not anything fundamentally broken. To keep a project going for 16 years is impressive and it is pretty widely used now. This is a quick list of things I found in CMake that confused me to start with but ultimately I think are good things.

1. Targets are everything

CMake is pretty similar to normal make in that all the things that you care about are ‘targets’. Libraries are targets, programs are targets, subdirectories are targets and custom commands create files which are considered targets. You can also create custom targets which run commands if executed. You need to use custom targets feature if you want a custom command target to be tied to the default target, which is a little confusing but works OK.

Targets have properties, which are useful.

2. Absolute paths to shared libraries

Traditionally you link to libfoo by passing -lfoo to the linker. Then, if libfoo is in a non-standard location, you pass -L/path/to/foo -lfoo. I don’t think pkg-config actually enforces this pattern but pretty much all the .pc files I have installed use the -L/path -Lname pattern.

CMake makes this quite awkward to do, because it makes every effort to forget about the linker paths. Library ‘targets’ in CMake keep track of associated include paths, dependent libraries, compile flags, and even extra source files, using ‘target properties’. There’s no target property for LINK_DIRECTORIES, though, so outside of the current CMakeLists.txt file they won’t be tracked. There is a global LINK_DIRECTORIES property, confusingly, but it’s specifically marked as “for debugging purposes.”

So the recommended way to link to libraries is with the absolute path. Which makes sense! Why say with two commandline arguments what you can say with one?

At least, this will be fine once CMake’s pkg-config integration returns absolute paths to libraries

3. Semicolon safety instead of whitespace safety

CMake has a ‘list’ type which is actually a string with ; (semicolon) used to delimit entities. Spaces are used as an argument separator, but converted to semicolons during argument parsing, I think. Crucially, they seem to be converted before variable expansion is done, which means that filenames with spaces don’t need any special treatment. I like this more than shell code where I have to quote literally every variable (or else Richard Maw shouts at me).

For example:

cmake_minimum_required(VERSION 3.2)
set(path "filename with spaces")
set(command ls ${path})
foreach(item ${command})
     message(item: ${item})
endforeach() 

Output:

item:ls
item:filename with spaces

On the other hand:

cmake_minimum_required(VERSION 3.2)
set(path "filename;with\;semicolons") 
set(command ls ${path}) 
foreach(item ${command})
     message(item: ${item})
endforeach()

Output:

item:ls
item:filename
item:with
item:semicolons

Semicolons occur less often in file names, I guess. Most of us are trained to avoid spaces, partly because we know how broken (all?) most shell-based build systems are in those cases. CMake hasn’t actually solved this but just punted the special character to a less often used one, as far as I can see. I guess that’s an improvement? Maybe?

The semi-colon separator can bite you in other ways, for example, when specifying CMAKE_PREFIX_PATH (library and header search path) you might expect this to work:

cmake . -DCMAKE_PREFIX_PATH=/opt/path1:/opt/path2

However, that won’t work (unless you did actually mean that to be one item). Instead, you need to pass this:

cmake . -DCMAKE_PREFIX_PATH=/opt/path1\;/opt/path2

Of course, ; is a special character in UNIX shells so must be escaped.

4. Ninja instead of Make

CMake supports multiple backends, and Ninja is often faster than GNU Make, so give the Ninja backend a try: cmake -G Ninja.

5. Policies

The CMake developers seem pretty good at backwards compatibility. To this end they have introduced the rather obtuse policies framework. The great thing about the policies framework is that you can completely ignore it, as long as you have cmake_minimum_required(VERSION 3.3) at the top of your toplevel CMakeLists.txt. You’ll only need it once you have a massive bank of existing CMakeLists.txt files and you are getting started on porting them to a newer version of CMake.

Quite a lot of CMake error messages are worded to make you think like you might need to care about policies, but don’t be fooled. Mostly these errors are for situations where there didn’t use to be an error, I think, and so the policy exists to bring back the ‘old’ behaviour, if you need it.

If a tool is weird but internally consistent, I can get on with it. Hopefully, CMake is getting there. I can see there have been a lot of good improvements since CMake 2.x, at least. And at no point so far has it made me more angry than GNU Autotools. It’s not crashed at all (impressive given it’s entirely C++ code!). And it is significantly faster and more widely applicable than Autotools or artisanal craft Makefiles. So I’ll be considering it in future. But I can’t help wishing for a build system that I actually liked

Edit: you might also be interested in a list of common CMake antipatterns.

10 thoughts on “Some CMake tips

  1. I’ve been using CMake for one of my projects, and it has generally made me much more angry than autotools. Unfortunately, autotools doesn’t work nearly as well on Windows.

    It’s worth mentioning that you need to be careful with the semicolon-separated lists. Sometimes CMake will store things as a string which you may expect to be a list; the compiler flags, for example. If you’re not careful you’ll end up with unexpected semicolons in your compile commands.

    I would also like to suggest a tip: one thing that I’ve found particularly bothersome is the command line interface. Instead of running a configure script with (usually) nice option names, people have to define strange variables (via `-DFOO=bar` flags) which aren’t generally conveniently documented (i.e., `cmake –help` isn’t very helpful). This would be tolerable if it were only an issue for the developers (as my other complaints are), but it causes problems for everyone who wants to compile the library. So, I wrote a wrapper script which (IMHO) everyone should include in their projects: https://github.com/nemequ/configure-cmake/

  2. The reason why pkg-config files specify the linker path and the library name is that the linker path is usually a variable which can be substituted — e.g.


    libdir=@libdir@

    Libs: -L${libdir} -lname-1.0

    Which gets useful on builders or isolated development environments. Now I can do:

    $ pkg-config –define-variable=libdir=/foo/lib –libs name-1.0

    And pkg-config will replace the variable for me, without having to set up LD_LIBRARY_PATH and friends. Another useful thing is being able to query the library paths using –libs-only-L when setting up a build environment.

    Obviously, as long as there are libraries that do not use pkg-config and instead rely on global variables or full paths, you’ll need to hack around them with environment variables and ad hoc cases.

    1. Makes sense. I guess it would be just OK to do this though?


      libdir=@libdir@

      Libs: ${libdir}/libname-1.0.so

      There is also PKG_CONFIG_SYSROOT_DIR, but that’s not quite as flexible I guess

  3. Curious what projects you’ve been porting to CMake. Any GNOME- or freedesktop-related ones?

    We’ve been using CMake in WebKit and have had good success, to the point that we were able to delete the Autotools build. The Visual Studio build is next on the chopping block. Some GNOME-specific things still need to be made easier (e.g. GtkDoc), but you’ve already been working on that!

    ninja is a huge win over make when used for big projects like WebKit. I rather doubt it has any significant advantage for GNOME, though.

  4. The actual work was on some proprietary embedded software. There are some helper macros that can be open sourced out of it, though, you might have similar things in webkit already for doing stuff like running glib-genmarshal, gdbus-codegen and the like. I’m wondering if it makes sense to try and get the GLib related CMake macros upstream in GLib…

  5. For semicolons you need a double backslash:

    “one\\;two”

    The reason is that just one backslash is used up when CMake reads the string literal, so “one\;two” results in an internal string of “one;two” which is just how CMake stores lists. You want an internal string of “one\;two” so that CMake knows the ; isn’t a list separator. You could also do [[one\;two]] using the ‘bracket string’ notation (i.e. raw strings).

    I agree it is annoying that they apparently learned that in-band list separation using spaces is a terrible idea, but then thought that just using a different character would be ok. I wish CMake had an actual type system, and also real function return values, and wasn’t quite so insane. That said it’s still better than the insanity of automake or raw Makefiles.

    The only build system (for C++ anyway) I’ve seen that is better is Qt’s QBS. It actually has a sane declarative syntax that IDEs can vaguely understand. It uses Javascript for the bits where you absolutely need custom code which is a reasonable choice (Javascript has some terrible parts, like automatic type coercion but at least it has real functions and lists). It’s not as obviously flexible as CMake but that’s actually a good thing. It’s generally a bad idea to have your build system super-flexible because a) no IDE will understand it, b) no other people will understand it, c) you’ll end up with ugly hacks, guaranteed.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.