4. CMake语法基础:注释、变量、列表、命令调用、条件判断、循环

各位同学,欢迎来到第四章。说实话,很多嵌入式工程师第一次接触CMake,都会被它的语法吓到——跟C语言长得像,但又不完全一样。我当年刚转过来的时候,也花了点时间适应。不过你放心,CMake的语法其实比C语言简单多了,核心就那么几个东西。

这一章,咱们就把CMake的语法地基打牢。你想想看,盖房子最重要的是什么?地基。CMake项目也一样,语法基础不牢,后面写复杂脚本的时候就会各种踩坑。来,咱们一个一个过。

4.1 注释:给代码加点说明

CMake的注释分两种:单行注释和块注释。单行注释用 #,跟shell脚本一样。块注释用 #[[ ]],这是CMake 3.0之后才有的特性。

# 这是单行注释
set(MY_VAR "hello")  # 行尾也可以加注释

#[[
  这是块注释
  可以跨多行
  适合临时屏蔽一大段代码
]]

我个人习惯,在CMakeLists.txt里多用单行注释。块注释用得少,除非是调试时临时屏蔽一大段逻辑。嗯,这里要注意:块注释不能嵌套,如果你在注释块里又写了一个 #[[ ]],CMake会报错。

小技巧: 我经常在变量赋值后面加注释,说明这个变量是干什么用的。比如 set(BAUD_RATE 115200) # 串口波特率。半年后你回来看代码,会感谢自己的。

4.2 变量:CMake里的"容器"

CMake里的变量,说白了就是一个字符串容器。你用 set() 命令来赋值,用 ${} 来取值。就这么简单。

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

变量名是大小写敏感的。MY_VARmy_var 是两个不同的变量。我在项目中遇到过有人因为大小写写错,排查了半天找不到问题——最后发现是变量名拼错了。

变量还可以用 unset() 来删除:

unset(MY_VAR)  # 删除变量

另外,CMake里有一些内置变量,比如 CMAKE_CURRENT_SOURCE_DIR(当前源码目录)、CMAKE_BINARY_DIR(构建目录)。这些变量你不用自己定义,直接用就行。

重点: 变量取值时,如果变量不存在,${VAR} 会展开为空字符串,不会报错。这一点跟C语言不一样,要小心。

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

CMake里的列表,本质上就是一个字符串,只不过用分号 ; 来分隔各个元素。你想想看,是不是很简单?

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

列表的常用操作包括:

  • list(APPEND) — 追加元素
  • list(LENGTH) — 获取长度
  • list(GET) — 获取指定索引的元素
  • list(REMOVE_ITEM) — 删除指定元素
set(SOURCES "main.c" "uart.c" "spi.c")
list(APPEND SOURCES "gpio.c")       # 现在有4个元素
list(LENGTH SOURCES LEN)            # LEN = 4
list(GET SOURCES 0 FIRST_FILE)      # FIRST_FILE = "main.c"

我在嵌入式项目里,最常用列表来管理源文件。比如把某个模块的所有.c文件放在一个列表里,然后统一加到 add_executable() 里。这样代码干净多了。

注意: 列表索引从0开始。如果你写 list(GET SOURCES 4 ...) 而列表只有4个元素(索引0-3),CMake会报错。我曾经踩过这个坑,索引越界了。

4.4 命令调用:函数调用的基本姿势

CMake里的命令调用,语法是:命令名(参数1 参数2 ...)。参数之间用空格分隔,如果参数本身包含空格,就用双引号包起来。

message("Hello, CMake!")          # 输出一条消息
set(MY_VAR "value with spaces")   # 带空格的参数
add_executable(my_app main.c)     # 添加可执行文件

命令名不区分大小写。你可以写 MESSAGE(),也可以写 message(),效果一样。但我个人建议统一用小写,看着舒服,也符合大多数人的习惯。

命令的参数可以是:

  • 普通字符串:set(VAR hello)
  • 带引号的字符串:set(VAR "hello world")
  • 变量引用:set(VAR ${OTHER_VAR})
  • 列表:set(VAR a b c)

说白了,CMake的命令调用就是"命令名 + 参数列表"。没有C语言那种括号嵌套的复杂语法,简单直接。

4.5 条件判断:让CMake"动脑子"

条件判断用 if()elseif()else()endif()。语法跟C语言有点像,但注意:endif() 后面要跟条件名(可选),我建议加上,方便阅读。

set(USE_FREERTOS ON)

if(USE_FREERTOS)
    message("FreeRTOS is enabled")
    add_subdirectory(freertos)
else()
    message("FreeRTOS is disabled")
endif()

条件判断支持的操作符:

操作符说明示例
STREQUAL字符串相等if(VAR STREQUAL "hello")
EQUAL数值相等if(COUNT EQUAL 10)
LESS数值小于if(COUNT LESS 5)
DEFINED变量是否已定义if(DEFINED VAR)
NOT逻辑非if(NOT USE_FREERTOS)
AND / OR逻辑与/或if(A AND B)

我在项目中遇到过最典型的场景:根据芯片型号选择不同的编译选项。比如STM32F4和STM32F7,启动文件不一样,链接脚本也不一样。用条件判断,一个CMakeLists.txt就能搞定多个芯片。

避坑指南: 我曾经在 if() 里直接写 if(VAR) 来判断变量是否非空。但CMake里,如果VAR的值是 OFFNOFALSE0、空字符串,都会被当作假。所以最好用 if(DEFINED VAR) 来判断变量是否存在,用 if(VAR STREQUAL "xxx") 来判断具体值。

4.6 循环:foreach 和 while

CMake的循环主要有两种:foreach()while()。foreach用得最多,while偶尔用。

4.6.1 foreach 循环

遍历列表里的每个元素:

set(SOURCES "main.c" "uart.c" "spi.c")
foreach(FILE ${SOURCES})
    message("Processing ${FILE}")
endforeach()

还可以用 RANGE 来生成数字序列:

foreach(I RANGE 1 5)
    message("I = ${I}")  # 输出 1 2 3 4 5
endforeach()

或者指定步长:

foreach(I RANGE 0 10 2)
    message("I = ${I}")  # 输出 0 2 4 6 8 10
endforeach()

4.6.2 while 循环

while循环用得少,但有时候也管用:

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

注意:CMake没有 breakcontinue(至少在旧版本里没有)。如果你需要提前退出循环,得用 if() 包一下逻辑。嗯,这一点确实不太方便,但实际项目中很少需要复杂的循环控制。

实用场景: 我经常用foreach来批量处理源文件。比如给每个源文件添加编译选项,或者生成依赖关系。你想想看,如果有50个.c文件,手动写50次 set_source_files_properties() 多累啊。一个foreach循环就搞定了。

4.7 本章知识体系

下面这张图,把CMake语法的核心知识点串起来了。你可以把它当作一个快速参考图,写CMakeLists.txt的时候瞄一眼。

CMake语法基础核心结构 CMake语法 注释 # 单行注释 #[[ 块注释 ]] 变量 set() / unset() ${VAR} 取值 列表 分号分隔的字符串 list(APPEND / LENGTH ...) 命令调用 命令名(参数1 参数2) 参数用空格分隔 条件判断 if() / elseif() / else() / endif() STREQUAL / EQUAL / DEFINED 循环 foreach() / while() RANGE / 列表遍历

这张图把注释、变量、列表、命令调用、条件判断、循环这六个核心知识点串在了一起。你写CMakeLists.txt的时候,脑子里过一遍这张图,基本就不会跑偏。

好了,这一章的内容就到这里。语法基础打牢了,后面咱们就可以开始写真正的嵌入式构建脚本了。记住:CMake语法不复杂,复杂的是你用它来解决什么问题。多写、多练、多踩坑,慢慢就熟练了。


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