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) |
-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})
这个脚本做了几件事:先收集源文件,再根据操作系统添加平台相关的文件,最后生成可执行文件。整个过程用到了变量、列表、条件判断和循环。
下一章我们会深入 add_executable 和 add_library,看看怎么把源文件组织成真正的构建目标。到时候会用到今天学的列表和条件判断,所以这一章的内容一定要动手敲一遍。