2、CMake基础入门:CMakeLists.txt 基本语法、project() 与 add_executable()、cmake_minimum_required() 版本控制
好,咱们正式开始聊 CMake。说实话,很多刚接触跨平台开发的朋友,一上来就被各种构建系统搞懵了。Makefile、Autotools、Ninja、MSBuild…… 每个平台都有自己的那一套。而 CMake 的出现,说白了就是帮你把这些乱七八糟的东西统一起来。
你只需要写一份 CMakeLists.txt,CMake 就能自动帮你生成对应平台的构建文件。比如在 Linux 上生成 Makefile,在 Windows 上生成 Visual Studio 的 .sln 工程文件。我个人习惯把 CMake 称为「构建系统的翻译官」—— 你对着它说人话,它帮你跟各个平台的构建工具沟通。
核心思想:CMake 不是编译器,也不是构建工具。它是一个构建系统生成器。你写 CMakeLists.txt,它负责生成 Makefile 或 Ninja 文件,然后真正的编译工作由 make 或 ninja 完成。
2.1 CMakeLists.txt 长什么样?
每个使用 CMake 的项目,根目录下都必须有一个 CMakeLists.txt 文件。这是 CMake 的入口文件,也是你与 CMake 对话的唯一窗口。
一个最简单的 CMakeLists.txt 大概长这样:
cmake_minimum_required(VERSION 3.10)
project(HelloWorld)
add_executable(hello main.cpp)
就三行。嗯,你没看错,三行就能构建一个可执行程序。我刚开始用 CMake 的时候,也觉得这东西比写 Makefile 省心太多了。
2.2 cmake_minimum_required():版本控制
第一行 cmake_minimum_required(VERSION 3.10) 是干什么用的?
它告诉 CMake:这个项目至少需要 3.10 版本才能构建。如果用户机器上装的 CMake 版本低于 3.10,CMake 会直接报错退出,不会继续往下执行。
为什么要加这一行?我在项目中遇到过好几次这样的坑:
- 同事的机器上 CMake 版本太老,不支持某些新语法
- CI 服务器上的 CMake 版本和本地不一致,导致构建失败
- 某些 CMake 模块在旧版本中行为不同,引发诡异 bug
我曾经踩过的坑:有一次我用了 target_sources() 这个命令,它在 CMake 3.1 才引入。但我写的 cmake_minimum_required(VERSION 2.8),结果在同事的机器上编译报错,排查了半天才发现是版本问题。从那以后,我养成了一个习惯:每次用新特性,都会查一下它从哪个版本开始支持,然后相应地提高最低版本要求。
版本号怎么选?我个人建议:
- 如果你用 Ubuntu 18.04,默认 CMake 是 3.10,那就写
3.10 - 如果你用 Ubuntu 20.04,默认是 3.16,那就写
3.16 - 如果你不确定,保守一点写
3.10基本够用 - 但别写太老,比如
2.8—— 很多现代语法都不支持
2.3 project():给项目起个名字
第二行 project(HelloWorld) 是给项目起名字。这个名字会影响到后面很多变量的命名,比如 PROJECT_NAME、PROJECT_SOURCE_DIR、PROJECT_BINARY_DIR 等等。
你还可以在 project() 里指定语言:
project(HelloWorld LANGUAGES CXX)
这样 CMake 就只会检查 C++ 编译器,不会去管 C 编译器。如果你的项目只用 C++,加上这个能加快配置速度。
另外,project() 还支持设置版本号:
project(HelloWorld VERSION 1.0.0 LANGUAGES CXX)
设置之后,你就可以在代码里通过 PROJECT_VERSION 拿到这个版本号。嗯,这个功能在发布软件包时特别有用。
2.4 add_executable():生成可执行文件
第三行 add_executable(hello main.cpp) 告诉 CMake:我要生成一个叫 hello 的可执行文件,它的源代码是 main.cpp。
语法很简单:
add_executable(目标名称 源文件1 源文件2 ...)
目标名称 hello 就是最终生成的可执行文件名。在 Windows 上会生成 hello.exe,在 Linux/macOS 上会生成 hello。
如果有多个源文件怎么办?直接列在后面就行:
add_executable(hello main.cpp utils.cpp network.cpp)
但项目大了之后,把所有源文件都写在一行里会变得非常难看。这时候我建议用变量来管理:
set(SOURCES
main.cpp
utils.cpp
network.cpp
database.cpp
config.cpp
)
add_executable(hello ${SOURCES})
这样清晰多了,对吧?
小技巧:如果你用 file(GLOB ...) 来收集源文件,比如 file(GLOB SOURCES src/*.cpp),要小心一点。这种方式不会自动检测新增文件。如果你在 src 目录下新建了一个 .cpp 文件,必须重新运行 cmake 才能把它加进来。我个人更倾向于手动列出源文件,虽然麻烦一点,但更可控。
2.5 知识体系总览
为了让你更直观地理解这三者的关系,我画了一张图:
2.6 一个完整的例子
把上面这些串起来,一个完整的 CMakeLists.txt 应该是这样的:
# 指定最低 CMake 版本
cmake_minimum_required(VERSION 3.10)
# 定义项目名称和版本号
project(MyApp VERSION 1.2.0 LANGUAGES CXX)
# 收集源文件
set(SOURCES
src/main.cpp
src/network.cpp
src/utils.cpp
src/config.cpp
)
# 生成可执行文件
add_executable(myapp ${SOURCES})
然后你在终端里执行:
mkdir build
cd build
cmake ..
make
就能看到 myapp 被编译出来了。是不是很简单?
个人习惯:我一般会在项目根目录下建一个 build 目录,所有构建产物都放在里面。这样源码目录保持干净,想清理的时候直接 rm -rf build 就行,不会误删源文件。
2.7 常见问题与避坑
- 文件名大小写:Linux 上区分大小写,Windows 上不区分。但为了跨平台,我建议统一用小写。
- 路径分隔符:CMake 内部统一用
/,即使在 Windows 上也是。别写成\,会出问题。 - 注释用 #:CMakeLists.txt 的注释符号是
#,不是//也不是/* */。 - 变量引用用 ${}:比如
${SOURCES},别漏了花括号。
嗯,这一章的内容就到这里。CMake 的基础语法其实不复杂,核心就是这三条命令。但别小看它们,后面所有复杂的构建逻辑都是在这三行基础上扩展出来的。