4、CMake入门:CMakeLists.txt基本结构、project与add_executable、add_library、target_link_libraries
说到构建系统,CMake 几乎是 C/C++ 项目的标配了。我刚开始接触它的时候,也觉得语法有点怪——又是大写命令又是小写参数,看着不像正经编程语言。但用顺手之后你会发现,它其实挺朴素的。
这一章我们就把 CMake 最核心的几个命令讲透。说白了,你只要搞懂 project、add_executable、add_library 和 target_link_libraries 这四个东西,就能写出 80% 项目的 CMakeLists.txt 了。
CMakeLists.txt 长什么样?
每个用 CMake 的项目,根目录下都得有一个 CMakeLists.txt。它就是个纯文本文件,CMake 读它来生成构建系统(比如 Makefile 或 Visual Studio 工程)。
一个最简单的 CMakeLists.txt 大概是这样:
cmake_minimum_required(VERSION 3.10)
project(HelloWorld)
add_executable(hello main.cpp)
三行,搞定。第一行指定 CMake 最低版本,第二行声明项目名,第三行告诉 CMake:「我要编译一个叫 hello 的可执行文件,源文件是 main.cpp」。
cmake_minimum_required(VERSION 3.10) 或更高。太老的版本有些特性不支持,太新的版本又怕用户环境没有。3.10 是个比较稳妥的起点。
project() — 给项目起个名
project() 不只是起名字那么简单。它会帮你设置好几个变量:
| 变量名 | 含义 | 示例值 |
|---|---|---|
PROJECT_NAME |
项目名称 | HelloWorld |
PROJECT_SOURCE_DIR |
项目源码根目录 | /home/user/hello |
PROJECT_BINARY_DIR |
构建目录 | /home/user/hello/build |
你还可以指定语言:
project(MyApp C CXX)
这样 CMake 就只检测 C 和 C++ 编译器。我遇到过有人不写语言列表,结果 CMake 去检测 Fortran、CUDA 什么的,白白浪费时间。
add_executable() — 生成可执行文件
这个命令最常用。格式很简单:
add_executable(目标名 源文件1 源文件2 ...)
举个例子:
add_executable(myapp main.cpp utils.cpp network.cpp)
CMake 会把这三个 .cpp 编译链接成一个叫 myapp 的可执行文件(Windows 下是 myapp.exe)。
源文件多了怎么办?我一般会用变量来存:
set(SOURCES
main.cpp
utils.cpp
network.cpp
database.cpp
config.cpp
)
add_executable(myapp ${SOURCES})
这样看着清爽,也方便后面加文件。
add_library() — 生成库文件
库分两种:静态库和动态库。CMake 用 add_library() 来创建:
add_library(mylib STATIC lib.cpp lib_utils.cpp)
add_library(mylib_shared SHARED lib.cpp lib_utils.cpp)
STATIC— 静态库,Windows 下是.lib,Linux 下是.aSHARED— 动态库,Windows 下是.dll,Linux 下是.so
还有一种叫 OBJECT 库,它不生成 .lib 或 .so,而是把 .o 文件打包起来,后面再链接到别的目标里。这个我用的不多,但某些场景下挺有用。
我个人的建议是:内部模块用静态库,发布给第三方用动态库。静态库省心,不会出现「DLL 找不到」的尴尬。
target_link_libraries() — 把东西连起来
有了可执行文件和库,怎么让它们产生关系?用 target_link_libraries():
add_executable(myapp main.cpp)
add_library(mylib STATIC lib.cpp)
target_link_libraries(myapp PRIVATE mylib)
这里 PRIVATE 是链接范围。CMake 提供了三种:
| 关键字 | 含义 | 使用场景 |
|---|---|---|
PRIVATE |
只链接给当前目标 | 内部实现依赖 |
PUBLIC |
链接给当前目标,也传给依赖它的目标 | 接口暴露 |
INTERFACE |
不链接给当前目标,只传给依赖它的目标 | 纯头文件库 |
举个例子你就明白了:
add_library(A STATIC a.cpp)
add_library(B STATIC b.cpp)
target_link_libraries(B PUBLIC A)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE B)
因为 B 对 A 是 PUBLIC,所以 myapp 链接 B 时,也会自动链接 A。如果 B 对 A 是 PRIVATE,那 myapp 就看不到 A 了。
知识体系总览
下面这张图把 CMakeLists.txt 的核心结构串起来了。你可以看到从项目声明到最终产物的完整链路:
一个完整的例子
把上面这些串起来,写一个稍微像样的 CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(Calculator CXX)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 源文件
set(APP_SOURCES main.cpp ui.cpp)
set(LIB_SOURCES calc.cpp math_utils.cpp)
# 定义目标
add_library(calc_lib STATIC ${LIB_SOURCES})
add_executable(calculator ${APP_SOURCES})
# 链接
target_link_libraries(calculator PRIVATE calc_lib)
# 包含头文件路径
target_include_directories(calc_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
这里多了个 target_include_directories,它告诉编译器去哪里找头文件。用 PUBLIC 的话,链接 calc_lib 的目标也能自动加上这个路径。
target_include_directories,结果编译时一堆「找不到头文件」的错误。排查了半天才发现是路径没传过去。记住:头文件路径不会自动传递,你得显式声明。
关于构建目录
CMake 推荐「out-of-source」构建,也就是在源码目录外面建一个 build 文件夹:
mkdir build
cd build
cmake ..
cmake --build .
这样做的好处是:源码目录干干净净,想重新构建直接删掉 build 文件夹就行。我见过有人直接在源码目录里构建,结果 git status 一看全是编译产物,乱得很。
小结
这一章的内容其实就四个命令:
project()— 给项目起名,设置环境add_executable()— 生成可执行文件add_library()— 生成库文件target_link_libraries()— 把目标连起来
掌握了这些,你已经能应付大多数中小型项目了。CMake 还有很多高级特性,但核心骨架就是这些。后面几章我们会在这个基础上继续加东西——比如条件判断、自定义命令、安装规则等等。
嗯,先消化这些吧。写个简单的 CMakeLists.txt 试试,把之前手写的编译命令替换掉,你会感受到「自动化」的甜头的。