CMake 高级特性
掌握 CMake 基础之后,高级特性可以帮助你更灵活地管理复杂项目的构建配置。
本文涵盖自定义模块、多配置构建、高级包查找、自定义构建步骤、跨平台交叉编译以及目标属性配置六大主题。
整体内容概览:
| 主题 | 核心能力 | 典型应用场景 |
|---|---|---|
| 自定义模块和脚本 | 封装可复用的 CMake 逻辑 | 统一编译器选项、自定义查找逻辑、团队共享构建规则 |
| 构建配置和目标 | 管理多配置和多目标 | Debug/Release 切换、构建多种输出产物 |
| 高级查找和配置 | 灵活定位外部依赖 | 查找特定组件、通过配置文件传递构建信息 |
| 自定义构建步骤 | 扩展构建流程 | 代码生成、资源处理、构建后自动化 |
| 跨平台和交叉编译 | 面向多平台构建 | 嵌入式开发、为不同操作系统编译 |
| 目标属性和配置 | 精细控制编译链接 | 针对特定目标设置警告级别、链接路径 |
自定义 CMake 模块和脚本
当多个 CMakeLists.txt 中需要重复同样的逻辑时,将这些逻辑封装成自定义模块可以大幅减少代码冗余。
自定义模块和脚本的本质是将常用的 CMake 函数、宏和配置提取到独立文件中,供多个项目复用。
自定义 CMake 模块
自定义模块是一个以 .cmake 为扩展名的文件,其中定义了可被 include 加载的 CMake 函数和宏。
创建步骤:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 创建 cmake/ 目录 |
在项目根目录下创建,用于集中存放自定义模块 |
| 2 | 创建模块文件 | 例如 cmake/MyModule.cmake,编写自定义函数 |
| 3 | 扩展模块搜索路径 | 在 CMakeLists.txt 中通过 CMAKE_MODULE_PATH 注册该目录 |
| 4 | 加载模块 | 使用 include() 引入模块并调用其中的函数 |
自定义模块示例——MyModule.cmake:
实例
# 自定义模块:提供项目通用的辅助函数
# 函数:为所有目标统一设置编译器警告选项
# 参数 ARG_TARGET:需要设置警告的目标名称
function(runoob_set_warnings ARG_TARGET)
# 根据编译器类型设置不同的警告标志
if(MSVC)
# MSVC 编译器:启用 /W4 警告级别
target_compile_options(${ARG_TARGET} PRIVATE /W4)
else()
# GCC / Clang 编译器:启用常用警告
target_compile_options(${ARG_TARGET} PRIVATE -Wall -Wextra -Wpedantic)
endif()
endfunction()
# 函数:打印目标的基本构建信息
function(runoob_print_target_info ARG_TARGET)
message(STATUS "目标名称:${ARG_TARGET}")
message(STATUS "源文件列表:$<TARGET_PROPERTY:${ARG_TARGET},SOURCES>")
endfunction()
在 CMakeLists.txt 中加载并使用模块:
实例
cmake_minimum_required(VERSION 3.10)
project(MyProject CXX)
# 将 cmake/ 目录添加到模块搜索路径
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# 加载自定义模块
include(MyModule)
# 添加可执行文件目标
add_executable(MyApp main.cpp)
# 调用模块中的自定义函数,为 MyApp 设置警告选项
runoob_set_warnings(MyApp)
# 调用模块中的自定义函数,打印目标信息
runoob_print_target_info(MyApp)
CMAKE_MODULE_PATH 是 CMake 查找模块文件时的搜索路径列表。CMake 会先在内置模块路径中查找,找不到时再到 CMAKE_MODULE_PATH 指定的路径中查找。
使用自定义 CMake 脚本
自定义脚本与模块类似,但通常用于执行配置操作而非定义可复用的函数。
脚本可以直接用 include() 加载,CMake 会按顺序执行其中的每一条指令。
创建脚本文件 config.cmake:
实例
# 自定义脚本:集中管理项目的配置选项
# 设置默认构建类型为 Release
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "构建类型" FORCE)
message(STATUS "未指定构建类型,默认使用 Release")
endif()
# 根据构建类型设置不同的编译选项
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "启用 Debug 模式:关闭优化,开启调试符号")
else()
message(STATUS "启用 Release 模式:开启 O2 优化")
endif()
在 CMakeLists.txt 中调用脚本:
# 使用绝对路径加载脚本
include(${CMAKE_SOURCE_DIR}/config.cmake)
构建配置和目标
CMake 支持在同一个项目中管理多种构建配置(如 Debug 和 Release),并定义多个构建目标。
多配置生成器
不同的 CMake 生成器对配置的处理方式不同。
像 Visual Studio 和 Xcode 这样的多配置生成器允许在同一个构建目录中切换 Debug/Release,而 Unix Makefiles 等单配置生成器需要在配置阶段就确定构建类型。
| 生成器类型 | 典型生成器 | 配置指定方式 |
|---|---|---|
| 单配置 | Unix Makefiles、Ninja | 配置时通过 -DCMAKE_BUILD_TYPE=Release 指定 |
| 多配置 | Visual Studio、Xcode | 构建时通过 --config Release 切换 |
在 CMakeLists.txt 中设置默认配置:
# 仅对单配置生成器生效,多配置生成器会忽略此变量 set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type")
CMAKE_BUILD_TYPE 只对单配置生成器有效。如果使用 Visual Studio,请通过 cmake --build . --config Release 或在 IDE 中切换配置。
构建目标
一个 CMake 项目中可以定义多个构建目标,每个目标独立配置编译选项和链接依赖。
这让你可以在一次构建中产出多个可执行文件或多个库。
实例
add_executable(MyApp src/main.cpp)
add_executable(MyTool src/tool.cpp)
# 为不同目标设置不同的编译宏,控制条件编译
set_target_properties(MyApp PROPERTIES COMPILE_DEFINITIONS "APP_MODE")
set_target_properties(MyTool PROPERTIES COMPILE_DEFINITIONS "TOOL_MODE")
# 为不同目标链接不同的库
target_link_libraries(MyApp PRIVATE MyLib)
target_link_libraries(MyTool PRIVATE MyLib CLI11::CLI11)
高级查找和配置
除了基本的 find_package 用法外,CMake 还支持指定组件、精确控制版本和通过配置文件模板传递构建信息。
find_package 高级用法
COMPONENTS 关键字允许你只查找库中的特定子模块,而非整个包。
这对于像 Boost 这样的大型库非常重要——按需引入可以避免链接不需要的组件。
实例
# filesystem 内部依赖 system,所以两个都要声明
find_package(Boost REQUIRED COMPONENTS filesystem system)
# 检查是否找到了所需组件
if(Boost_FOUND)
message(STATUS "Boost 版本:${Boost_VERSION}")
message(STATUS "Boost 头文件路径:${Boost_INCLUDE_DIRS}")
# 分别链接每个需要的组件
target_link_libraries(MyApp PRIVATE
Boost::filesystem
Boost::system
)
endif()
指定搜索路径(适用于非标准安装位置):
# 方式一:通过变量指定根目录 cmake .. -DBOOST_ROOT=/path/to/custom/boost # 方式二:在 CMakeLists.txt 中预先设置 set(BOOST_ROOT "/path/to/custom/boost") find_package(Boost REQUIRED COMPONENTS filesystem)
配置文件与构建选项
configure_file() 指令可以将 CMake 变量的值注入到代码文件中,实现在编译时传递配置信息。
这在需要将版本号、构建时间等信息编译进程序时非常实用。
创建配置模板文件 config.h.in:
实例
// CMake 配置模板文件,通过 configure_file() 生成 config.h
// 项目版本号(由 CMake 变量 @PROJECT_VERSION@ 替换)
#define RUNOOB_VERSION "@PROJECT_VERSION@"
// 构建类型(Debug 或 Release)
#define RUNOOB_BUILD_TYPE "@CMAKE_BUILD_TYPE@"
// 编译时间戳
#define RUNOOB_BUILD_TIMESTAMP "@BUILD_TIMESTAMP@"
在 CMakeLists.txt 中配置并生成头文件:
实例
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S")
# 将 config.h.in 中的 @变量@ 替换为 CMake 变量的值
# 输出到 build 目录下的 config.h
configure_file(config.h.in config.h)
# 将 build 目录添加到头文件搜索路径,使源文件能找到 config.h
include_directories(${CMAKE_BINARY_DIR})
在源文件中使用生成的配置:
// 在 main.cpp 中包含生成的配置文件 #include "config.h" std::cout << "RUNOOB Version: " << RUNOOB_VERSION << std::endl; std::cout << "Build Type: " << RUNOOB_BUILD_TYPE << std::endl;
生成自定义构建步骤
除了标准的编译和链接,CMake 允许你在构建过程中插入自定义操作,例如代码生成、资源复制和构建后通知。
自定义命令
add_custom_command() 用于定义一个自定义命令,它会在某个文件需要更新时执行。
这个指令的核心概念是依赖驱动——只有当 OUTPUT 文件不存在或 DEPENDS 文件更新时,命令才会执行。
实例
# 仅当 generated_file.cpp 不存在或 input_template.txt 更新时才执行
add_custom_command(
# 输出文件(CMake 通过检查此文件决定是否重新执行命令)
OUTPUT ${CMAKE_BINARY_DIR}/generated_file.cpp
# 要执行的命令(此处用 cmake -E 执行内置的文件生成操作)
COMMAND ${CMAKE_COMMAND}
-E echo "// 自动生成的源文件" > ${CMAKE_BINARY_DIR}/generated_file.cpp
# 依赖文件(此文件变更时命令重新执行)
DEPENDS ${CMAKE_SOURCE_DIR}/input_template.txt
# 执行时输出的提示信息
COMMENT "正在从模板生成源文件..."
)
构建阶段触发命令——在链接完成后执行:
实例
add_custom_command(
TARGET MyApp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:MyApp>
${CMAKE_SOURCE_DIR}/deploy/
COMMENT "将 MyApp 复制到部署目录"
)
自定义目标
add_custom_target() 定义一个不产生输出文件的构建目标,通常用于将多个自定义命令组织在一起。
除非使用 ALL 关键字,否则自定义目标不会在默认构建中执行,需要显式指定目标名。
实例
# ALL 表示在默认构建(make)时自动执行
add_custom_target(generate_code ALL
# 依赖前面定义的 OUTPUT 文件
DEPENDS ${CMAKE_BINARY_DIR}/generated_file.cpp
)
# 自定义目标:执行辅助任务(不会在默认构建时执行)
add_custom_target(deploy
COMMAND ${CMAKE_COMMAND} -E echo "部署到服务器..."
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_BINARY_DIR}/bin
/opt/runoob/deploy/
COMMENT "执行部署操作"
)
# 在构建时单独触发此目标
# $ cmake --build . --target deploy
add_custom_command 和 add_custom_target 的区别:前者定义一个「如何生成文件」的规则,由依赖关系触发;后者定义一个「做什么事」的动作目标,需要显式调用或在 ALL 时自动执行。
跨平台和交叉编译
CMake 的核心优势之一就是跨平台支持——同一份 CMakeLists.txt 可以在不同操作系统和架构上生成对应的构建文件。
跨平台构建
CMake 会自动检测当前平台的编译器和系统环境,生成匹配的构建文件。
你也可以通过变量显式指定目标平台信息。
| 变量 | 作用 | 示例值 |
|---|---|---|
CMAKE_SYSTEM_NAME |
目标操作系统 | Linux、Windows、Darwin、Android |
CMAKE_SYSTEM_PROCESSOR |
目标处理器架构 | x86_64、arm、aarch64 |
CMAKE_C_COMPILER |
C 编译器路径 | /usr/bin/arm-linux-gnueabihf-gcc |
CMAKE_CXX_COMPILER |
C++ 编译器路径 | /usr/bin/arm-linux-gnueabihf-g++ |
交叉编译
交叉编译是指在当前平台(如 x86_64 Linux)上编译出运行在另一种架构(如 ARM)上的程序。
CMake 通过工具链文件(Toolchain File)来指定交叉编译所需的编译器、系统信息和链接器配置。
创建工具链文件 toolchain.cmake:
实例
# ARM Linux 交叉编译工具链配置文件
# 目标系统为 Linux
set(CMAKE_SYSTEM_NAME Linux)
# 目标处理器架构为 ARM
set(CMAKE_SYSTEM_PROCESSOR arm)
# 指定交叉编译器(必填:路径需根据实际工具链位置调整)
set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)
# 指定目标系统的根文件系统路径(可选,用于查找库和头文件)
set(CMAKE_FIND_ROOT_PATH /path/to/arm-sysroot)
# 查找程序时只在目标系统路径中搜索(不在主机路径搜索)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# 查找库时只在目标系统路径中搜索
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# 查找头文件时只在目标系统路径中搜索
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
使用工具链文件进行交叉编译:
# 配置时通过 -DCMAKE_TOOLCHAIN_FILE 指定工具链文件 mkdir build_arm && cd build_arm cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake .. # 使用生成的 Makefile 编译(将调用 ARM 交叉编译器) make
工具链文件必须在第一次运行 cmake 时指定。如果已有 CMakeCache.txt,需要删除 build 目录后重新配置才能切换工具链。
目标属性和配置
CMake 允许以目标为单位精细控制编译选项、链接选项和各类属性,这是现代 CMake 推荐的做法。
目标属性
set_target_properties() 用于设置目标级别的属性,这些属性影响编译器和链接器的行为。
| 属性 | 作用 | 示例 |
|---|---|---|
COMPILE_OPTIONS |
设置编译选项 | -Wall、-O2 |
COMPILE_DEFINITIONS |
设置预处理器宏 | DEBUG、VERSION=2 |
LINK_FLAGS |
设置链接器标志 | -L/path/to/lib |
OUTPUT_NAME |
修改输出文件名 | 目标名是 MyApp,输出为 my_app |
实例
add_executable(MyApp main.cpp)
# 为目标设置编译选项和链接标志
set_target_properties(MyApp PROPERTIES
COMPILE_OPTIONS "-Wall;-Wextra"
COMPILE_DEFINITIONS "RUNOOB_DEBUG"
LINK_FLAGS "-L/usr/local/lib"
OUTPUT_NAME "my_app"
)
# 上面的 set_target_properties 等同于以下三条指令的效果:
# target_compile_options(MyApp PRIVATE -Wall -Wextra)
# target_compile_definitions(MyApp PRIVATE RUNOOB_DEBUG)
# target_link_options(MyApp PRIVATE -L/usr/local/lib)
在现代 CMake 中,推荐使用 target_compile_options()、target_compile_definitions() 和 target_link_options() 替代 set_target_properties(),因为它们支持 PUBLIC/PRIVATE/INTERFACE 可见性控制,依赖关系更明确。
自定义编译和链接选项
使用 target 系列指令可以更精确地控制选项的传播范围。
实例
# PRIVATE:只有 MyApp 自身编译时使用这些选项
target_compile_options(MyApp PRIVATE -Wall -Wextra -Wpedantic)
# 为 MyApp 设置预处理器定义
# 等同于在源文件中写 #define RUNOOB_VERSION "1.0"
target_compile_definitions(MyApp PRIVATE RUNOOB_VERSION="1.0")
# 为 MyApp 设置链接选项
target_link_options(MyApp PRIVATE -L/usr/local/lib)
# 如果是库,使用 PUBLIC 可以让链接此库的目标也继承这些选项
# 例如:使用 MyLib 的任何目标都会自动启用 C++17
target_compile_features(MyLib PUBLIC cxx_std_17)
注意事项
CMAKE_BUILD_TYPE 仅对单配置生成器有效。如果项目需要同时支持 Makefile 和 Visual Studio,不要依赖 CMAKE_BUILD_TYPE 的值。使用生成器表达式
$<$<CONFIG:Debug>:DEBUG_VALUE>来根据实际配置动态切换。
优先使用 target_xxx 系列指令。set_target_properties 虽然功能强大,但缺少可见性控制。在需要 PUBLIC/PRIVATE/INTERFACE 语义的场景下,使用 target_compile_options 等专门的指令是更好的选择。
工具链文件必须在首次 cmake 时指定。交叉编译时,如果在已有 CMakeCache.txt 的目录中重新指定工具链,可能会产生不完整的配置。始终在干净的 build 目录中配置交叉编译。
自定义模块中的函数名建议加上前缀。避免与 CMake 内置命令或其他模块产生命名冲突。例如使用项目名作为前缀:myproject_set_warnings()。
