第1章 生成器表达式:CMake的“活条件”
各位同学,咱们今天聊一个CMake里特别有意思、也特别实用的东西——生成器表达式。说实话,我刚开始接触NDK开发那会儿,看到CMakeLists.txt里一堆$<...>的符号,第一反应是“这什么鬼?”后来用多了才发现,这玩意儿简直就是CMake的“活条件”,能帮我们省掉大量重复代码。
1.1 生成器表达式的语法
生成器表达式的语法其实很简单,就一个格式:$<...>。尖括号里放的是具体的表达式内容。它不像普通的变量引用${...}那样在配置阶段就展开,而是在构建阶段才被求值。嗯,这一点很关键。
我打个比方你就明白了:${CMAKE_SOURCE_DIR}在CMake运行时就确定了,但$要到真正编译时才知道是Debug还是Release。这就是“生成器”这个名字的由来——它生成的是构建系统最终使用的值。
核心语法:
$<expression> # 基本形式
$<condition:value> # 条件表达式
$<condition:true:false> # 条件三目表达式
我个人习惯把生成器表达式分成三类:布尔判断类、查询信息类、还有字符串变换类。咱们一个一个来看。
1.2 $<CONFIG>:区分Debug和Release
$<CONFIG>是我用得最多的一个。它返回当前构建的配置名称,比如Debug、Release、RelWithDebInfo等。你想想看,很多时候我们需要根据不同的配置链接不同的库,或者加不同的编译选项。
举个例子,我在做一个音视频项目时,Debug版本需要链接一个带日志的libavcodec_d.so,Release版本则链接优化过的libavcodec.so。用生成器表达式,一行就搞定:
target_link_libraries(myapp
$<$<CONFIG:Debug>:libavcodec_d>
$<$<CONFIG:Release>:libavcodec>
)
这里用了嵌套表达式:内层的$<CONFIG:Debug>返回1或0,外层的$<1:libavcodec_d>或$<0:libavcodec_d>决定是否链接。说白了就是“如果配置是Debug,就链接libavcodec_d,否则不链接”。
小技巧: 可以用$<NOT:$<CONFIG:Debug>>来表示“非Debug”的情况。我曾经在项目里用这个给Release版本统一加优化标志,省了不少事。
1.3 $<TARGET_FILE>:获取目标文件路径
这个表达式用来获取某个目标(比如库或可执行文件)的完整路径。它的变体很多,我列个表给你看:
| 表达式 | 返回值 |
|---|---|
$<TARGET_FILE:tgt> | 目标文件的完整路径 |
$<TARGET_FILE_NAME:tgt> | 目标文件名(不含路径) |
$<TARGET_FILE_DIR:tgt> | 目标文件所在目录 |
$<TARGET_LINKER_FILE:tgt> | 链接时使用的文件(比如.so或.a) |
$<TARGET_SONAME_FILE:tgt> | 共享库的SONAME文件 |
我记得有一次做NDK集成,需要把编译好的动态库拷贝到assets目录下。如果用硬编码路径,换个构建类型就得改。后来用了$<TARGET_FILE:my_native_lib>,不管输出目录怎么变,都能自动找到正确的文件。
add_custom_command(TARGET myapp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:my_native_lib>
${CMAKE_SOURCE_DIR}/assets/
COMMENT "Copying native library to assets"
)
1.4 条件链接:让链接更智能
条件链接是生成器表达式最强大的应用之一。说白了就是“满足某个条件才链接这个库”。常见的场景有:
- 只在Android平台上链接log库
- 只在ARM64架构下链接特定的加速库
- 只在Debug版本链接测试辅助库
来看一个实际例子。我在做跨平台NDK项目时,需要根据目标平台决定链接哪些系统库:
target_link_libraries(myapp
# Android平台需要链接log和android
$<$<PLATFORM_ID:Android>:log>
$<$<PLATFORM_ID:Android>:android>
# Linux平台需要链接dl和pthread
$<$<PLATFORM_ID:Linux>:dl>
$<$<PLATFORM_ID:Linux>:pthread>
)
你可能会问:“为什么不用if语句?”嗯,if语句在配置阶段就执行了,而生成器表达式是在构建阶段。有些信息(比如目标架构)在配置阶段还不确定,这时候生成器表达式就派上用场了。
注意: 生成器表达式不能用于所有地方。比如set()、if()这些命令就不支持。它主要用在target_*系列命令、add_custom_command、install等地方。我曾经踩过这个坑,在set()里用了生成器表达式,结果发现它根本没展开。
1.5 平台相关配置:一套代码,多平台适配
做NDK开发,最头疼的就是不同平台、不同架构的差异。生成器表达式在这方面简直是救星。常用的平台判断表达式有:
$<PLATFORM_ID>:返回平台名称,如Android、Linux、Windows$<ANDROID>:Android平台时为1,否则为0$<TARGET_ARCH>:返回目标架构,如arm64-v8a、armeabi-v7a$<TARGET_OS>:返回目标操作系统
来看一个综合例子。我在做一个NDK项目时,需要根据不同的Android ABI链接不同的预编译库:
target_link_libraries(myapp
# arm64-v8a架构
$<$<STREQUAL:$<TARGET_ARCH>,arm64-v8a>:
${CMAKE_SOURCE_DIR}/prebuilt/arm64/libneon_accel.a>
# armeabi-v7a架构
$<$<STREQUAL:$<TARGET_ARCH>,armeabi-v7a>:
${CMAKE_SOURCE_DIR}/prebuilt/armv7/libneon_accel.a>
# x86_64架构(没有NEON,用普通版本)
$<$<STREQUAL:$<TARGET_ARCH>,x86_64>:
${CMAKE_SOURCE_DIR}/prebuilt/x86_64/libgeneric.a>
)
这里用了$<STREQUAL>来做字符串比较。说实话,这种嵌套写法看起来有点吓人,但用熟了就会发现它非常灵活。我建议你从简单的开始,比如先只用$<CONFIG>,慢慢再尝试复杂的组合。
1.6 知识体系总览
为了让你对生成器表达式有个整体认识,我画了一张图:
从这张图你可以看到,生成器表达式覆盖了配置查询、目标信息、平台判断、条件链接和字符串操作这几个核心领域。它们之间可以互相组合,形成非常强大的条件逻辑。
1.7 避坑指南
最后,分享几个我踩过的坑:
- 不要滥用嵌套:三层以上的嵌套可读性会急剧下降。我曾经写过一个五层嵌套的表达式,两周后自己都看不懂了。建议拆成多个变量。
- 注意布尔上下文:
$<CONFIG:Debug>返回的是1或0,不是true或false。在$<IF:cond:true:false>中,cond必须是0或1。 - 调试技巧:可以用
message(STATUS "config = $<CONFIG>")来打印生成器表达式的值。不过要注意,message命令在配置阶段执行,所以有些生成器表达式可能显示为空。
好了,生成器表达式的基础内容就这些。说白了,它就是CMake里的一把瑞士军刀,用好了能让你的构建脚本变得既简洁又强大。下一节我们会深入更多实战案例,看看怎么在NDK项目里把这些技巧用起来。
公众号:蓝海资料掘金营,微信deep3321