3、CMake基础语法:CMakeLists.txt文件结构,cmake_minimum_required,project,add_executable,message命令

好,咱们正式开始写CMake了。说实话,很多初学者一上来就被各种复杂的变量和函数吓住了。其实CMake的核心语法就那么几条命令,你把它搞明白了,后面再学什么target、库依赖,都是水到渠成的事。

这一节,我带你手把手写第一个CMakeLists.txt。咱们从最基础的文件结构讲起,再到四个最常用的命令。嗯,你想想看,一个工程再复杂,它的根目录下也离不开这几行代码。

3.1 CMakeLists.txt 文件结构

CMake的构建规则,全部写在 CMakeLists.txt 这个文件里。每个源码目录下,都可以放一个。但最顶层那个,是CMake的入口。

一个典型的文件结构长这样:

project_root/
├── CMakeLists.txt          # 顶层构建文件
├── src/
│   ├── main.c
│   └── CMakeLists.txt      # 子目录构建文件(可选)
├── include/
│   └── mylib.h
└── build/                  # 构建目录(手动创建)

我个人习惯,在项目根目录只放一个顶层CMakeLists.txt,把所有源文件都列清楚。除非项目特别大,才拆分子目录。为什么呢?因为简单啊,你一眼就能看到整个项目的构建全貌。

小提示: 构建目录(build)和源码目录分开,这是CMake的推荐做法。你可以在build里随便折腾,删了重来也不影响源码。我在项目中吃过这个亏——直接在源码目录下构建,结果一堆临时文件混在一起,clean都clean不干净。

3.2 cmake_minimum_required — 指定CMake最低版本

这是CMakeLists.txt的第一行,几乎每个项目都有。它的作用很简单:告诉CMake,你的版本不能低于我写的这个数。

cmake_minimum_required(VERSION 3.10)

为什么要写这个?因为不同版本的CMake,语法和功能有差异。比如3.10之后才支持 if(DEFINED) 的某些写法。如果你用新语法,但用户机器上装的是老版本CMake,构建就会报错。

我曾经遇到过这样一个坑:同事在本地用CMake 3.20写了个项目,用了 IN_LIST 操作符。结果CI服务器上装的是3.5,直接报语法错误。排查了半天,最后发现是版本问题。从那以后,我每个项目都会老老实实写上 cmake_minimum_required。

注意: 版本号不要写得太高,也不要太低。写太高,用户可能没有;写太低,你又用不了新特性。我一般写3.10或3.16,这两个版本比较稳定,功能也够用。

3.3 project — 定义项目名称和语言

project() 命令用来给项目起个名字,顺便告诉CMake我们用什么语言。

project(MyProject LANGUAGES C CXX)

这里 MyProject 是项目名,LANGUAGES 指定了语言。C代表C语言,CXX代表C++。如果你只写C,那CMake就不会去找C++编译器。

这个命令还会自动生成几个变量,比如:

变量名 含义
PROJECT_NAME 项目名称,就是"MyProject"
PROJECT_SOURCE_DIR 源码根目录路径
PROJECT_BINARY_DIR 构建目录路径

你想想看,有了这些变量,后面写路径就方便多了。比如你要引用源码目录下的某个文件,直接写 ${PROJECT_SOURCE_DIR}/src/main.c 就行。

3.4 add_executable — 生成可执行文件

这是最常用的命令之一。它的作用是把源文件编译成一个可执行程序。

add_executable(myapp src/main.c src/utils.c)

第一个参数 myapp 是目标名称,也就是生成的可执行文件名(在Windows上会变成 myapp.exe)。后面跟的是源文件列表,可以写多个,用空格隔开。

我刚开始用CMake时,总喜欢把所有源文件写在一行。后来项目大了,源文件一多,看着就头疼。现在我习惯用变量来管理:

set(SOURCES
    src/main.c
    src/utils.c
    src/network.c
    src/parser.c
)

add_executable(myapp ${SOURCES})

这样清晰多了,对吧?而且后面要加文件,直接在 SOURCES 里加一行就行,不用改 add_executable 那行。

重点: add_executable 里的源文件路径,是相对于当前CMakeLists.txt所在目录的。如果你在顶层写,那路径就是相对于项目根目录。

3.5 message — 打印调试信息

写CMake时,最怕的就是变量值不对,或者条件判断没走对。这时候 message() 就是你的救命稻草。

message("Hello, CMake!")
message(STATUS "当前项目名称: ${PROJECT_NAME}")
message(WARNING "这个变量可能没定义")
message(FATAL_ERROR "出错了,停止构建")

message 有几种模式:

模式 说明
无前缀 普通信息,直接打印
STATUS 状态信息,前面会加两个横线,比如 -- 当前项目名称: MyProject
WARNING 警告,会显示黄色提示
FATAL_ERROR 致命错误,打印后直接停止构建

我个人调试时,最喜欢用 STATUS。因为它不会中断构建,又能看到变量值。比如:

message(STATUS "源文件列表: ${SOURCES}")

这样在CMake配置阶段,你就能看到 SOURCES 变量里到底有哪些文件。如果漏了某个文件,一眼就能发现。

调试技巧: 如果你不确定某个变量有没有定义,可以用 if(DEFINED 变量名) 判断一下,然后用 message 打印出来。我曾经靠这个办法,排查出一个因为变量名拼写错误导致的链接失败问题。

3.6 完整示例:第一个CMakeLists.txt

好了,咱们把上面讲的四个命令串起来,写一个完整的例子:

# 指定CMake最低版本
cmake_minimum_required(VERSION 3.10)

# 定义项目
project(HelloWorld LANGUAGES C)

# 设置源文件列表
set(SOURCES
    src/main.c
    src/hello.c
)

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

# 打印一些信息
message(STATUS "项目名称: ${PROJECT_NAME}")
message(STATUS "源文件: ${SOURCES}")

把这个文件放在项目根目录,然后在 build 目录下执行:

mkdir build
cd build
cmake ..
make

你就会看到CMake打印出状态信息,然后编译出 hello 可执行文件。嗯,是不是很简单?

说实话,我当年第一次跑通这个流程时,还挺有成就感的。虽然只是打印了个"Hello World",但那种从源码到可执行文件的掌控感,真的很棒。

下一节,咱们会深入讲变量和列表操作。到时候你会发现,CMake的灵活性和强大之处,才刚刚开始展现。