CMake#
CMake has evolved significantly, and modern CMake (3.12+) emphasizes target-based configuration over global variables. This cheatsheet covers contemporary best practices for building C++ projects, managing dependencies, and creating reusable packages.
CMake CLI Quick Reference#
Common command-line operations for configuring, building, and managing CMake projects.
Configure and Build#
# Configure (generate build system)
cmake -B build # Configure into build/
cmake -B build -S src # Specify source directory
cmake -B build -G Ninja # Use Ninja generator
cmake -B build -G "Unix Makefiles" # Use Make generator
# Build
cmake --build build # Build the project
cmake --build build -j 8 # Parallel build (8 jobs)
cmake --build build --target app # Build specific target
cmake --build build --clean-first # Clean before building
# Install
cmake --install build # Install to default prefix
cmake --install build --prefix /opt # Install to custom prefix
Build Types and Options#
The -D flag sets CMake cache variables. These persist in CMakeCache.txt
and control build configuration.
# Build types
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake -B build -DCMAKE_BUILD_TYPE=MinSizeRel
# Compiler selection
cmake -B build -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
cmake -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
# Install prefix
cmake -B build -DCMAKE_INSTALL_PREFIX=/usr/local
cmake -B build -DCMAKE_INSTALL_PREFIX=$HOME/.local
# Project options (defined by option() in CMakeLists.txt)
cmake -B build -DBUILD_TESTS=ON
cmake -B build -DBUILD_SHARED_LIBS=ON
cmake -B build -DENABLE_FEATURE_X=OFF
# Multiple options
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_STANDARD=17 \
-DBUILD_SHARED_LIBS=ON \
-DBUILD_TESTS=OFF
Inspect and Debug#
# List available targets
cmake --build build --target help
# List cache variables
cmake -B build -L # List non-advanced variables
cmake -B build -LA # List all variables
cmake -B build -LAH # List all with help strings
# Print variable value
cmake -B build -N -L | grep CMAKE_CXX
# Verbose build output
cmake --build build --verbose
cmake --build build -- VERBOSE=1 # Make-specific
Testing and Packaging#
# Run tests
ctest --test-dir build # Run all tests
ctest --test-dir build -V # Verbose output
ctest --test-dir build -j 8 # Parallel testing
ctest --test-dir build -R "regex" # Run matching tests
ctest --test-dir build --rerun-failed
# Create package
cmake --build build --target package
cpack --config build/CPackConfig.cmake
Project Setup#
Every CMake project starts with basic configuration. Modern CMake recommends setting the C++ standard on targets rather than globally, which provides better control and avoids polluting the global namespace.
Minimal Project#
The simplest CMake project requires only a few lines. The cmake_minimum_required
ensures compatibility, while project defines metadata used throughout the build.
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
add_executable(app main.cpp)
target_compile_features(app PRIVATE cxx_std_17)
Modern C++ Standard Setting#
Using target_compile_features is preferred over global CMAKE_CXX_STANDARD
because it applies only to specific targets and can require specific language features.
# Per-target (preferred)
target_compile_features(app PRIVATE cxx_std_17)
target_compile_features(app PRIVATE cxx_std_20)
# Or require specific features
target_compile_features(app PRIVATE cxx_constexpr cxx_lambdas)
# Global (legacy, avoid)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
Build Commands#
CMake separates configuration from building. The -B flag specifies the build
directory, keeping source and build files separate (out-of-source build).
# Configure and build
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
# Common build types
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
# Specify generator
cmake -B build -G Ninja
cmake -B build -G "Unix Makefiles"
# Install
cmake --install build --prefix /usr/local
Targets and Properties#
Modern CMake is target-centric. Use target_* commands instead of global variables
like CMAKE_CXX_FLAGS. This approach provides better encapsulation and makes
dependencies explicit.
Executables and Libraries#
CMake supports multiple library types. Use STATIC for archives, SHARED for
dynamic libraries, and INTERFACE for header-only libraries.
# Executable
add_executable(app main.cpp utils.cpp)
# Static library
add_library(mylib STATIC lib.cpp)
# Shared library
add_library(mylib SHARED lib.cpp)
# Header-only library
add_library(mylib INTERFACE)
# Let CMake decide (BUILD_SHARED_LIBS)
add_library(mylib lib.cpp)
Include Directories#
The target_include_directories command specifies where the compiler should look
for header files. Generator expressions like $<BUILD_INTERFACE> and
$<INSTALL_INTERFACE> allow different paths during build and after installation.
This is essential for creating relocatable packages that work both in the build tree
and when installed to a system location.
target_include_directories(mylib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
Compile Options and Definitions#
Compile flags and preprocessor definitions should be set per-target. Generator expressions enable conditional flags based on build type or compiler.
# Compile flags
target_compile_options(app PRIVATE
-Wall -Wextra -Wpedantic
$<$<CONFIG:Debug>:-g -O0>
$<$<CONFIG:Release>:-O3>
)
# Preprocessor definitions
target_compile_definitions(app PRIVATE
APP_VERSION="${PROJECT_VERSION}"
$<$<CONFIG:Debug>:DEBUG_MODE>
)
Linking#
The target_link_libraries command specifies libraries to link against and
automatically handles transitive dependencies. When you link to a library, CMake
propagates that library’s PUBLIC include directories and compile definitions to
your target. Libraries with :: in their name (like Threads::Threads) are
imported targets that carry all necessary usage requirements.
target_link_libraries(app
PRIVATE
mylib
Threads::Threads
PUBLIC
Boost::boost
)
PUBLIC vs PRIVATE vs INTERFACE#
These keywords control how properties propagate to dependent targets:
PRIVATE: The property applies only to the target itself, not to anything that links to it. Use for implementation details like source files or internal dependencies.PUBLIC: The property applies to both the target and anything that links to it. Use for headers that consumers need to include.INTERFACE: The property applies only to targets that link to this one, not to the target itself. Use for header-only libraries.
Understanding these keywords is crucial for creating well-encapsulated libraries that don’t leak unnecessary dependencies.
# PRIVATE: only for this target
# PUBLIC: for this target and dependents
# INTERFACE: only for dependents (header-only libs)
add_library(mylib STATIC lib.cpp)
target_include_directories(mylib PUBLIC include/) # Consumers need headers
target_link_libraries(mylib PRIVATE internal_dep) # Implementation detail
target_compile_definitions(mylib INTERFACE USE_MYLIB) # Define for consumers
Dependency Management#
Modern CMake provides multiple ways to manage external dependencies. FetchContent is preferred for most cases as it integrates seamlessly with the build.
FetchContent (Recommended)#
FetchContent downloads and configures dependencies at configure time, making them
available as regular CMake targets. Unlike ExternalProject (which builds at build time),
FetchContent integrates dependencies into your build system so they can be used
immediately with target_link_libraries. This is the recommended approach for
most projects as it provides the simplest integration.
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.1.1
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(app PRIVATE fmt::fmt)
Multiple Dependencies#
Multiple dependencies can be declared together and made available with a single
FetchContent_MakeAvailable call for efficiency.
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
)
FetchContent_MakeAvailable(googletest nlohmann_json)
target_link_libraries(app PRIVATE nlohmann_json::nlohmann_json)
target_link_libraries(tests PRIVATE GTest::gtest_main)
find_package#
Use find_package for system-installed libraries or when you want to use
pre-built binaries instead of building from source. CMake supports two modes:
Config mode (preferred): The library provides its own CMake config files (e.g.,
fmtConfig.cmake). These are typically installed alongside the library.Module mode: CMake provides a
Find*.cmakemodule that knows how to locate the library. Used for libraries that don’t provide CMake support.
Config mode is preferred because the library author knows best how to expose their targets.
# Config mode (preferred, library provides CMake config)
find_package(fmt REQUIRED CONFIG)
target_link_libraries(app PRIVATE fmt::fmt)
# Module mode (CMake provides Find*.cmake)
find_package(Threads REQUIRED)
find_package(OpenSSL REQUIRED)
target_link_libraries(app PRIVATE
Threads::Threads
OpenSSL::SSL
OpenSSL::Crypto
)
# Optional dependency
find_package(Boost COMPONENTS filesystem)
if(Boost_FOUND)
target_link_libraries(app PRIVATE Boost::filesystem)
endif()
Common find_package Examples#
These examples show how to link against commonly used libraries that provide CMake config files or have built-in Find modules.
# Boost (header-only and compiled components)
find_package(Boost 1.70 REQUIRED COMPONENTS system filesystem)
target_link_libraries(app PRIVATE Boost::system Boost::filesystem)
# Boost header-only
find_package(Boost REQUIRED)
target_link_libraries(app PRIVATE Boost::boost)
# Protobuf
find_package(Protobuf REQUIRED)
target_link_libraries(app PRIVATE protobuf::libprotobuf)
# CURL
find_package(CURL REQUIRED)
target_link_libraries(app PRIVATE CURL::libcurl)
# OpenSSL
find_package(OpenSSL REQUIRED)
target_link_libraries(app PRIVATE OpenSSL::SSL OpenSSL::Crypto)
# ZLIB
find_package(ZLIB REQUIRED)
target_link_libraries(app PRIVATE ZLIB::ZLIB)
pkg-config Fallback#
For libraries that don’t provide CMake config files, pkg-config can be used as
a fallback. The IMPORTED_TARGET option creates a proper CMake target.
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBSSH2 REQUIRED IMPORTED_TARGET libssh2)
target_link_libraries(app PRIVATE PkgConfig::LIBSSH2)
Project Structure#
Organizing larger projects with subdirectories keeps code modular and maintainable. Each subdirectory can have its own CMakeLists.txt defining local targets.
Subdirectories#
A typical project structure separates source, library, and test code into distinct directories, each with its own CMakeLists.txt.
project/
├── CMakeLists.txt
├── src/
│ ├── CMakeLists.txt
│ └── main.cpp
├── lib/
│ ├── CMakeLists.txt
│ ├── mylib.cpp
│ └── mylib.h
└── tests/
├── CMakeLists.txt
└── test_mylib.cpp
Root CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
option(BUILD_TESTS "Build tests" ON)
add_subdirectory(lib)
add_subdirectory(src)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
lib/CMakeLists.txt:
add_library(mylib mylib.cpp)
add_library(MyProject::mylib ALIAS mylib)
target_include_directories(mylib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
target_compile_features(mylib PUBLIC cxx_std_17)
src/CMakeLists.txt:
add_executable(app main.cpp)
target_link_libraries(app PRIVATE MyProject::mylib)
Generator Expressions#
Generator expressions are evaluated during build system generation, enabling
conditional configuration based on build type, compiler, platform, and more.
They use the syntax $<...>.
Common Patterns#
Generator expressions enable conditional logic without if() statements, making CMakeLists.txt cleaner and more portable.
# Build type
$<$<CONFIG:Debug>:-g>
$<$<CONFIG:Release>:-O3>
$<IF:$<CONFIG:Debug>,-g,-O3>
# Compiler
$<$<CXX_COMPILER_ID:GNU>:-Wall>
$<$<CXX_COMPILER_ID:Clang>:-Weverything>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
# Platform
$<$<PLATFORM_ID:Linux>:-pthread>
$<$<PLATFORM_ID:Windows>:-DWIN32>
# Build vs install paths
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
Cross-Platform Compile Options#
Generator expressions make it easy to set compiler-specific flags without platform-specific if() blocks.
target_compile_options(app PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Werror>
)
Installation and Packaging#
Making your library installable allows other projects to use it via find_package.
This requires exporting targets and creating config files.
Basic Installation#
The install command copies built artifacts to the installation prefix (default
/usr/local on Unix). Including GNUInstallDirs provides standard directory
variables (CMAKE_INSTALL_BINDIR, CMAKE_INSTALL_LIBDIR, etc.) that follow
platform conventions, ensuring your project installs correctly on different systems.
include(GNUInstallDirs)
install(TARGETS mylib app
EXPORT MyProjectTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
Export for find_package#
To allow other CMake projects to use your library with find_package(MyProject),
you need to export your targets and create config files. This involves:
Exporting targets to a
*Targets.cmakefileCreating a
*Config.cmakethat includes the targets fileOptionally creating a
*ConfigVersion.cmakefor version checking
The namespace (e.g., MyProject::) prevents target name collisions and signals
that the target is imported.
install(EXPORT MyProjectTargets
FILE MyProjectTargets.cmake
NAMESPACE MyProject::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)
include(CMakePackageConfigHelpers)
configure_package_config_file(
cmake/MyProjectConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)
cmake/MyProjectConfig.cmake.in:
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# find_dependency(Boost REQUIRED) # if needed
include("${CMAKE_CURRENT_LIST_DIR}/MyProjectTargets.cmake")
CPack for Distribution#
CPack generates distributable packages (ZIP, TGZ, DEB, RPM) from your CMake project.
set(CPACK_PACKAGE_NAME ${PROJECT_NAME})
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_VENDOR "Your Name")
set(CPACK_GENERATOR "TGZ;ZIP")
# For DEB packages
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "your@email.com")
include(CPack)
cmake --build build --target package
Testing#
CTest is CMake’s built-in testing framework that integrates with CMake to provide
test discovery and execution. For GoogleTest users, the gtest_discover_tests
function automatically registers all TEST() macros as individual CTest tests,
enabling parallel execution and better reporting.
enable_testing()
add_executable(tests test_main.cpp)
target_link_libraries(tests PRIVATE mylib GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(tests)
# Or manual test registration
add_test(NAME MyTest COMMAND tests)
ctest --test-dir build -V
ctest --test-dir build -j$(nproc)
Useful Variables#
CMake provides many built-in variables for accessing project information, paths, and build configuration. Understanding these variables helps write portable CMakeLists.txt files that work across different environments.
# Project info
${PROJECT_NAME}
${PROJECT_VERSION}
${PROJECT_SOURCE_DIR}
${PROJECT_BINARY_DIR}
# Current directory
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_LIST_DIR}
# Install paths (use with GNUInstallDirs)
${CMAKE_INSTALL_PREFIX}
${CMAKE_INSTALL_BINDIR}
${CMAKE_INSTALL_LIBDIR}
${CMAKE_INSTALL_INCLUDEDIR}
# Build info
${CMAKE_BUILD_TYPE}
${CMAKE_CXX_COMPILER_ID}
${CMAKE_SYSTEM_NAME}
Options and Cache Variables#
Options allow users to customize the build without modifying CMakeLists.txt.
Cache variables persist across CMake invocations (stored in CMakeCache.txt)
and can be set via command line with -D. This is how users enable/disable
features, specify paths, or configure build options.
# Boolean option
option(BUILD_SHARED_LIBS "Build shared libraries" ON)
option(BUILD_TESTS "Build tests" OFF)
# Cache variable with type
set(MY_OPTION "default" CACHE STRING "Description")
set(MY_PATH "/usr/local" CACHE PATH "Install path")
# Use in CMake
if(BUILD_TESTS)
add_subdirectory(tests)
endif()
cmake -B build -DBUILD_TESTS=ON -DMY_OPTION=custom
Presets (CMake 3.19+)#
CMakePresets.json provides reproducible, shareable build configurations that can be version-controlled with your project. It eliminates the need to remember complex command-line options and ensures all developers use consistent settings. Presets can define configure options, build settings, and test configurations.
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug"
},
{
"name": "release",
"configurePreset": "release"
}
]
}
cmake --preset debug
cmake --build --preset debug
CUDA Support#
CMake has native CUDA support since version 3.8, treating CUDA as a first-class
language alongside C and C++. This eliminates the need for the legacy
FindCUDA module. The CUDAToolkit package (CMake 3.17+) provides imported
targets for CUDA libraries like cuBLAS and cuFFT.
Basic CUDA Project#
Enable CUDA by adding it to the LANGUAGES list. CMake automatically finds
the CUDA compiler (nvcc) and configures .cu file compilation.
cmake_minimum_required(VERSION 3.18)
project(cuda_example LANGUAGES CXX CUDA)
find_package(CUDAToolkit REQUIRED)
add_executable(app main.cu)
target_compile_features(app PRIVATE cuda_std_17)
target_link_libraries(app PRIVATE CUDA::cudart)
Mixed C++/CUDA#
For projects with both .cpp and .cu files, enable separable compilation
to allow device code in different translation units to call each other.
add_library(kernels kernels.cu)
set_target_properties(kernels PROPERTIES
CUDA_SEPARABLE_COMPILATION ON
CUDA_ARCHITECTURES "70;80;86;90" # Volta, Ampere, Hopper
)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE kernels CUDA::cudart)
CUDA Architectures#
CUDA_ARCHITECTURES specifies GPU compute capabilities to target. Use
semicolon-separated values for multiple architectures. Common values:
70: Volta (V100)
75: Turing (RTX 20xx)
80: Ampere (A100)
86: Ampere (RTX 30xx)
89: Ada Lovelace (RTX 40xx)
90: Hopper (H100)
# Per-target (preferred)
set_target_properties(app PROPERTIES CUDA_ARCHITECTURES "80;86;90") # Ampere, Hopper
# Global default
set(CMAKE_CUDA_ARCHITECTURES "80;86;90")
# Detect host GPU at configure time (CMake 3.24+)
set(CMAKE_CUDA_ARCHITECTURES native)
CUDA Libraries#
The CUDAToolkit package provides imported targets for NVIDIA libraries.
find_package(CUDAToolkit REQUIRED)
target_link_libraries(app PRIVATE
CUDA::cudart # Runtime API
CUDA::cublas # Dense linear algebra
CUDA::cufft # Fast Fourier transforms
CUDA::curand # Random number generation
CUDA::cusolver # Direct solvers
CUDA::cusparse # Sparse linear algebra
CUDA::nvml # GPU monitoring and management
)