3、CMake语法基础:注释、命令调用、变量、列表、流程控制、函数与宏

好,咱们开始聊CMake的语法。说实话,很多初学者一上来就盯着CMakeLists.txt看,觉得它跟Shell脚本差不多。嗯,有点像,但又不完全是。我当年从Makefile转过来的时候,也踩过不少坑。今天这一章,我把最核心的语法点给你捋一遍。

3.1 注释:给未来的自己留点线索

注释这事儿,看着简单,但我在项目里见过太多「不写注释,或者写了等于没写」的情况。CMake里注释分两种:

  • 单行注释:用 # 开头,跟Python一样。
  • 块注释:用 #[[]] 包裹起来。
# 这是一个单行注释
# 我习惯在关键逻辑前加一行注释

#[[
  这是一个块注释
  可以跨多行
  适合临时屏蔽一大段代码
]]
我的习惯:块注释我用的不多。真要屏蔽代码,我更喜欢用 if(FALSE) 包起来,因为很多编辑器对块注释的高亮支持不太好。

3.2 命令调用:CMake的「函数调用」

CMake里一切皆命令。命令名不区分大小写,但我建议全用小写。为什么?因为CMake官方文档和大多数开源项目都这么写,保持一致省心。

# 命令调用格式:命令名(参数1 参数2 ...)
message("Hello, CMake!")
add_executable(myapp main.cpp utils.cpp)

参数之间用空格分隔。如果参数本身包含空格,就用双引号包起来。这一点跟Shell很像。

注意:CMake命令的参数不是用逗号分隔的!我见过有人写成 add_executable(myapp, main.cpp),结果CMake报错半天找不到原因。记住,空格,不是逗号。

3.3 变量:存值、取值、作用域

变量是CMake里最基础的东西。用 set() 来设值,用 ${变量名} 来取值。

set(MY_VAR "Hello")
message("MY_VAR = ${MY_VAR}")  # 输出:MY_VAR = Hello

变量名是大小写敏感的。MY_VARmy_var 是两个不同的变量。我个人习惯全大写,这样一眼就能看出是CMake变量,而不是普通字符串。

作用域这块,我简单说一下:

  • CMakeLists.txt 顶层设置的变量,默认是全局的。
  • function() 内部设置的变量,默认是局部的。
  • add_subdirectory() 引入的子目录中,变量默认是继承的,但子目录的修改不会影响父目录。

避坑指南:我曾经在一个大项目里,子目录的CMakeLists.txt里改了某个变量,以为父目录也会跟着变。结果折腾了两小时,最后发现要用 set(PARENT_SCOPE) 才行。记住:子目录改变量,默认不影响父目录。

3.4 列表:其实就是分号分隔的字符串

CMake里没有真正的数组类型。列表本质上就是一个字符串,元素之间用分号 ; 分隔。

set(MY_LIST a b c)          # 等价于 set(MY_LIST "a;b;c")
message("${MY_LIST}")       # 输出:a;b;c

# 获取列表长度
list(LENGTH MY_LIST len)
message("len = ${len}")     # 输出:len = 3

# 遍历列表
foreach(item IN LISTS MY_LIST)
    message("item = ${item}")
endforeach()

你想想看,为什么CMake要这么设计?说白了,就是为了跟命令行参数保持一致。你在命令行里传参数,不也是用空格隔开吗?CMake把空格转成分号,内部统一处理。

小技巧:如果你要往列表里追加元素,用 list(APPEND) 比用字符串拼接更安全。因为字符串拼接可能会搞乱分号。

3.5 流程控制:if/else/while/foreach

流程控制是写复杂构建逻辑的基础。咱们一个一个看。

3.5.1 if/elseif/else/endif

set(BUILD_SHARED_LIBS ON)

if(BUILD_SHARED_LIBS)
    message("Building shared libraries")
elseif(BUILD_STATIC_LIBS)
    message("Building static libraries")
else()
    message("Unknown build type")
endif()

条件判断里,ONYESTRUE1 都视为真。OFFNOFALSE0、空字符串、未定义变量都视为假。

注意:变量名在 if() 里可以直接写,不用加 ${}。但如果你写 if(${BUILD_SHARED_LIBS}),CMake会先展开变量,再判断。大多数情况下结果一样,但遇到复杂表达式时可能会有微妙差异。我建议统一不加 ${}

3.5.2 while/endwhile

while循环用得不多,但偶尔有用。比如等待某个条件满足。

set(COUNTER 0)
while(COUNTER LESS 5)
    message("COUNTER = ${COUNTER}")
    math(EXPR COUNTER "${COUNTER} + 1")
endwhile()

3.5.3 foreach/endforeach

foreach是遍历列表的利器。我几乎每个项目都会用到。

set(SOURCES main.cpp utils.cpp network.cpp)

foreach(src ${SOURCES})
    message("Processing ${src}")
    # 这里可以做编译、复制等操作
endforeach()

foreach还有几种变体,比如 foreach(RANGE 1 10) 可以生成数字序列。具体用法可以查文档,但上面这种 IN LISTS 形式最常用。

3.6 函数与宏:封装你的构建逻辑

当你的CMakeLists.txt超过100行时,就该考虑用函数或宏来封装了。两者很像,但有一个关键区别。

3.6.1 函数 function()

函数会创建新的变量作用域。函数内部设置的变量,默认不会影响外部。

function(add_my_library lib_name)
    add_library(${lib_name} STATIC ${lib_name}.cpp)
    target_include_directories(${lib_name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
endfunction()

# 调用
add_my_library(mylib)

函数内部可以用 set(var PARENT_SCOPE) 来把变量传递到父作用域。这个我前面提过,是很多人的坑。

3.6.2 宏 macro()

宏不会创建新作用域。宏内部的变量,直接修改外部的同名变量。

macro(print_var var_name)
    message("${var_name} = ${${var_name}}")
endmacro()

set(MY_VAR "hello")
print_var(MY_VAR)  # 输出:MY_VAR = hello

宏看起来更方便,但我个人更推荐用函数。为什么?因为宏的变量污染问题,在大型项目里很容易引发难以调试的bug。我曾经在一个项目里,因为宏内部不小心改了一个外部变量,导致整个构建流程乱掉,查了整整一个下午。

我的建议:除非你明确需要修改外部变量,否则一律用函数。函数的作用域隔离,是CMake里最优雅的设计之一。

3.7 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引。

CMake语法基础 注释 # 单行注释 #[[ 块注释 ]] 命令调用 命令名(参数1 参数2) 小写命名,空格分隔 变量 set() / ${} 作用域:全局/局部/PARENT_SCOPE 列表 分号分隔的字符串 list() 命令操作 流程控制 if/elseif/else/endif while/endwhile foreach/endforeach 函数与宏 function():独立作用域 macro():共享作用域 推荐用 function() 掌握这些,你已经能看懂80%的CMakeLists.txt了

3.8 小结

这一章的内容,说白了就是CMake的「语法骨架」。注释、命令、变量、列表、流程控制、函数与宏,这些你掌握了,写一个中等规模的CMakeLists.txt完全没问题。

我个人觉得,最难的是作用域的理解。函数、宏、子目录、PARENT_SCOPE,这些概念一开始容易混淆。我的建议是:多写几个小例子,用 message() 打印变量值,亲眼看看它们怎么变,比死记硬背强得多。

好,这一章就到这里。下一章咱们聊CMake里最实用的东西——查找依赖库和第三方包。到时候我会分享一些我在实际项目中踩过的坑,保证有用。


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