4、基本语法:注释、命令、变量、列表、流程控制

好,咱们进入正题。CMake 的语法其实不复杂,但有几个坑,我当年刚接触时没少踩。今天把这几个核心概念捋清楚,后面写 CMakeLists.txt 就会顺手很多。

4.1 注释:给代码留个话

注释这事,说白了就是写给未来的自己看的。CMake 支持两种注释方式:

  • 单行注释:用 # 开头,行内有效
  • 块注释:用 #[[ 和 ]] 包裹
# 这是单行注释,我习惯在关键逻辑前加一行

#[[
  这是块注释
  可以跨多行
  适合临时屏蔽一大段代码
]]
我的习惯:块注释我一般只在调试时用。正式提交的代码里,我更倾向用单行注释,因为阅读起来更直观。

4.2 命令:CMake 的基本操作单元

CMake 的命令,其实就是函数调用。写法是:命令名(参数1 参数2 ...)。注意,参数之间用空格分隔,不是逗号。这一点和 C++ 不一样,我刚开始老写错。

# 定义一个可执行文件
add_executable(my_app main.cpp utils.cpp)

# 添加一个库
add_library(my_lib STATIC lib.cpp)

# 打印信息
message("Hello, CMake!")

命令名不区分大小写,但社区惯例是全小写。我个人也推荐小写,看着清爽。

4.3 变量:存东西、取东西

变量是 CMake 的灵魂。定义变量用 set(),读取变量用 ${}。

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

这里有个细节:${} 会在解析时被替换成变量的值。如果变量没定义,替换成空字符串,不会报错。嗯,这既是优点也是坑——我曾经因为拼错变量名,排查了半天。

避坑指南:变量名尽量用大写+下划线,比如 MY_PROJECT_SRC。这样一眼就能看出是 CMake 变量,而不是普通字符串。

4.4 列表:CMake 里的数组

CMake 没有真正的数组,它用「分号分隔的字符串」来模拟列表。说白了,列表就是一个字符串,里面用 ; 隔开各个元素。

set(SOURCES main.cpp utils.cpp helper.cpp)
# 等价于:set(SOURCES "main.cpp;utils.cpp;helper.cpp")

message("${SOURCES}")  # 输出:main.cpp;utils.cpp;helper.cpp

操作列表的常用命令:

命令 作用 示例
list(APPEND) 追加元素 list(APPEND SOURCES new.cpp)
list(LENGTH) 获取长度 list(LENGTH SOURCES len)
list(GET) 按索引取值 list(GET SOURCES 0 first)
list(REMOVE_ITEM) 删除指定元素 list(REMOVE_ITEM SOURCES old.cpp)
重点:列表索引从 0 开始。取最后一个元素用 -1,这个和 Python 一样,挺方便的。

4.5 流程控制:让 CMake 学会做决定

流程控制让 CMake 不再是「死板」的配置脚本。它可以根据条件、循环来动态生成构建规则。

4.5.1 if/elseif/else:条件判断

语法很简单,但要注意 if 和 endif 必须成对出现。

set(BUILD_TEST ON)

if(BUILD_TEST)
    message("构建测试代码")
    add_subdirectory(tests)
elseif(BUILD_BENCHMARK)
    message("构建性能测试")
    add_subdirectory(benchmarks)
else()
    message("只构建主程序")
endif()

条件表达式支持很多操作符:

  • EQUAL、LESS、GREATER:数值比较
  • STREQUAL、STRLESS:字符串比较
  • DEFINED:判断变量是否定义
  • EXISTS:判断文件或目录是否存在
if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
    message("这是顶层项目")
endif()

if(EXISTS "${CMAKE_SOURCE_DIR}/config.h.in")
    message("配置文件模板存在")
endif()
我的经验:判断变量是否定义时,用 if(DEFINED VAR) 比 if(VAR) 更安全。因为 if(VAR) 在变量值为 FALSE、OFF、0 时也会判为假,容易误判。

4.5.2 foreach:遍历列表

foreach 是我最常用的循环。它可以遍历列表、范围,甚至同时遍历多个列表。

# 遍历列表
set(FILES a.cpp b.cpp c.cpp)
foreach(F ${FILES})
    message("处理文件:${F}")
endforeach()

# 遍历范围(1 到 5)
foreach(I RANGE 1 5)
    message("迭代次数:${I}")
endforeach()

# 同时遍历多个列表(CMake 3.17+)
set(KEYS   one two three)
set(VALUES 1   2   3)
foreach(K V IN ZIP_LISTS KEYS VALUES)
    message("${K} -> ${V}")
endforeach()

注意 foreach 的循环变量(比如上面的 F)会在每次迭代中被重新赋值。循环结束后,它保留最后一次的值。

4.5.3 while:条件循环

while 用得相对少一些,但在某些场景下很管用。比如动态生成一组目标。

set(COUNT 0)
while(COUNT LESS 5)
    message("当前计数:${COUNT}")
    math(EXPR COUNT "${COUNT} + 1")
endwhile()

这里用 math() 做算术运算。CMake 本身不支持 COUNT++ 这种写法,得用 math() 或者 string(CONCAT) 来变通。

避坑指南:小心死循环!我曾经在 while 里忘记更新循环变量,结果 CMake 配置过程卡死了。建议在循环体内加一个 break() 作为安全出口。

4.6 综合示例:一个实用的配置脚本

把上面这些知识点串起来,写一个实际点的例子:

cmake_minimum_required(VERSION 3.16)
project(MyApp)

# 收集源文件
set(SOURCES main.cpp)
list(APPEND SOURCES utils.cpp helper.cpp)

# 根据平台添加不同源文件
if(WIN32)
    list(APPEND SOURCES win_specific.cpp)
elseif(APPLE)
    list(APPEND SOURCES mac_specific.cpp)
else()
    list(APPEND SOURCES linux_specific.cpp)
endif()

# 遍历源文件,打印出来(调试用)
foreach(SRC ${SOURCES})
    message("添加源文件:${SRC}")
endforeach()

# 生成可执行文件
add_executable(${PROJECT_NAME} ${SOURCES})

这个脚本做了几件事:先收集源文件,再根据操作系统添加平台相关的文件,最后生成可执行文件。整个过程用到了变量、列表、条件判断和循环。

总结一下:注释是给代码留话,命令是基本操作,变量和列表是数据容器,流程控制让脚本有了「智能」。把这几个点吃透,CMakeLists.txt 对你来说就不再是黑盒了。

下一章我们会深入 add_executable 和 add_library,看看怎么把源文件组织成真正的构建目标。到时候会用到今天学的列表和条件判断,所以这一章的内容一定要动手敲一遍。