Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Modern CMake for C++

You're reading from   Modern CMake for C++ Effortlessly build cutting-edge C++ code and deliver high-quality solutions

Arrow left icon
Product type Paperback
Published in May 2024
Publisher Packt
ISBN-13 9781805121800
Length 502 pages
Edition 2nd Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Rafał Świdziński Rafał Świdziński
Author Profile Icon Rafał Świdziński
Rafał Świdziński
Arrow right icon
View More author details
Toc

Table of Contents (20) Chapters Close

Preface 1. First Steps with CMake 2. The CMake Language FREE CHAPTER 3. Using CMake in Popular IDEs 4. Setting Up Your First CMake Project 5. Working with Targets 6. Using Generator Expressions 7. Compiling C++ Sources with CMake 8. Linking Executables and Libraries 9. Managing Dependencies in CMake 10. Using the C++20 Modules 11. Testing Frameworks 12. Program Analysis Tools 13. Generating Documentation 14. Installing and Packaging 15. Creating Your Professional Project 16. Writing CMake Presets 17. Other Books You May Enjoy
18. Index
Appendix

Mastering the command line

The majority of this book will teach you how to prepare CMake projects for your users. To cater to their needs, we need to thoroughly understand how users interact with CMake in different scenarios. This will allow you to test your project files and ensure they’re working correctly.

CMake is a family of tools and consists of five executables:

  • cmake: The main executable that configures, generates, and builds projects
  • ctest: The test driver program used to run and report test results
  • cpack: The packaging program used to generate installers and source packages
  • cmake-gui: The graphical wrapper around cmake
  • ccmake: The console-based GUI wrapper around cmake

Additionally, Kitware, the company behind CMake, offers a separate tool called CDash to provide advanced oversight over the health of our projects’ builds.

CMake command line

The cmake is the main binary of the CMake suite, and provides a few modes of operation (also sometimes called actions):

  • Generating a project buildsystem
  • Building a project
  • Installing a project
  • Running a script
  • Running a command-line tool
  • Running a workflow preset
  • Getting help

Let’s see how they work.

Generating a project buildsystem

The first step required to build our project is to generate a buildsystem. Here are three forms of command to execute the CMake generating a project buildsystem action:

cmake [<options>] -S <source tree> -B <build tree>
cmake [<options>] <source tree>
cmake [<options>] <build tree>

We’ll discuss available <options> in the upcoming sections. Right now, let’s focus on choosing the right form of the command. One important feature of CMake is the support for out-of-source builds or the support for storing build artifacts in a directory different from the source tree. This is a preferred approach to keep the source directory clean from any build-related files and avoid polluting the Version Control Systems (VCSs) with accidental files or ignore directives.

This is why the first form of command is the most practical. It allows us to specify the paths to the source tree and the produced buildsystem specified with -S and -B, respectively:

cmake -S ./project -B ./build

CMake will read the project files from the ./project directory and generate a buildsystem in the ./build directory (creating it beforehand if needed).

We can skip one of the arguments and cmake will “guess” that we intended to use the current directory for it. Note that skipping both will produce an in-source build and store the build artifacts along with source files, which we don’t want.

BE EXPLICIT WHEN RUNNING CMAKE

Do not use the second or third form of the cmake <directory> command, because they can produce a messy in-source build. In Chapter 4, Setting Up Your First CMake Project, we’ll learn how to prevent users from doing that.

As hinted in the syntax snippet, the same command behaves differently if a previous build already exists in <directory>: it will use the cached path to the sources and rebuild from there. Since we often invoke the same commands from the Terminal command history, we might get into trouble here; before using this form, always check whether your shell is currently working in the right directory.

Examples

Generate the build tree in the current directory using the source from one directory up:

cmake -S ..

Generate the build tree in the ./build directory using the source from the current directory:

cmake -B build

Choosing a generator

As discussed earlier, you can specify a few options during the generation stage. Selecting and configuring a generator decides which build tool from our system will be used for building in the subsequent Building a project section, what build files will look like, and what the structure of the build tree will be.

So, should you care? Luckily, the answer is often “no.” CMake does support multiple native buildsystems on many platforms; however, unless you have installed a few generators at the same time, CMake will correctly select one for you. This can be overridden by the CMAKE_GENERATOR environment variable or by specifying the generator directly on the command line, like so:

cmake -G <generator name> -S <source tree> -B <build tree>

Some generators (such as Visual Studio) support a more in-depth specification of a toolset (compiler) and platform (compiler or SDK). Additionally, CMake will scan environment variables that override the defaults: CMAKE_GENERATOR_TOOLSET and CMAKE_GENERATOR_PLATFORM. Alternatively, the values can be specified directly in the command line:

cmake -G <generator name>
      -T <toolset spec>
      -A <platform name>
      -S <source tree> -B <build tree>

Windows users usually want to generate a buildsystem for their preferred IDE. On Linux and macOS, it’s very common to use the Unix Makefiles or Ninja generators.

To check which generators are available on your system, use the following command:

cmake --help

At the end of the help printout, you will get a full list of generators, like this one produced on Windows 10 (some output was truncated for readability):

The following generators are available on this platform:

Visual Studio 17 2022       
Visual Studio 16 2019       
Visual Studio 15 2017 [arch]
Visual Studio 14 2015 [arch]
Visual Studio 12 2013 [arch]
Visual Studio 11 2012 [arch]
Visual Studio 9 2008 [arch] 
Borland Makefiles           
NMake Makefiles             
NMake Makefiles JOM         
MSYS Makefiles              
MinGW Makefiles             
Green Hills MULTI           
Unix Makefiles              
Ninja                       
Ninja Multi-Config          
Watcom WMake                
CodeBlocks - MinGW Makefiles
CodeBlocks - NMake Makefiles
CodeBlocks - NMake Makefiles JOM
CodeBlocks - Ninja          
CodeBlocks - Unix Makefiles 
CodeLite - MinGW Makefiles  
CodeLite - NMake Makefiles  
CodeLite - Ninja            
CodeLite - Unix Makefiles   
Eclipse CDT4 - NMake Makefiles
Eclipse CDT4 - MinGW Makefiles
Eclipse CDT4 - Ninja        
Eclipse CDT4 - Unix Makefiles
Kate - MinGW Makefiles      
Kate - NMake Makefiles      
Kate - Ninja                
Kate - Unix Makefiles       
Sublime Text 2 - MinGW Makefiles
Sublime Text 2 - NMake Makefiles
Sublime Text 2 - Ninja      
Sublime Text 2 - Unix Makefiles

As you can see, CMake supports a lot of different generator flavors and IDEs.

Managing the project cache

CMake queries the system for all kinds of information during the configuration stage. Because these operations can take a bit of time, the collected information is cached in the CMakeCache.txt file in the build tree directory. There are a few command-line options that allow you to manage the behavior of the cache more conveniently.

The first option at our disposal is the ability to prepopulate cached information:

cmake -C <initial cache script> -S <source tree> -B <build tree>

We can provide a path to the CMake listfile, which (only) contains a list of set() commands to specify variables that will be used to initialize an empty build tree. We’ll discuss writing the listfiles in the next chapter.

The initialization and modification of existing cache variables can be done in another way (for instance, when creating a file is a bit much to only set a few variables). You can set them directly in a command line, as follows:

cmake -D <var>[:<type>]=<value> -S <source tree> -B <build tree>

The :<type> section is optional (it is used by GUIs) and it accepts the following types: BOOL, FILEPATH, PATH, STRING or INTERNAL. If you omit the type, CMake will check if the variable exists in the CMakeCache.txt file and use its type; otherwise, it will be set to UNINITIALIZED.

One particularly important variable that we’ll often set through the command line specifies the build type (CMAKE_BUILD_TYPE). Most CMake projects will use it on numerous occasions to decide things such as the verbosity of diagnostic messages, the presence of debugging information, and the level of optimization for created artifacts.

For single-configuration generators (such as GNU Make and Ninja), you should specify the build type during the configuration phase and generate a separate build tree for each type of config. Values used here are Debug, Release, MinSizeRel, or RelWithDebInfo. Missing this information may have undefined effects on projects that rely on it for configuration.

Here’s an example:

cmake -S . -B ../build -D CMAKE_BUILD_TYPE=Release

Note that multi-configuration generators are configured during the build stage.

For diagnostic purposes, we can also list cache variables with the -L option:

cmake -L -S <source tree> -B <build tree>

Sometimes, project authors may provide insightful help messages with variables – to print them, add the H modifier:

cmake -LH -S <source tree> -B <build tree>
cmake -LAH -S <source tree> -B <build tree>

Surprisingly, custom variables that are added manually with the -D option won’t be visible in this printout unless you specify one of the supported types.

The removal of one or more variables can be done with the following option:

cmake -U <globbing_expr> -S <source tree> -B <build tree>

Here, the globbing expression supports the * (wildcard) and ? (any character) symbols. Be careful when using these, as it is easy to erase more variables than intended.

Both the -U and -D options can be repeated multiple times.

Debugging and tracing

The cmake command can be run with a multitude of options that allow you to peek under the hood. To get general information about variables, commands, macros, and other settings, run the following:

cmake --system-information [file]

The optional file argument allows you to store the output in a file. Running it in the build tree directory will print additional information about the cache variables and build messages from the log files.

In our projects, we’ll be using message() commands to report details of the build process. CMake filters the log output of these based on the current log level (by default, this is STATUS). The following line specifies the log level that we’re interested in:

cmake --log-level=<level>

Here, level can be any of the following: ERROR, WARNING, NOTICE, STATUS, VERBOSE, DEBUG, or TRACE. You can specify this setting permanently in the CMAKE_MESSAGE_LOG_LEVEL cache variable.

Another interesting option allows you to display log context with each message() call. To debug very complex projects, the CMAKE_MESSAGE_CONTEXT variable can be used like a stack. Whenever your code enters an interesting context, you can name it descriptively. By doing this, our messages will be decorated with the current CMAKE_MESSAGE_CONTEXT variable, like so:

[some.context.example] Debug message.

The option to enable this kind of log output is as follows:

cmake --log-context <source tree>

We’ll discuss naming contexts and logging commands in more detail in Chapter 2, The CMake Language.

If all else fails and we need to use the big guns, there is always trace mode, which will print every executed command with its filename, the line number it is called from, and a list of passed arguments. You can enable it as follows:

cmake --trace

As you can imagine, it’s not recommended for everyday use, as the output is very long.

Configuring presets

There are many, many options that users can specify to generate a build tree from your project. When dealing with the build tree path, generator, cache, and environmental variable, it’s easy to get confused or miss something. Developers can simplify how users interact with their projects and provide a CMakePresets.json file that specifies some defaults.

To list all of the available presets, execute the following:

cmake --list-presets

You can use one of the available presets as follows:

cmake --preset=<preset> -S <source> -B <build tree>

To learn more, please refer to the Navigating the project files section of this chapter and Chapter 16, Writing CMake Presets.

Cleaning the build tree

Every now and then, we might need to erase generated files. This may be due to some changes in the environment that were made between builds, or just to ensure that we are working on a clean slate. We can go ahead and delete the build tree directory manually, or just add the --fresh parameter to the command line:

cmake --fresh -S <source tree> -B <build tree>

CMake will then erase CMakeCache.txt and CMakeFiles/ in a system-agnostic way and generate the buildsystem from scratch.

Building a project

After generating our build tree, we’re ready for the building a project action. Not only does CMake know how to generate input files for many different builders but it can also run them for us providing appropriate arguments, as required by our project.

AVOID CALLING MAKE DIRECTLY

Many online sources recommend running GNU Make directly after the generation stage by calling the make command directly. Because GNU Make is a default generator for Linux and macOS, this recommendation can work. However, use the method described in this section instead, as it is generator-independent and is officially supported across all platforms. As a result, you won’t need to worry about the exact environment of every user of your application.

The syntax of build mode is:

cmake --build <build tree> [<options>] [-- <build-tool-options>]

In the majority of cases, it is enough to simply provide the bare minimum to get a successful build:

cmake --build <build tree>

The only required argument is the path to the generated build tree. This is the same path that was passed with the -B argument in the generation stage.

CMake allows you to specify key build parameters that work for every builder. If you need to provide special arguments to your chosen native builder, pass them at the end of the command after the -- token:

cmake --build <build tree> -- <build tool options>

Let’s see what other options are available.

Running parallel builds

By default, many build tools will use multiple concurrent processes to leverage modern processors and compile your sources in parallel. Builders know the structure of project dependencies, so they can simultaneously process steps that have their dependencies met to save users’ time.

You might want to override that setting if you’d like to build faster on a multi-core machine (or to force a single-threaded build for debugging).

Simply specify the number of jobs with either of the following options:

cmake --build <build tree> --parallel [<number of jobs>]
cmake --build <build tree> -j [<number of jobs>]

The alternative is to set it with the CMAKE_BUILD_PARALLEL_LEVEL environment variable. The command-line option will override this variable.

Selecting targets to build and clean

Every project is made up of one or more parts, called targets (we’ll discuss these in the second part of the book). Usually, we’ll want to build all available targets; however, on occasion, we might be interested in skipping some or explicitly building a target that was deliberately excluded from normal builds. We can do this as follows:

cmake --build <build tree> --target <target1> --target <target2> …

We can specify multiple targets to build by repeating the –target argument. Also, there’s a shorthand version, -t <target>, that can be used instead.

Cleaning the build tree

One special target that isn’t normally built is called clean. Building it has the special effect of removing all artifacts from the build directory, so everything can be created from scratch later. You can start this process like this:

cmake --build <build tree> -t clean

Additionally, CMake offers a convenient alias if you’d like to clean first and then implement a normal build:

cmake --build <build tree> --clean-first

This action is different from cleaning mentioned in the Cleaning the build tree section, as it only affects target artifacts and nothing else (like the cache).

Configuring the build type for multi-configuration generators

So, we already know a bit about generators: they come in different shapes and sizes. Some of them offer the ability to build both Debug and Release build types in a single build tree. Generators that support this feature include Ninja Multi-Config, Xcode, and Visual Studio. Every other generator is a single-configuration generator, and they require a separate build tree for every config type we want to build.

Select Debug, Release, MinSizeRel, or RelWithDebInfo and specify it as follows:

cmake --build <build tree> --config <cfg>

Otherwise, CMake will use Debug as the default.

Debugging the build process

When things go bad, the first thing we should do is check the output messages. However, veteran developers know that printing all the details all the time is confusing, so they often hide them by default. When we need to peek under the hood, we can ask for far more detailed logs by telling CMake to be verbose:

cmake --build <build tree> --verbose
cmake --build <build tree> -v

The same effect can be achieved by setting the CMAKE_VERBOSE_MAKEFILE cached variable.

Installing a project

When artifacts are built, users can install them on the system. Usually, this means copying files into the correct directories, installing libraries, or running some custom installation logic from a CMake script.

The syntax of installation mode is:

cmake --install <build tree> [<options>]

As with other modes of operation, CMake requires a path to a generated build tree:

cmake --install <build tree>

The install action also has plenty of additional options. Let’s see what they can do.

Choosing the installation directory

We can prepend the installation path with a prefix of our choice (for example, when we have limited write access to some directories). The /usr/local path that is prefixed with /home/user becomes /home/user/usr/local.

The signature for this option is as follows:

cmake --install <build tree> --install-prefix <prefix>

If you use CMake 3.21 or older, you’ll have to use a less explicit option:

cmake --install <build tree> --prefix <prefix>

Note that this won’t work on Windows, as paths on this platform usually start with the drive letter.

Installation for multi-configuration generators

Just like in the build stage, we can specify which build type we want to use for our installation (for more details, please refer to the Building a project section). The available types include Debug, Release, MinSizeRel, and RelWithDebInfo. The signature is as follows:

cmake --install <build tree> --config <cfg>

Selecting components to install

As a developer, you might choose to split your project into components that can be installed independently. We’ll discuss the concept of components in further detail in Chapter 14, Installing and Packaging. For now, let’s just assume they represent sets of artifacts that don’t need to be used in every case. This might be something like application, docs, and extra-tools.

To install a single component, use the following option:

cmake --install <build tree> --component <component>

Setting file permissions

If the installation is performed on a Unix-like platform, you can specify default permissions for the installed directories with the following option, using the format of u=rwx,g=rx,o=rx:

cmake --install <build tree>
      --default-directory-permissions <permissions>

Debugging the installation process

Similarly to the build stage, we can also choose to view a detailed output of the installation stage. To do this, use any of the following:

cmake --install <build tree> --verbose
cmake --install <build tree> -v

The same effect can be achieved if the VERBOSE environment variable is set.

Running a script

CMake projects are configured using CMake’s custom language. It’s cross-platform and quite powerful. Since it’s already there, why not make it available for other tasks? Sure enough, CMake can run standalone scripts (more on that in the Discovering scripts and modules section), like so:

cmake [{-D <var>=<value>}...] -P <cmake script file>
      [-- <unparsed options>...]

Running such a script won’t run any configuration or generate stages, and it won’t affect the cache.

There are two ways you can pass values to this script:

  • Through variables defined with the -D option
  • Through arguments that can be passed after a -- token

CMake will create CMAKE_ARGV<n> variables for all arguments passed to the script with the latter (including the -- token).

Running a command-line tool

On rare occasions, we might need to run a single command in a platform-independent way – perhaps copy a file or compute a checksum. Not all platforms were created equal, so not all commands are available in every system (or they have been named differently).

CMake offers a mode in which most common commands can be executed in the same way across platforms. Its syntax is:

cmake -E <command> [<options>]

As the use of this particular mode is fairly limited, we won’t cover it in depth. However, if you’re interested in the details, I recommend calling cmake -E to list all the available commands. To simply get a glimpse of what’s on offer, CMake 3.26 supports the following commands: capabilities, cat, chdir, compare_files, copy, copy_directory, copy_directory_if_different, copy_if_different, echo, echo_append, env, environment, make_directory, md5sum, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, remove, remove_directory, rename, rm, sleep, tar, time, touch, touch_nocreate, create_symlink, create_hardlink, true, and false.

If a command you’d like to use is missing or you need a more complex behavior, consider wrapping it in a script and running it in -P mode.

Running a workflow preset

We mentioned in the How does it work? section that building with CMake has three stages: configure, generate, and build. Additionally, we can also run automated tests and even create redistributable packages with CMake. Usually, users need to manually execute every such step separately by calling the appropriate cmake action through the command line. However, advanced projects can specify workflow presets that bundle multiple steps into a single action that can be executed with just one command. For now, we’ll only mention that users can get the list of available presets by running:

cmake ––workflow --list-presets

They can execute a workflow preset with:

cmake --workflow --preset <name>

This will be explained in depth in Chapter 16, Writing CMake Presets.

Getting help

It isn’t a surprise that CMake offers extensive help that is accessible through its command line. The syntax of help mode is:

cmake --help

This will print the list of the possible topics to dive deeper into and explain which parameters need to be added to the command to get more help.

CTest command line

Automated testing is very important in order to produce and maintain high-quality code. The CMake suite comes with a dedicated command-line tool for this purpose called CTest. It is provided to standardize the way tests are run and reported. As a CMake user, you don’t need to know the details of testing this particular project: what framework is used or how to run it. CTest provides a convenient interface to list, filter, shuffle, retry, and timebox test runs.

To run tests for a built project, we just need to call ctest in the generated build tree:

$ ctest
Test project /tmp/build
Guessing configuration Debug
    Start 1: SystemInformationNew
1/1 Test #1: SystemInformationNew .........   Passed 3.19 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) =   3.24 sec

We devoted an entire chapter to this subject: Chapter 11, Testing Frameworks.

CPack command line

After we have built and tested our amazing software, we are ready to share it with the world. The rare few power users are completely fine with the source code. However, the vast majority of the world uses precompiled binaries for convenience and time-saving reasons.

CMake doesn’t leave you stranded here; it comes with batteries included. CPack is a tool that will create redistributable packages for various platforms: compressed archives, executable installers, wizards, NuGet packages, macOS bundles, DMG packages, RPMs, and more.

CPack works in a very similar way to CMake: it is configured with the CMake language and has many package generators to pick from (not to be confused with CMake buildsystem generators). We’ll go through all the details in Chapter 14, Installing and Packaging, as this tool is meant to be used by mature CMake projects.

CMake GUI

CMake for Windows comes with a GUI version to configure the building process of previously prepared projects. For Unix-like platforms, there is a version built with Qt libraries. Ubuntu provides it in the cmake-qt-gui package.

To access the CMake GUI, run the cmake-gui executable:

Figure 1.3: The CMake GUI – the configuring stage for a buildsystem using a generator for Visual Studio 2019

The GUI application is a convenience for users of your application: it can be useful for those who aren’t familiar with the command line and would prefer a graphical interface.

USE COMMAND-LINE TOOLS INSTEAD

I would definitely recommend the GUI to end users, but for programmers like you, I suggest avoiding any manual blocking steps that require clicking on forms every time you build your programs. This is especially advantageous in mature projects, where entire builds can be fully executed without any user interaction.

CCMake command line

The ccmake executable is an interactive text user interface for CMake on Unix-like platforms (it’s unavailable for Windows unless explicitly built). I’m mentioning it here so you know what it is when you see it (Figure 1.4, but as with the GUI, developers will benefit more from editing the CMakeCache.txt file directly.

Figure 1.4: The configuring stage in ccmake

Having this out of the way, we have concluded the basic introduction to the command line of the CMake suite. It’s time to discover the structure of a typical CMake project.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime