CMake 生成器表达式
生成器表达式(Generator Expressions)是 CMake 中一个强大但常被忽视的特性。它允许在构建系统生成阶段(而非配置阶段)计算值,从而实现条件化的构建配置。
什么是生成器表达式?
核心概念
普通的 CMake 变量和条件判断在 配置阶段(运行 cmake 命令时)求值。但有些信息只有在 构建阶段(运行 cmake --build 时)才能确定,例如:
- 当前使用的构建类型(Debug/Release)
- 目标文件的实际路径
- 某个源文件是否被编译
生成器表达式就是为了解决这类问题而设计的。它的语法是 $<表达式>,在构建系统生成时被求值。
为什么需要生成器表达式?
考虑一个常见场景:你想在 Debug 模式下添加调试符号,在 Release 模式下启用优化。
传统方式(有问题):
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_options(myapp PRIVATE -g -O0)
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
target_compile_options(myapp PRIVATE -O3)
endif()
这种方式的问题是:当使用多配置生成器(如 Visual Studio、Xcode、Ninja Multi-Config)时,CMAKE_BUILD_TYPE 在配置阶段是空的,因为构建类型在构建时才指定。
使用生成器表达式(正确方式):
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-g -O0>
$<$<CONFIG:Release>:-O3>
)
这个表达式会在构建时根据实际选择的配置求值,无论是单配置还是多配置生成器都能正常工作。
基本语法
生成器表达式的基本形式是 $<表达式>,其中表达式可以是:
$<条件:真值> # 条件表达式
$<变量名> # 变量引用
$<函数:参数> # 函数调用
条件表达式
基本条件
$<条件:真值>
$<条件:真值,假值> # 三元运算符形式
条件为真,返回冒号后面的值;条件为假,返回空字符串或指定的假值。
构建类型判断
$<CONFIG:类型> 判断当前构建类型是否匹配:
# Debug 配置时添加 -g 选项
$<$<CONFIG:Debug>:-g>
# Debug 配置返回 "debug",否则返回 "release"
$<IF:$<CONFIG:Debug>,debug,release>
平台判断
# Windows 平台
$<PLATFORM_ID:Windows>
# Linux 平台
$<PLATFORM_ID:Linux>
# macOS 平台
$<PLATFORM_ID:Darwin>
编译器判断
# GCC 编译器
$<CXX_COMPILER_ID:GNU>
# Clang 编译器
$<CXX_COMPILER_ID:Clang>
# MSVC 编译器
$<CXX_COMPILER_ID:MSVC>
# 版本判断
$<CXX_COMPILER_VERSION:VERSION_GREATER_EQUAL,9.0>
编译器 ID 对照表:
| 编译器 | ID 值 |
|---|---|
| GCC | GNU |
| Clang | Clang |
| Apple Clang | AppleClang |
| MSVC | MSVC |
| Intel | Intel |
常用生成器表达式
目标相关表达式
目标文件路径
# 获取目标文件的完整路径
$<TARGET_FILE:myapp>
# 获取目标文件所在目录
$<TARGET_FILE_DIR:myapp>
# 仅获取文件名
$<TARGET_FILE_NAME:myapp>
实际应用:将构建好的可执行文件复制到输出目录
add_custom_command(TARGET myapp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:myapp>
${CMAKE_BINARY_DIR}/bin/
)
目标属性
# 获取目标的某个属性
$<TARGET_PROPERTY:myapp,OUTPUT_NAME>
# 获取目标链接的库
$<TARGET_PROPERTY:myapp,LINK_LIBRARIES>
布尔表达式
逻辑运算
# 与运算
$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Linux>>
# 或运算
$<OR:$<PLATFORM_ID:Windows>,$<PLATFORM_ID:Darwin>>
# 非运算
$<NOT:$<CONFIG:Release>>
比较运算
# 字符串相等
$<STREQUAL:a,b>
# 数字比较
$<VERSION_LESS:1.0,2.0>
$<VERSION_GREATER:2.0,1.0>
$<VERSION_EQUAL:1.0,1.0>
列表和字符串操作
# 连接列表元素
$<JOIN:list,分隔符>
# 示例:用分号连接源文件列表
$<JOIN:${SOURCES},;>
# 过滤列表
$<FILTER:list,INCLUDE,正则表达式>
$<FILTER:list,EXCLUDE,正则表达式>
# 示例:排除测试文件
$<FILTER:${SOURCES},EXCLUDE,.*_test\\.cpp>
编译特性
# 编译器支持某特性
$<COMPILE_FEATURES:cxx_std_17>
# C++ 标准
$<COMPILE_LANGUAGE:CXX>
实战示例
示例 1:根据构建类型设置编译选项
add_executable(myapp src/main.cpp)
target_compile_options(myapp PRIVATE
# 通用选项
-Wall
# Debug 专用选项
$<$<CONFIG:Debug>:
-g
-O0
-DDEBUG
>
# Release 专用选项
$<$<CONFIG:Release>:
-O3
-DNDEBUG
>
# RelWithDebInfo 专用选项
$<$<CONFIG:RelWithDebInfo>:
-O2
-g
>
# MinSizeRel 专用选项
$<$<CONFIG:MinSizeRel>:
-Os
-DNDEBUG
>
)
解读:
$<$<CONFIG:Debug>:-g -O0 -DDEBUG>这是一个嵌套表达式- 外层是条件表达式
$<条件:值> - 内层
$<CONFIG:Debug>作为条件,当构建类型为 Debug 时返回真 - 只有条件为真时,整个表达式才展开为
-g -O0 -DDEBUG - 条件为假时,表达式展开为空字符串,对编译选项没有影响
示例 2:跨平台编译选项
target_compile_options(myapp PRIVATE
# GCC/Clang 选项
$<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>:
-Wall -Wextra -Wpedantic
>
# MSVC 选项
$<$<CXX_COMPILER_ID:MSVC>:
/W4 /utf-8
>
# Windows 特定
$<$<AND:$<PLATFORM_ID:Windows>,$<CXX_COMPILER_ID:GNU>>:
-static-libgcc -static-libstdc++
>
)
示例 3:条件性链接库
find_package(Threads REQUIRED)
find_package(OpenSSL QUIET)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE
Threads::Threads
# 仅在找到 OpenSSL 时链接
$<$<TARGET_EXISTS:OpenSSL::SSL>:OpenSSL::SSL>
$<$<TARGET_EXISTS:OpenSSL::Crypto>:OpenSSL::Crypto>
)
# 根据平台选择不同的库
target_link_libraries(myapp PRIVATE
$<IF:$<PLATFORM_ID:Windows>,ws2_32,>
$<IF:$<PLATFORM_ID:Linux>,rt,>
)
示例 4:生成配置头文件
使用 configure_file 结合生成器表达式的限制是,生成器表达式不能直接用于 configure_file。但可以通过 file(GENERATE) 实现:
# 生成版本信息头文件
file(GENERATE
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/version.h
CONTENT "
#define PROJECT_VERSION \"${PROJECT_VERSION}\"
#define BUILD_TYPE \"$<CONFIG>\"
#define BUILD_TIME \"${TIMESTAMP}\"
"
)
示例 5:复制依赖文件到输出目录
# 将 DLL 文件复制到可执行文件目录(Windows)
if(WIN32)
add_custom_command(TARGET myapp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:myapp>
$<TARGET_FILE_DIR:myapp>
COMMAND_EXPAND_LISTS
)
endif()
示例 6:条件性添加源文件
set(SOURCES
src/main.cpp
src/common.cpp
)
# Windows 特定源文件
if(WIN32)
list(APPEND SOURCES src/windows_utils.cpp)
endif()
# 使用生成器表达式的替代方案
add_executable(myapp
src/main.cpp
src/common.cpp
$<$<PLATFORM_ID:Windows>:src/windows_utils.cpp>
)
目标输出表达式
文件路径表达式
# 可执行文件完整路径
$<TARGET_FILE:myapp>
# Linux: /path/to/build/myapp
# Windows: C:/path/to/build/Debug/myapp.exe
# 可执行文件目录
$<TARGET_FILE_DIR:myapp>
# 库文件路径
$<TARGET_FILE:mylib>
# Linux 静态库: /path/to/build/libmylib.a
# Linux 动态库: /path/to/build/libmylib.so
# Windows DLL: C:/path/to/build/Debug/mylib.dll
# Windows 导入库: C:/path/to/build/Debug/mylib.lib
链接相关表达式
# 获取目标需要链接的所有库
$<TARGET_LINKER_FILE:myapp>
# 运行时依赖的 DLL 列表(Windows 有用)
$<TARGET_RUNTIME_DLLS:myapp>
高级用法
组合多个条件
# Debug 配置下且为 Linux 平台
$<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Linux>>:-fsanitize=address>
# 非 Windows 平台
$<$<NOT:$<PLATFORM_ID:Windows>>:-pthread>
在自定义命令中使用
add_custom_command(
OUTPUT output.txt
COMMAND $<TARGET_FILE:generator_app> --output output.txt
DEPENDS generator_app
COMMENT "Generating output.txt"
)
定义传播
# 仅在当前目标使用,不传播给依赖者
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE>
)
# 传播给所有依赖者
target_compile_definitions(mylib PUBLIC
$<$<CONFIG:Debug>:MYLIB_DEBUG>
)
常见陷阱
陷阱 1:在 if() 中使用生成器表达式
错误做法:
if($<CONFIG:Debug>)
# 这不会按预期工作!
endif()
原因:if() 在配置阶段求值,此时生成器表达式尚未展开。
正确做法:使用配置阶段的变量或条件
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
# 单配置生成器可用
endif()
或者使用生成器表达式替代 if:
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-g>
)
陷阱 2:忘记嵌套条件表达式
错误做法:
# 想在 Debug 模式下添加 -g
target_compile_options(myapp PRIVATE $<CONFIG:Debug>:-g)
正确做法:
# 需要用条件表达式包裹
target_compile_options(myapp PRIVATE $<$<CONFIG:Debug>:-g>)
陷阱 3:多配置生成器的混淆
使用 Visual Studio 或 Ninja Multi-Config 时,配置阶段 CMAKE_BUILD_TYPE 为空:
# 这在多配置生成器下不工作
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(OPTIMIZATION_LEVEL 0)
endif()
应使用生成器表达式:
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-O0>
)
调试生成器表达式
查看展开结果
使用 message() 配合生成器表达式进行调试:
# 配置阶段输出(生成器表达式未展开)
message(STATUS "Config value: $<CONFIG>") # 输出: $<CONFIG>
# 构建阶段生成文件查看
file(GENERATE OUTPUT debug.txt CONTENT "Build type: $<CONFIG>")
使用 --trace 选项
cmake --trace ..
cmake --trace-expand ..
表达式参考速查
| 表达式 | 说明 | 示例值 |
|---|---|---|
$<CONFIG> | 当前构建类型 | Debug, Release |
$<PLATFORM_ID> | 当前平台 | Linux, Windows, Darwin |
$<CXX_COMPILER_ID> | C++ 编译器 ID | GNU, Clang, MSVC |
$<TARGET_FILE:tgt> | 目标文件完整路径 | /build/Debug/myapp |
$<TARGET_FILE_DIR:tgt> | 目标文件目录 | /build/Debug |
$<TARGET_FILE_NAME:tgt> | 目标文件名 | myapp.exe |
$<IF:cond,t,f> | 条件表达式 | t 或 f |
$<AND:...> | 逻辑与 | 0 或 1 |
$<OR:...> | 逻辑或 | 0 或 1 |
$<NOT:cond> | 逻辑非 | 0 或 1 |
$<STREQUAL:a,b> | 字符串相等 | 0 或 1 |
$<VERSION_LESS:a,b> | 版本比较 | 0 或 1 |
$<JOIN:list,sep> | 连接列表 | a;b;c |
$<LOWER_CASE:str> | 转小写 | hello |
$<UPPER_CASE:str> | 转大写 | HELLO |
小结
本章我们学习了:
- 核心概念:生成器表达式在构建系统生成阶段求值
- 基本语法:
$<表达式>和$<条件:真值> - 常用表达式:CONFIG、PLATFORM_ID、COMPILER_ID、TARGET_FILE 等
- 实战应用:根据构建类型设置编译选项、跨平台配置、条件链接
- 常见陷阱:避免在
if()中使用、注意嵌套、理解多配置生成器
生成器表达式是现代 CMake 的核心特性之一,掌握它能让你写出更简洁、更跨平台的构建脚本。
练习
- 使用生成器表达式为 Debug 和 Release 配置不同的预处理器定义
- 编写一个跨平台的编译选项配置,针对 GCC/Clang 和 MSVC 分别设置
- 使用
$<TARGET_FILE>实现构建后自动运行程序的测试命令