Book Image

Modern CMake for C++

By : Rafał Świdziński
5 (2)
Book Image

Modern CMake for C++

5 (2)
By: Rafał Świdziński

Overview of this book

Creating top-notch software is an extremely difficult undertaking. Developers researching the subject have difficulty determining which advice is up to date and which approaches have already been replaced by easier, better practices. At the same time, most online resources offer limited explanation, while also lacking the proper context and structure. This book offers a simpler, more comprehensive, experience as it treats the subject of building C++ solutions holistically. Modern CMake for C++ is an end-to-end guide to the automatization of complex tasks, including building, testing, and packaging. You'll not only learn how to use the CMake language in CMake projects, but also discover what makes them maintainable, elegant, and clean. The book also focuses on the structure of source directories, building targets, and packages. As you progress, you’ll learn how to compile and link executables and libraries, how those processes work, and how to optimize builds in CMake for the best results. You'll understand how to use external dependencies in your project – third-party libraries, testing frameworks, program analysis tools, and documentation generators. Finally, you'll get to grips with exporting, installing, and packaging for internal and external purposes. By the end of this book, you’ll be able to use CMake confidently on a professional level.
Table of Contents (18 chapters)
1
Section 1: Introducing CMake
5
Section 2: Building With CMake
10
Section 3: Automating With CMake

Navigating the project files

CMake uses quite a few files to manage its projects. Let's attempt to get a general idea of what each file does before tinkering with the contents. It's important to realize, that even though a file contains CMake language commands, it's not certain that it's meant for developers to edit. Some files are generated to be used by subsequent tools, and any changes made to those files will be written over at some stage. Other files are meant for advanced users to adjust your project to their individual needs. Finally, there are some temporary files that provide valuable information in specific contexts. This section will also specify which of them should be in the ignore file of your version control system.

The source tree

This is the directory where your project will live (it is also called the project root). It contains all of the C++ sources and CMake project files.

Here are the key takeaways of this directory:

  • It is required that you provide a CMakeLists.txt configuration file in its top directory.
  • It should be managed with a VCS such as git.
  • The path to this directory is given by the user with a -S argument of the cmake command.
  • Avoid hardcoding any absolute paths to the source tree in your CMake code – users of your software can store the project under a different path.

The build tree

CMake uses this directory to store everything that gets generated during the build: the artifacts of the project, the transient configuration, the cache, the build logs, and anything that your native build tool will create. Alternative names for this directory include build root and binary tree.

Here are the key takeaways of this directory:

  • Your binary files will be created here, such as executables and libraries, along with object files and archives used for final linking.
  • Don't add this directory to your VCS – it's specific to your system. If you decide to put it inside the source tree, make sure to add it to the VCS ignore file.
  • CMake recommends out-of-source builds or builds that produce artifacts in a directory that is separate from all source files. This way, we can avoid polluting our project's source tree with temporary, system-specific files (or in-source builds).
  • It is specified with -B or as a last argument to the cmake command if you have provided a path to the source, for example, cmake -S ../project ./.
  • It's recommended that your projects include an installation stage that allows you to put the final artifacts in the correct place in the system, so all temporary files used for building can be removed.

Listfiles

Files that contain the CMake language are called listfiles and can be included one in another, by calling include() and find_package(), or indirectly with add_subdirectory():

  • CMake doesn't enforce consistent naming for these files, but usually, they have a .cmake extension.
  • A very important naming exception is a file called CMakeLists.txt, which is the first file to be executed in the configuration stage. It is required at the top of the source tree.
  • As CMake walks the source tree and includes different listfiles, the following variables are set: CMAKE_CURRENT_LIST_DIR, CMAKE_CURRENT_LIST_FILE, CMAKE_PARENT_LIST_FILE, and CMAKE_CURRENT_LIST_LINE.

CMakeLists.txt

CMake projects are configured with CMakeLists.txt listfiles. You are required to provide at least one in the root of the source tree. Such a top-level file is the first to be executed in the configuration stage, and it should contain at least two commands:

  • cmake_minimum_required(VERSION <x.xx>): Sets an expected version of CMake (and implicitly tells CMake what policies to apply with regard to legacy behaviors).
  • project(<name> <OPTIONS>): This is used to name the project (the provided name will be stored in the PROJECT_NAME variable) and specify the options to configure it (we'll discuss this further in the Chapter 2, The CMake Language).

As your software grows, you might want to partition it into smaller units that can be configured and reasoned about separately. CMake supports this through the notion of subdirectories and their own CMakeLists.txt files. Your project structure might look similar to the following example:

CMakeLists.txt
api/CMakeLists.txt
api/api.h
api/api.cpp

A very simple CMakeLists.txt file can then be used to bring it all together:

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(app)
message("Top level CMakeLists.txt")
add_subdirectory(api)

The main aspects of the project are covered in the top-level file: managing the dependencies, stating the requirements, and detecting the environment. In this file, we also have an add_subdirectory(api) command to include another CMakeListst.txt file from the api directory to perform steps that are specific to the API part of our application.

CMakeCache.txt

Cache variables will be generated from listfiles and stored in CMakeCache.txt when the configure stage is run for the first time. This file resides in the root of the build tree and has a fairly simple format:

# This is the CMakeCache file.
# For build in directory:
  c:/Users/rapha/Desktop/CMake/empty_project/build
# It was generated by CMake: C:/Program
  Files/CMake/bin/cmake.exe
# You can edit this file to change values found and used by
  cmake.
# If you do want to change a value, simply edit, save, and
  exit the editor.
# The syntax for the file is as follows:
# KEY:TYPE=VALUE
# KEY is the name of a variable in the cache.
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT
  TYPE!.
# VALUE is the current value for the KEY.
########################
# EXTERNAL cache entries
########################
//Flags used by the CXX compiler during DEBUG builds.
CMAKE_CXX_FLAGS_DEBUG:STRING=/MDd /Zi /Ob0 /Od /RTC1
// ... more variables here ...
########################
# INTERNAL cache entries
########################
//Minor version of cmake used to create the current loaded
  cache
CMAKE_CACHE_MINOR_VERSION:INTERNAL=19
// ... more variables here ...

As you can observe from comments in the heading, this format is pretty self-explanatory. Cache entries in the EXTERNAL section are meant for users to modify, while the INTERNAL section is managed by CMake. Note that it's not recommended that you change them manually.

Here are several key takeaways to bear in mind:

  • You can manage this file manually, by calling cmake (please refer to Options for caching in the Mastering the command line section), or through ccmake/cmake-gui.
  • You can reset the project to its default configuration by deleting this file; it will be regenerated from the listfiles.
  • Cache variables can be read and written from the listfiles. Sometimes, variable reference evaluation is a bit complicated; however, we will cover that in more detail in Chapter 2, The CMake Language.

The Config-files for packages

A big part of the CMake ecosystem includes the external packages that projects can depend on. They allow developers to use libraries and tools in a seamless, cross-platform way. Packages that support CMake should provide a configuration file so that CMake understands how to use them.

We'll learn how to write those files in Chapter 11, Installing and Packaging. Meanwhile, here's a few interesting details to bear in mind:

  • Config-files (original spelling) contain information regarding how to use the library binaries, headers, and helper tools. Sometimes, they expose CMake macros to use in your project.
  • Use the find_package() command to include packages.
  • CMake files describing packages are named <PackageName>-config.cmake and <PackageName>Config.cmake.
  • When using packages, you can specify which version of the package you need. CMake will check this in the associated <Config>Version.cmake file.
  • Config-files are provided by package vendors supporting the CMake ecosystem. If a vendor doesn't provide such a config-file, it can be replaced with a Find-module (original spelling).
  • CMake provides a package registry to store packages system-wide and for each user.

The cmake_install.cmake, CTestTestfile.cmake, and CPackConfig.cmake files

These files are generated in the build tree by the cmake executable in the generation stage. As such, they shouldn't be edited manually. CMake uses them as a configuration for the cmake install action, CTest, and CPack. If you're implementing an in-source build (not recommended), it's probably a good idea to add them to the VCS ignore file.

CMakePresets.json and CMakeUserPresets.json

The configuration of the projects can become a relatively busy task when we need to be specific about things such as cache variables, chosen generators, the path of the build tree, and more – especially when we have more than one way of building our project. This is where the presets come in.

Users can choose presets through the GUI or use the command line to --list-presets and select a preset for the buildsystem with the --preset=<preset> option. You'll find more details in the Mastering the command line section of this chapter.

Presets are stored in the same JSON format in two files:

  • CMakePresets.json: This is meant for project authors to provide official presets.
  • CMakeUserPresets.json: This is dedicated to users who want to customize the project configuration to their liking (you can add it to your VCS ignore file).

Presets are project files, so their explanation belongs here. However, they are not required in projects, and they only become useful when we have completed the initial setup. So, feel free to skip to the next section and return here later, if needed:

chapter-01/02-presets/CMakePresets.json

{
  "version": 1,
  "cmakeMinimumRequired": {
    "major": 3, "minor": 19, "patch": 3
  },
  "configurePresets": [ ],
  "vendor": {
    "vendor-one.com/ExampleIDE/1.0": {
      "buildQuickly": false
    }
  }
}

CMakePresets.json specifies the following root fields:

  • Version: This is required, and it is always 1.
  • cmakeMinimumRequired: This is optional. It specifies the CMake version in form of a hash with three fields: major, minor, and patch.
  • vendor: An IDE can use this optional field to store its metadata. It's a map keyed with a vendor domain and slash-separated path. CMake virtually ignores this field.
  • configurePresets: This is an optional array of available presets.

Let's add two presets to our configurePresets array:

chapter-01/02-presets/CMakePresets.json : my-preset

{
  "name": "my-preset",
  "displayName": "Custom Preset",
  "description": "Custom build - Ninja",
  "generator": "Ninja",
  "binaryDir": "${sourceDir}/build/ninja",
  "cacheVariables": {
    "FIRST_CACHE_VARIABLE": {
      "type": "BOOL", "value": "OFF"
    },
    "SECOND_CACHE_VARIABLE": "Ninjas rock"
  },
  "environment": {
    "MY_ENVIRONMENT_VARIABLE": "Test",
    "PATH": "$env{HOME}/ninja/bin:$penv{PATH}"
  },
  "vendor": {
    "vendor-one.com/ExampleIDE/1.0": {
      "buildQuickly": true
    }
  }
},

This file supports a tree-like structure, where children presets inherit properties from multiple parent presets. This means that we can create a copy of the preceding preset and only override the fields we need. Here's an example of what a child preset might look like:

chapter-01/02-presets/CMakePresets.json : my-preset-multi

{
  "name": "my-preset-multi",
  "inherits": "my-preset",
  "displayName": "Custom Ninja Multi-Config",
  "description": "Custom build - Ninja Multi",
  "generator": "Ninja Multi-Config"
}

Note

The CMake documentation only labels a few fields as explicitly required. However, there are some other fields that are labeled as optional, which must be provided either in the preset or inherited from its parent.

Presets are defined as maps with the following fields:

  • name: This is a required string that identifies the preset. It has to be machine-friendly and unique across both files.
  • Hidden: This is an optional Boolean hiding the preset from the GUI and command-line list. Such a preset can be a parent of another and isn't required to provide anything but its name.
  • displayName: This is an optional string with a human-friendly name.
  • description: This is an optional string describing the preset.
  • Inherits: This is an optional string or array of preset names to inherit from. Values from earlier presets will be preferred in the case of conflicts, and every preset is free to override any inherited field. Additionally, CMakeUserPresets.json can inherit from project presets but not the other way around.
  • Vendor: This is an optional map of vendor-specific values. It follows the same convention as a root-level vendor field.
  • Generator: This is a required or inherited string that specifies a generator to use for the preset.
  • architecture and toolset: These are optional fields for configuring generators that support these options (mentioned in the Generating a project buildsystem section). Each field can simply be a string or a hash with value and strategy fields, where strategy is either set or external. The strategy field, configured to set, will set the value and produce an error if the generator doesn't support this field. Configuring external means that the field value is set for an external IDE, and CMake should ignore it.
  • binaryDir: This is a required or inherited string that provides a path to the build tree directory (which is absolute or relative to the source tree). It supports macro expansion.
  • cacheVariables: This is an optional map of cache variables where keys denote variable names. Accepted values include null, "TRUE", "FALSE", a string value, or a hash with an optional type field and a required value field. value can be a string value of either "TRUE" or "FALSE". Cache variables are inherited with a union operation unless the value is specified as null – then, it remains unset. String values support macro expansion.
  • Environment: This is an optional map of environment variables where keys denote variable names. Accepted values include null or string values. Environment variables are inherited with a union operation unless the value is specified as null – then, it remains unset. String values support macro expansion, and variables might reference each other in any order, as long as there is no cyclic reference.

The following macros are recognized and evaluated:

  • ${sourceDir}: This is the path to the source tree.
  • ${sourceParentDir}: This is the path to the source tree's parent directory.
  • ${sourceDirName}: This is the last filename component of ${sourceDir}. For example, for /home/rafal/project, it would be project.
  • ${presetName}: This is the value of the preset's name field.
  • ${generator}: This is the value of the preset's generator field.
  • ${dollar}: This is a literal dollar sign ($).
  • $env{<variable-name>}: This is an environment variable macro. It will return the value of the variable from the preset if defined; otherwise, it will return the value from the parent environment. Remember that variable names in presets are case-sensitive (unlike in Windows environments).
  • $penv{<variable-name>}: This option is just like $env but always returns values from the parent environment. This allows you to resolve issues with circular references that are not allowed in the environment variables of the preset.
  • $vendor{<macro-name>}: This enables vendors to insert their own macros.

Ignoring files in Git

There are many VCSs; one of the most popular types out there is Git. Whenever we start a new project, it is good to make sure that we only check in to the repository files that need to be there. Project hygiene is easier to maintain if we just add generated, user, or temporary files to the .gitignore file. In this way, Git knows to automatically skip them when building new commits. Here's the file that I use in my projects:

chapter-01/01-hello/.gitignore

# If you put build tree in the source tree add it like so:
build_debug/
build_release/
# Generated and user files
**/CMakeCache.txt
**/CMakeUserPresets.json
**/CTestTestfile.cmake
**/CPackConfig.cmake
**/cmake_install.cmake
**/install_manifest.txt
**/compile_commands.json

Using the preceding file in your projects will allow for more flexibility for you and other contributors and users.

The unknown territory of project files has now been charted. With this map, you'll soon be able to write your own listfiles, configure the cache, prepare presets, and more. Before you sail on the open sea of project writing, let's take a look at what other kinds of self-contained units you can create with CMake.