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很像。
add_executable(myapp, main.cpp),结果CMake报错半天找不到原因。记住,空格,不是逗号。
3.3 变量:存值、取值、作用域
变量是CMake里最基础的东西。用 set() 来设值,用 ${变量名} 来取值。
set(MY_VAR "Hello")
message("MY_VAR = ${MY_VAR}") # 输出:MY_VAR = Hello
变量名是大小写敏感的。MY_VAR 和 my_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()
条件判断里,ON、YES、TRUE、1 都视为真。OFF、NO、FALSE、0、空字符串、未定义变量都视为假。
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 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引。
3.8 小结
这一章的内容,说白了就是CMake的「语法骨架」。注释、命令、变量、列表、流程控制、函数与宏,这些你掌握了,写一个中等规模的CMakeLists.txt完全没问题。
我个人觉得,最难的是作用域的理解。函数、宏、子目录、PARENT_SCOPE,这些概念一开始容易混淆。我的建议是:多写几个小例子,用 message() 打印变量值,亲眼看看它们怎么变,比死记硬背强得多。
好,这一章就到这里。下一章咱们聊CMake里最实用的东西——查找依赖库和第三方包。到时候我会分享一些我在实际项目中踩过的坑,保证有用。