
Modern CMake for C++
By :

Quite a lot of files and directories make up CMake projects. Let’s get a general idea of what each one does so we can start tinkering with them. There are several general categories:
This section will also suggest which files you can put in the ignore file of your Version Control System (VCS).
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 most important takeaways from this directory:
CMakeLists.txt
configuration file.-S
argument of the cmake
command when generating a buildsystem.It’s a good idea to initialize a repository in this directory, perhaps using a VCS like Git
.
CMake creates this directory in a path specified by the user. It will store the buildsystem and everything that gets created during the build: the artifacts of the project, the transient configuration, the cache, the build logs, and the output of your native build tool (like GNU Make). Alternative names for this directory include build root and binary tree.
Key things to remember:
-B
to the cmake
command when generating a buildsystem.Don’t add this directory to your VCS – every user picks one for themselves. If you have a good reason to do an in-source build, make sure to add this directory to the VCS ignore file (like .gitignore
).
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 any naming rules for these files but, by convention, they have a .cmake
extension.
CMake projects are configured with a CMakeLists.txt
listfile (notice that due to historical reasons, this file has an unconventional extension). This file is required at the top of the source tree of every project and is the first to be executed in the configuration stage.
A top-level CMakeLists.txt
should contain at least two commands:
cmake_minimum_required(VERSION <x.xx>)
: Sets an expected version of CMake and tells CMake how to handle legacy behaviors with policiesproject(<name> <OPTIONS>)
: Names the project (the provided name will be stored in the PROJECT_NAME
variable) and specifies the options to configure it (more on this in 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 with their own CMakeLists.txt
files. Your project structure might look similar to the following example:
myProject/CMakeLists.txt
myProject/api/CMakeLists.txt
myProject/api/api.h
myProject/api/api.cpp
A very simple top-level CMakeLists.txt
file can then be used to bring it all together:
cmake_minimum_required(VERSION 3.26)
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. We also have an add_subdirectory(api)
command to include another CMakeListst.txt
file from the api
subdirectory to perform steps that are specific to the API part of our application.
Cache variables will be generated from the 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 (some lines removed for brevity):
# This is the CMakeCache file.
# For build in directory: /root/build tree
# It was generated by CMake: /usr/local/bin/cmake
# 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 see from the header comments, 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.
Here are a couple of key takeaways to bear in mind:
cmake
(see Options for caching in the Mastering the command line section of this chapter), or through ccmake
or cmake-gui
.Cache variables can be read and written from the listfiles. Sometimes, variable reference evaluation is a bit complicated; we will cover that in more detail in Chapter 2, The CMake Language.
A big part of the CMake ecosystem is the external packages that projects can depend on. They provide libraries and tools in a seamless, cross-platform way. Package authors that want to provide CMake support will ship it with a CMake package configuration file.
We’ll learn how to write those files in Chapter 14, Installing and Packaging. Meanwhile, here’s a few interesting details to bear in mind:
<PackageName>-config.cmake
or <PackageName>Config.cmake
.find_package()
command to include packages.If a specific version of the package is required, CMake will check this against the associated <PackageName>-config-version.cmake
or <PackageName>ConfigVersion.cmake
.
If a vendor doesn’t provide a config file for the package, sometimes, the configuration is bundled with the CMake itself or can be provided in the project with Find-module (original spelling).
Many 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.
Files that you may encounter are:
cmake_install.cmake
CTestTestfile.cmake
CPackConfig.cmake
If you’re implementing an in-source build, it’s probably a good idea to add them to the VCS ignore file.
Other formats used by CMake are JavaScript Object Notation (JSON) and Yet Another Markup Language (YAML). These files are introduced as an interface to communicate with external tools (like IDEs) or to provide configuration that can be easily generated and parsed.
The advanced 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 – instead of manually configuring these values through the command line, we can just provide a file that stores all the details and ship it with the project. Since CMake 3.25, presets also allow us to configure workflows, which tie stages (configure, build, test, and package) into a named list of steps to execute.
As mentioned in the Mastering the command line section of this chapter, users can choose presets through the GUI or use the command --list-presets
and select a preset for the buildsystem with the --preset=<preset>
option.
Presets are stored 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 not required in projects and only become useful in advanced scenarios. See Chapter 16, Writing CMake Presets, for details.
CMake 3.14 introduced an API that allows external tools to query the buildsystem information: paths to generated files, cache entries, toolchains, and such. We only mention this very advanced topic to avoid confusion if you come across a file-based API phrase in the documentation. The name suggests how it works: a JSON file with a query has to be placed in a special path inside the build tree. CMake will read this file during the buildsystem generation and write a response to another file, so it can be parsed by external applications.
The file-based API was introduced to replace a deprecated mechanism called server mode (or cmake-server
), which was finally removed in CMake 3.26.
Since version 3.26, CMake will provide a structured log file for really advanced debugging of the configure stage at:
<build tree>/CMakeFiles/CMakeConfigureLog.yaml
It’s one of these features that you don’t normally need to pay attention to – until you do.
There are many VCSs; one of the most popular out there is Git. Whenever we start a new project, it is good to make sure that we only add the necessary files to the repository. Project hygiene is easier to maintain if we specify unwanted files in the .gitignore
file. For example, we might exclude files that are generated, user-specific, or temporary.
Git will automatically skip them when forming new commits. Here’s the file that I use in my projects:
ch01/01-hello/.gitignore
CMakeUserPresets.json
# If in-source builds are used, exclude their output 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
Now you hold a map to the sea of project files. Some files are very important and you will use them all the time – others, not so much. While it might seem like a waste to learn about them, it can be invaluable to know where not to look for answers. In any case, one last question for this chapter remains: what other self-contained units can you create with CMake?
Change the font size
Change margin width
Change background colour