第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_commandinstall等地方。我曾经踩过这个坑,在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 知识体系总览

为了让你对生成器表达式有个整体认识,我画了一张图:

生成器表达式 语法结构 $<...> 基本形式 $<cond:true:false> 配置查询 $<CONFIG> $<CONFIG:Debug> 目标信息 $<TARGET_FILE> $<TARGET_FILE_NAME> 平台判断 $<PLATFORM_ID> $<ANDROID> $<TARGET_ARCH> 条件链接 $<$<CONFIG:Debug>:lib> 嵌套条件组合 字符串操作 $<STREQUAL:a,b> $<JOIN:list:,> 构建阶段求值 · 灵活适配多平台 · 减少重复代码

从这张图你可以看到,生成器表达式覆盖了配置查询、目标信息、平台判断、条件链接和字符串操作这几个核心领域。它们之间可以互相组合,形成非常强大的条件逻辑。

1.7 避坑指南

最后,分享几个我踩过的坑:

  • 不要滥用嵌套:三层以上的嵌套可读性会急剧下降。我曾经写过一个五层嵌套的表达式,两周后自己都看不懂了。建议拆成多个变量。
  • 注意布尔上下文$<CONFIG:Debug>返回的是1或0,不是true或false。在$<IF:cond:true:false>中,cond必须是0或1。
  • 调试技巧:可以用message(STATUS "config = $<CONFIG>")来打印生成器表达式的值。不过要注意,message命令在配置阶段执行,所以有些生成器表达式可能显示为空。

好了,生成器表达式的基础内容就这些。说白了,它就是CMake里的一把瑞士军刀,用好了能让你的构建脚本变得既简洁又强大。下一节我们会深入更多实战案例,看看怎么在NDK项目里把这些技巧用起来。


公众号:蓝海资料掘金营,微信deep3321