CMake Advanced Features
Once you have mastered the basics of CMake, advanced features can help you manage the build configuration of complex projects more flexibly.
This article covers six major topics: custom modules, multi-configuration builds, advanced package finding, custom build steps, cross-platform cross-compilation, and target property configuration.
Overall Content Overview:
| Topic | Core Capability | Typical Application Scenario |
|---|---|---|
| Custom modules and scripts | Encapsulate reusable CMake logic | Unified compiler options, custom find logic, team-shared build rules |
| Build configurations and targets | Manage multi-configuration and multi-target | Debug/Release switching, building multiple output artifacts |
| Advanced finding and configuration | Flexibly locate external dependencies | Finding specific components, passing build information via configuration files |
| Custom build steps | Extend build process | Code generation, resource processing, post-build automation |
| Cross-platform and cross-compilation | Multi-platform builds | Embedded development, compiling for different operating systems |
| Target properties and configuration | Fine-grained control over compilation and linking | Set warning levels and link paths for specific targets |
Custom CMake Modules and Scripts
When multiple CMakeLists.txt files need to repeat the same logic, encapsulating this logic into custom modules can greatly reduce code redundancy.
The essence of custom modules and scripts is to extract commonly used CMake functions, macros, and configurations into separate files for reuse across multiple projects.
Custom CMake Modules
A custom module is a.cmakefile with .cmake as its extension, which defines CMake functions and macros that can be loaded via include().
Creation steps:
| Step | Action | Description |
|---|---|---|
| 1 | Createcmake/Directory |
Create it under the project root directory to store custom modules centrally. |
| 2 | Create module file | For examplecmake/MyModule.cmakewrite custom functions |
| 3 | Extend the module search path | Register this directory via CMAKE_MODULE_PATH in CMakeLists.txt |
| 4 | Load the module | Use include() to import the module and call its functions |
Custom module example — MyModule.cmake:
Example
# Custom module: provides helper functions common to the project
# Function: uniformly set compiler warning options for all targets
# Parameter ARG_TARGET: name of the target for which warnings need to be set
function(example_set_warnings ARG_TARGET)
# Set different warning flags based on the compiler type
if(MSVC)
# MSVC compiler: enable /W4 warning level
target_compile_options(${ARG_TARGET} PRIVATE /W4)
else()
# GCC / Clang compiler: enable common warnings
target_compile_options(${ARG_TARGET} PRIVATE -Wall -Wextra -Wpedantic)
endif()
endfunction()
# Function: print basic build information for a target
function(example_print_target_info ARG_TARGET)
message(STATUS "Target name: ${ARG_TARGET}")
message(STATUS "Source file list: $<TARGET_PROPERTY:${ARG_TARGET},SOURCES>")
endfunction()
Load and use the module in CMakeLists.txt:
Example
cmake_minimum_required(VERSION 3.10)
project(MyProject CXX)
# Add the cmake/ directory to the module search path
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# Load the custom module
include(MyModule)
# Add an executable target
add_executable(MyApp main.cpp)
# Call the custom function in the module to set warning options for MyApp
example_set_warnings(MyApp)
# Call the custom function in the module to print target information
example_print_target_info(MyApp)
CMAKE_MODULE_PATHis the search path list used by CMake when looking for module files. CMake first searches the built-in module path, and if not found, searches the paths specified by CMAKE_MODULE_PATH.
Using Custom CMake Scripts
Custom scripts are similar to modules, but are typically used to perform configuration operations rather than define reusable functions.
Scripts can be loaded directly with include(), and CMake executes each command in them in order.
Create script file config.cmake:
Example
# Custom script: centrally manage project configuration options
# Set default build type to Release
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type" FORCE)
message(STATUS "Build type not specified, defaulting to Release")
endif()
# Set different compilation options based on build type
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Enable Debug mode: disable optimizations, enable debug symbols")
else()
message(STATUS "Enable Release mode: enable O2 optimization")
endif()
Invoke the script in CMakeLists.txt:
# 使用绝对路径加载脚本
include(${CMAKE_SOURCE_DIR}/config.cmake)
Build Configurations and Targets
CMake supports managing multiple build configurations (such as Debug and Release) in the same project and defining multiple build targets.
Multi-Configuration Generators
Different CMake generators handle configurations differently.
Such as Visual Studio and XcodeMulti-configuration generatorsallow switching Debug/Release in the same build directory, while Unix Makefiles, etc.Single-configuration generatorsneed to determine the build type at the configuration stage.
| Generator type | Typical generators | Configuration specification method |
|---|---|---|
| Single-configuration | Unix Makefiles、Ninja | At configuration time via-DCMAKE_BUILD_TYPE=Releasespecify |
| Multi-configuration | Visual Studio、Xcode | At build time via--config Releaseswitch |
Set default configuration in CMakeLists.txt:
# 仅对单配置生成器生效,多配置生成器会忽略此变量 set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type")
CMAKE_BUILD_TYPE is only effective for single-configuration generators. If you use Visual Studio, please switch configurations via cmake --build . --config Release or in the IDE.
Build Targets
Multiple build targets can be defined in a CMake project, and each target independently configures compilation options and link dependencies.
This allows you to produce multiple executables or multiple libraries in a single build.
Example
add_executable(MyApp src/main.cpp)
add_executable(MyTool src/tool.cpp)
# Set different compile macros for different targets to control conditional compilation
set_target_properties(MyApp PROPERTIES COMPILE_DEFINITIONS "APP_MODE")
set_target_properties(MyTool PROPERTIES COMPILE_DEFINITIONS "TOOL_MODE")
# Link different libraries for different targets
target_link_libraries(MyApp PRIVATE MyLib)
target_link_libraries(MyTool PRIVATE MyLib CLI11::CLI11)
Advanced Finding and Configuration
In addition to basic find_package usage, CMake also supports specifying components, precisely controlling versions, and passing build information through configuration file templates.
Advanced Usage of find_package
COMPONENTSThe keyword allows you to find only specific submodules of a library rather than the entire package.
This is very important for large libraries like Boost—importing on demand avoids linking unneeded components.
Example
# filesystem internally depends on system, so both need to be declared
find_package(Boost REQUIRED COMPONENTS filesystem system)
# Check whether the required components were found
if(Boost_FOUND)
message(STATUS "Boost version: ${Boost_VERSION}")
message(STATUS "Boost include directories: ${Boost_INCLUDE_DIRS}")
# Link each required component separately
target_link_libraries(MyApp PRIVATE
Boost::filesystem
Boost::system
)
endif()
Specify search paths (for non-standard installation locations):
# 方式一:通过变量指定根目录 cmake .. -DBOOST_ROOT=/path/to/custom/boost # 方式二:在 CMakeLists.txt 中预先设置 set(BOOST_ROOT "/path/to/custom/boost") find_package(Boost REQUIRED COMPONENTS filesystem)
Configuration Files and Build Options
configure_file()The directive can inject the values of CMake variables into code files, enabling configuration information to be passed at compile time.
This is very useful when you need to compile information such as version numbers, build time, etc. into the program.
Create the configuration template file config.h.in:
Example
// CMake configuration template file, generates config.h via configure_file()
// Project version number (replaced by CMake variable @PROJECT_VERSION@)
#define EXAMPLE_VERSION "@PROJECT_VERSION@"
// Build type (Debug or Release)
#define EXAMPLE_BUILD_TYPE "@CMAKE_BUILD_TYPE@"
// Build timestamp
#define EXAMPLE_BUILD_TIMESTAMP "@BUILD_TIMESTAMP@"
Configure and generate the header file in CMakeLists.txt:
Example
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S")
# Replace @variables@ in config.h.in with the values of CMake variables
# Output to config.h in the build directory
configure_file(config.h.in config.h)
# Add the build directory to the header file search path so that source files can find config.h
include_directories(${CMAKE_BINARY_DIR})
Use the generated configuration in source files:
// 在 main.cpp 中包含生成的配置文件 #include "config.h" std::cout << "EXAMPLE Version: " << EXAMPLE_VERSION << std::endl; std::cout << "Build Type: " << EXAMPLE_BUILD_TYPE << std::endl;
Generating Custom Build Steps
Besides standard compilation and linking, CMake allows you to insert custom operations into the build process, such as code generation, resource copying, and post-build notifications.
Custom Commands
add_custom_command()Used to define a custom command that is executed when a file needs to be updated.
The core concept of this command isdependency-driven— the command executes only when the OUTPUT file does not exist or the DEPENDS file has been updated.
Example
# Execute only when generated_file.cpp does not exist or input_template.txt has been updated
add_custom_command(
# Output file (CMake checks this file to decide whether to re-execute the command)
OUTPUT ${CMAKE_BINARY_DIR}/generated_file.cpp
# Command to execute (here cmake -E is used to perform the built-in file generation operation)
COMMAND ${CMAKE_COMMAND}
-E echo "// Auto-generated source file" > ${CMAKE_BINARY_DIR}/generated_file.cpp
# Dependency file (the command re-executes when this file changes)
DEPENDS ${CMAKE_SOURCE_DIR}/input_template.txt
# Message displayed when executing
COMMENT "Generating source file from template..."
)
Build-stage trigger command — executes after linking completes:
Example
add_custom_command(
TARGET MyApp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:MyApp>
${CMAKE_SOURCE_DIR}/deploy/
COMMENT "Copy MyApp to the deployment directory"
)
Custom Targets
add_custom_target()Defines a build target that does not produce an output file, usually used to group multiple custom commands together.
Unless the ALL keyword is used, custom targets will not execute in the default build; you need to explicitly specify the target name.
Example
# ALL means it will execute automatically during the default build (make)
add_custom_target(generate_code ALL
# Depends on the OUTPUT file defined earlier
DEPENDS ${CMAKE_BINARY_DIR}/generated_file.cpp
)
# Custom target: perform auxiliary tasks (will not execute during the default build)
add_custom_target(deploy
COMMAND ${CMAKE_COMMAND} -E echo "Deploy to server..."
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_BINARY_DIR}/bin
/opt/example/deploy/
COMMENT "Execute deployment operation"
)
# Trigger this target separately during the build
# $ cmake --build . --target deploy
add_custom_command and add_custom_target differ: the former defines a rule for "how to generate files" and is triggered by dependencies; the latter defines an action target for "what to do" and needs to be explicitly invoked or runs automatically when ALL is used.
Cross-Platform and Cross-Compilation
One of CMake's core advantages is cross-platform support — the same CMakeLists.txt can generate corresponding build files on different operating systems and architectures.
Cross-Platform Builds
CMake automatically detects the compiler and system environment of the current platform and generates matching build files.
You can also explicitly specify target platform information via variables.
| Variable | Description | Example value |
|---|---|---|
CMAKE_SYSTEM_NAME |
Target operating system | Linux、Windows、Darwin、Android |
CMAKE_SYSTEM_PROCESSOR |
Target processor architecture | x86_64、arm、aarch64 |
CMAKE_C_COMPILER |
C compiler path | /usr/bin/arm-linux-gnueabihf-gcc |
CMAKE_CXX_COMPILER |
C++ compiler path | /usr/bin/arm-linux-gnueabihf-g++ |
Cross-Compilation
Cross compilation refers to compiling programs that run on another architecture (such as ARM) on the current platform (such as x86_64 Linux).
CMake usesa toolchain file (Toolchain File)to specify the compiler, system information, and linker configuration required for cross compilation.
Create a toolchain file toolchain.cmake:
Example
# ARM Linux cross-compilation toolchain configuration file
# Target system is Linux
set(CMAKE_SYSTEM_NAME Linux)
# Target processor architecture is ARM
set(CMAKE_SYSTEM_PROCESSOR arm)
# Specify the cross compiler (required: adjust the path according to the actual toolchain location)
set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)
# Specify the root filesystem path of the target system (optional, used to find libraries and header files)
set(CMAKE_FIND_ROOT_PATH /path/to/arm-sysroot)
# When searching for programs, only search in target system paths (not in host paths)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# When searching for libraries, only search in target system paths
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# When searching for header files, only search in target system paths
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
Cross-compile using the toolchain file:
# 配置时通过 -DCMAKE_TOOLCHAIN_FILE 指定工具链文件 mkdir build_arm && cd build_arm cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake .. # 使用生成的 Makefile 编译(将调用 ARM 交叉编译器) make
The toolchain file must be specified when running cmake for the first time. If CMakeCache.txt already exists, you need to delete the build directory and reconfigure to switch toolchains.
Target Properties and Configuration
CMake allows fine-grained control of compile options, link options, and various properties on a per-target basis, which is the recommended practice in modern CMake.
Target Properties
set_target_properties()Used to set target-level properties, which affect the behavior of the compiler and linker.
| Property | Description | Example |
|---|---|---|
COMPILE_OPTIONS |
Set compile options | -Wall、-O2 |
COMPILE_DEFINITIONS |
Set preprocessor macros | DEBUG、VERSION=2 |
LINK_FLAGS |
Set linker flags | -L/path/to/lib |
OUTPUT_NAME |
Modify output file name | The target name is MyApp, and the output is my_app |
Example
add_executable(MyApp main.cpp)
# Set compile options and linker flags for the target
set_target_properties(MyApp PROPERTIES
COMPILE_OPTIONS "-Wall;-Wextra"
COMPILE_DEFINITIONS "EXAMPLE_DEBUG"
LINK_FLAGS "-L/usr/local/lib"
OUTPUT_NAME "my_app"
)
# The set_target_properties above is equivalent to the following three commands:
# target_compile_options(MyApp PRIVATE -Wall -Wextra)
# target_compile_definitions(MyApp PRIVATE EXAMPLE_DEBUG)
# target_link_options(MyApp PRIVATE -L/usr/local/lib)
In modern CMake, it is recommended to use target_compile_options(), target_compile_definitions(), and target_link_options() instead of set_target_properties(), because they support PUBLIC/PRIVATE/INTERFACE visibility control and make dependencies clearer.
Custom Compile and Link Options
Using the target series of commands allows more precise control over the propagation scope of options.
Example
# PRIVATE: Only MyApp itself uses these options when compiling
target_compile_options(MyApp PRIVATE -Wall -Wextra -Wpedantic)
# Set preprocessor definitions for MyApp
# Equivalent to writing #define EXAMPLE_VERSION "1.0" in the source file
target_compile_definitions(MyApp PRIVATE EXAMPLE_VERSION="1.0")
# Set link options for MyApp
target_link_options(MyApp PRIVATE -L/usr/local/lib)
# If it is a library, using PUBLIC allows targets that link this library to also inherit these options
# For example: any target using MyLib will automatically enable C++17
target_compile_features(MyLib PUBLIC cxx_std_17)
Notes
CMAKE_BUILD_TYPE is only valid for single-configuration generators.If the project needs to support both Makefile and Visual Studio, do not rely on the value of CMAKE_BUILD_TYPE. Use generator expressions
$<$<CONFIG:Debug>:DEBUG_VALUE>to dynamically switch based on the actual configuration.
Prefer the target_xxx series of commands.Although set_target_properties is powerful, it lacks visibility control. In scenarios requiring PUBLIC/PRIVATE/INTERFACE semantics, it is better to use dedicated commands such as target_compile_options.
The toolchain file must be specified on the first cmake invocation.When cross-compiling, re-specifying the toolchain in a directory with an existing CMakeCache.txt may produce an incomplete configuration. Always configure cross-compilation in a clean build directory.
Other extensionsIt is recommended to add a prefix to function names in custom modules.Avoid naming conflicts with CMake built-in commands or other modules. For example, use the project name as a prefix: myproject_set_warnings().