More CMake Tips
22 November 2025
I've written about modern CMake in earlier blog post. Here are a few
changes to the earlier advice that aim to make your CMakeLists.txt more robust
along with a few other tips.
Presets
CMake presets were introduced in version 3.19.0 and they "allow users to
specify common configure options and share them with others". By moving
everything that is compiler and/or machine specific from CMakeLists.txt into
CMakePresets.json, CMake scripts are shorter, cleaner, and more welcoming to
build configurations that were not anticipated by the project author. Presets
present the users with the typical ways of building the project and avoiding
enforcing things that are not required in order to build the project.
Firstly, warning flags do not belong into CMakeLists.txt. They are
compiler specific and not required to build the project. Instead of a target
that defines warnings as I recommended earlier, warning flags should live in the
preset files.
The C++ standard specification is also moved to the preset files. It is better
to not enforce the standard in CMakeLists.txt so that the users that wish to
attempt to build the project with earlier or newer standards are able to do so.
Presets are a good place to specify if the libraries built by the project are
static or shared. The BUILD_SHARED_LIBS and CMAKE_POSITION_INDEPENDENT_CODE
variables offer control over the library type. Therefore, add_library()
calls for installable libraries should not specify their type.
My starting point for a CMakePresets.json looks something like this:
{
"version": 8,
"configurePresets": [
{
"name": "base",
"hidden": true,
"binaryDir": "build",
"cacheVariables": {
"CMAKE_CXX_STANDARD": "17",
"CMAKE_CXX_STANDARD_REQUIRED": true,
"CMAKE_CXX_EXTENSIONS": false,
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": true,
"CMAKE_POSITION_INDEPENDENT_CODE": false,
"BUILD_SHARED_LIBS": false
}
},
{
"name": "gcc",
"inherits": ["base"],
"cacheVariables": {
"CMAKE_CXX_COMPILER": "g++",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -Wpedantic -Wconversion -Wnon-virtual-dtor"
}
}
],
"buildPresets": [
{
"name": "gcc",
"configurePreset": "gcc"
}
]
}
I have a base preset that specifies settings common for all compilers. The
gcc preset inherits the base preset and additionally specifies the compiler
and the compiler flags - this is where warning flags are defined.
Finally, I also specify build presets so that I configure and build the project with a simple one-liner:
PRESET=gcc; cmake --preset $PRESET && cmake --build --preset $PRESET
Non-toy projects also specify test presets so that I am able to run tests by invoking CTest after a successful build like so:
ctest --preset $PRESET
File sets
Instead of specifying sources and headers in the
add_executable()/add_library() calls and specifying include directories with
target_include_directories(), a better approach is to use target_sources()
with file sets. File sets enable CMake to automatically set the appropriate
include directories and, when using install() to install the target, install
the headers at the appropriate path.
Here is an example on a typical library project structure:
foo
├── CMakeLists.txt
├── CMakePresets.json
├── src
│ └── foo.cpp
└── include
└── foo.hpp
The CMakeLists.txt would look something like this:
cmake_minimum_required(VERSION "3.23.0")
project(foo VERSION "0.1.0" LANGUAGES "CXX")
add_library(foo)
target_sources(foo
PRIVATE
"src/foo.cpp"
PUBLIC
FILE_SET HEADERS
BASE_DIRS "include"
FILES
"include/foo.hpp")
install(TARGETS foo)
More environment variables
In the previous blog post on CMake, I listed environment variables that set reasonable CMake defaults for my needs. Here are a couple more:
export CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)"
export CMAKE_INSTALL_PARALLEL_LEVEL="$(nproc)"
export CTEST_PARALLEL_LEVEL="$(nproc)"
export CTEST_PROGRESS_OUTPUT='true'
export CTEST_OUTPUT_ON_FAILURE='true'
These make build, install, and test CMake actions run in parallel on the number processing units available on the machine. Finally, I have CTest produce less verbose output and provide the output of the executable in case of a failed test.
Resources
Some of these these tips were inspired by the following talks:
- How to Avoid Headaches with Simple CMake, Bret Brown, C++Now 2025
- Post-Modern Cmake - From 3.0 to 4.0, Vito Gamberini, C++Now 2025
Finally, the Common Pacakge Specification seems to be the next big thing in CMake's ecosystem so keep an eye out for it. Here is one talk about it: