Book Image

CMake Best Practices

By : Dominik Berner, Mustafa Kemal Gilor
5 (2)
Book Image

CMake Best Practices

5 (2)
By: Dominik Berner, Mustafa Kemal Gilor

Overview of this book

CMake is a powerful tool used to perform a wide variety of tasks, so finding a good starting point for learning CMake is difficult. This book cuts to the core and covers the most common tasks that can be accomplished with CMake without taking an academic approach. While the CMake documentation is comprehensive, it is often hard to find good examples of how things fit together, especially since there are lots of dirty hacks and obsolete solutions available on the internet. This book focuses on helping you to tie things together and create clean and maintainable projects with CMake. You'll not only get to grips with the basics but also work through real-world examples of structuring large and complex maintainable projects and creating builds that run in any programming environment. You'll understand the steps to integrate and automate various tools for improving the overall software quality, such as testing frameworks, fuzzers, and automatic generation of documentation. And since writing code is only half of the work, the book also guides you in creating installers and packaging and distributing your software. All this is tailored to modern development workflows that make heavy use of CI/CD infrastructure. By the end of this CMake book, you'll be able to set up and maintain complex software projects using CMake in the best way possible.
Table of Contents (22 chapters)
Part 1: The Basics
Part 2: Practical CMake – Getting Your Hands Dirty with CMake
Part 3: Mastering the Details

Different toolchains and build types

The power of CMake comes from the fact that you can use the same build specification – that is, CMakeLists.txt – for various compiler toolchains without the need to rewrite anything. A toolchain typically consists of a series of programs that can compile and link binaries, as well as creating archives and similar.

CMake supports a variety of languages that the toolchains can be configured for. In this book, we will focus on C++. Configuring the toolchain for different programming languages is done by replacing the CXX part of the following variables with the respective language tag:

  • C
  • CXX – C++
  • CUDA
  • OBJC – Objective C
  • OBJCXX – Objective C++
  • Fortran
  • HIP – HIP C++ runtime API for NVIDIA and AMD GPUs
  • ISPC – C-based SPMD programming language
  • ASM – Assembler

If a project does not specify its language, it's assumed that C and CXX are being used.

CMake will automatically detect the toolchain to use by inspecting the system, but if needed, this can be configured by environment variables or, in the case of cross-compiling, by providing a toolchain file. This toolchain is stored in the cache, so if the toolchain changes, the cache must be deleted and rebuilt. If multiple compilers are installed, you can specify a non-default compiler by either setting the environment variables as CC for C or CXX for a C++ compiler before calling CMake. Here, we're using the CXX environment variable to overwrite the default compiler to be used in CMake:

CXX=g++-7 cmake /path/to/the/source

Alternatively, you can overwrite the C++ compiler to use by passing the respective cmake variable using -D, as shown here:

cmake -D CMAKE_CXX_COMPILER=g++-7 /path/to/source

Both methods ensure that CMake is using GCC version 7 to build instead of whatever default compiler is available in the system. Avoid setting the compiler toolchain inside the CMakeLists.txt files as this clashes with the paradigm that states that CMake files should be platform- and compiler-agnostic.

By default, the linker is automatically selected by the chosen compiler, but it is possible to select a different one by passing the path to the linker executable with the CMAKE_CXX_LINKER variable.

Build types

When you're building C++ applications, it is quite common to have various build types, such as a debug build that contains all debug symbols and release builds that are optimized.

CMake natively provides four build types:

  • Debug: This is non-optimized and contains all the debug symbols. Here, all the asserts are enabled. This is the same as setting -O0 -g for GCC and Clang.
  • Release: This is optimized for speed without debugging symbols and asserts disabled. Usually, this is the build type that is shipped. This is the same as -O3 -DNDEBUG.
  • RelWithDebInfo: This provides optimized code and includes debug symbols but disabled asserts, which is the same as -O2 -g -DNDEBUG.
  • MinSizeRel: This is the same as Release but optimized for a small binary size instead of speed, which would be -Os -DNDEBUG. Note that this configuration is not supported for all generators on all platforms.

Note that the build types must be passed during the configuration state and are only relevant for single-target generators such as CMake or Ninja. For multi-target generators such as MSVC, they are not used, as the build-system itself can build all build types. It is possible to create custom build types, but since they do not work for every generator, this is usually not encouraged.

Since CMake supports such a wide variety of toolchains, generators, and languages, a frequent question is how to find and maintain working combinations of these options. Here, presets can help.