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})

这样看着清爽,也方便后面加文件。

注意:源文件列表里不要放头文件。虽然放了也没错,但没必要。CMake 会自动处理头文件依赖关系。

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 下是 .a
  • SHARED — 动态库,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 了。

核心原则:能用 PRIVATE 就别用 PUBLIC。过度暴露依赖关系会让构建图变得混乱。我见过一个项目把所有链接都写成 PUBLIC,结果改一个库要重新编译半个项目。

知识体系总览

下面这张图把 CMakeLists.txt 的核心结构串起来了。你可以看到从项目声明到最终产物的完整链路:

CMakeLists.txt 核心结构 cmake_minimum_required project(项目名 语言) add_executable(可执行文件) add_library(静态/动态库) add_library(OBJECT库) target_link_libraries(目标 PRIVATE/PUBLIC 依赖) 最终产物:.exe / .lib / .dll / .so / .a 核心流程:声明项目 → 定义目标(可执行文件/库) → 链接依赖 → 生成产物

一个完整的例子

把上面这些串起来,写一个稍微像样的 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 试试,把之前手写的编译命令替换掉,你会感受到「自动化」的甜头的。