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:

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: