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 知识体系总览

为了让你更直观地理解这三者的关系,我画了一张图:

CMakeLists.txt 核心结构 cmake_minimum_required() 版本控制 指定最低 CMake 版本 防止旧版本兼容问题 project() 项目定义 设置项目名称 指定语言和版本号 add_executable() 生成可执行文件 指定目标名称 列出所有源文件 输出:生成对应平台的构建文件 Makefile | Ninja | Visual Studio .sln | Xcode 三者缺一不可,顺序固定:版本 → 项目 → 目标

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 的基础语法其实不复杂,核心就是这三条命令。但别小看它们,后面所有复杂的构建逻辑都是在这三行基础上扩展出来的。