跳到主要内容

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 值
GCCGNU
ClangClang
Apple ClangAppleClang
MSVCMSVC
IntelIntel

常用生成器表达式

目标相关表达式

目标文件路径

# 获取目标文件的完整路径
$<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++ 编译器 IDGNU, 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

小结

本章我们学习了:

  1. 核心概念:生成器表达式在构建系统生成阶段求值
  2. 基本语法$<表达式>$<条件:真值>
  3. 常用表达式:CONFIG、PLATFORM_ID、COMPILER_ID、TARGET_FILE 等
  4. 实战应用:根据构建类型设置编译选项、跨平台配置、条件链接
  5. 常见陷阱:避免在 if() 中使用、注意嵌套、理解多配置生成器

生成器表达式是现代 CMake 的核心特性之一,掌握它能让你写出更简洁、更跨平台的构建脚本。

练习

  1. 使用生成器表达式为 Debug 和 Release 配置不同的预处理器定义
  2. 编写一个跨平台的编译选项配置,针对 GCC/Clang 和 MSVC 分别设置
  3. 使用 $<TARGET_FILE> 实现构建后自动运行程序的测试命令

参考资源