Linux C++ 之 CMake:从入门到工程化构建
摘要:在 Linux 下开发 C++ 程序时,手写 Makefile 往往面临跨平台适配难、依赖管理混乱、大型项目维护成本高的问题。而 CMake 作为跨平台构建系统生成器,能通过简洁的
CMakeLists.txt自动生成适配不同平台/生成器的构建文件(Makefile、Ninja、VS 工程等),并提供目标化(target-based)的依赖、编译选项、安装导出等工程能力。本文从核心价值、基础语法、进阶工程化、依赖管理、测试安装、常见坑与命令速查出发,形成一份CMake 使用笔记。
一、为什么 Linux C++ 开发必须学 CMake?
Linux 下 C++ 开发绕不开"构建"环节,但传统方式存在痛点:
- 手写 Makefile 复杂:语法晦涩;多文件/多目录项目依赖关系复杂,维护成本随规模上升;
- 跨平台兼容差:为 Linux 写的 Makefile 很难无痛迁移到 macOS/Windows;
- 依赖管理难:链接第三方库(Boost/OpenCV/fmt...)需手动指定 include/lib 路径,容易出错;
- 配置混乱:Debug/Release、编译选项、宏定义等切换易遗漏;
- 大型工程不可控:缺少标准化构建流程、安装导出、测试集成等能力。
CMake 的核心价值:一次写规则,多处生成构建系统 。只需维护 CMakeLists.txt,在不同平台上用不同生成器(Make/Ninja/VS)构建,同时通过 target 机制把"include/define/link/options"绑定到目标,构建规则更可维护、更可复用。
二、核心概念与环境准备
2.1 核心概念
| 概念 | 含义 | 作用 |
|---|---|---|
CMakeLists.txt |
CMake 配置入口 | 定义项目、目标、依赖、选项、安装等 |
| 构建目录(Build Dir) | 编译产物目录 | 推荐源码外构建,保持源码干净 |
| 生成器(Generator) | 生成构建系统类型 | Unix Makefiles、Ninja、VS |
| 目标(Target) | 可执行/库的抽象 | 以 target 为中心配置 include/link/options |
| 配置阶段 vs 构建阶段 | configure 生成规则;build 执行编译 | cmake -S/-B 配置;cmake --build 构建 |
| Cache(缓存) | build 目录里的配置缓存 | 改变量/选项常需要重新配置或清缓存 |
find_package |
查找已安装依赖 | 优先使用"目标导入"(Imported Targets) |
install/export |
安装与导出包 | 让别人能 find_package(YourLib) |
2.2 安装
bash
# Debian / Ubuntu
sudo apt-get install cmake cmake-data
# CentOS / RHEL
sudo yum install cmake
cmake --version # 推荐 3.10+,更建议 3.16+ / 3.20+(现代工程更顺滑)
推荐安装 Ninja(更快、更适合增量):
bash
sudo apt-get install ninja-build
三、基础实操:CMake 入门
3.1 最简示例:单文件程序
main.cpp:
cpp
#include <iostream>
int main() {
std::cout << "Hello CMake!" << std::endl;
return 0;
}
CMakeLists.txt:
cmake
cmake_minimum_required(VERSION 3.10)
project(HelloCMake LANGUAGES CXX)
add_executable(hello_cmake main.cpp)
# 推荐:用 target 方式声明标准
target_compile_features(hello_cmake PRIVATE cxx_std_17)
构建:
bash
cmake -S . -B build # 配置阶段:指定源码目录为当前目录(.),构建目录为 build/;在 build/ 里生成构建系统文件(如 Makefile/Ninja)和 CMake 缓存
cmake --build build -j4 # 构建阶段:在 build/ 目录中调用底层构建工具进行编译,-j4 表示使用 4 个并行线程加速编译
./build/hello_cmake
四、基础语法(围绕 target 写配置)
CMake 最重要的习惯:不要把选项写成全局,尽量把配置绑定到 target。
4.1 变量与文件列表
cmake
set(SOURCES main.cpp utils.cpp)
add_executable(app ${SOURCES})
4.2 编译选项(只作用于某个目标)
cmake
target_compile_options(app PRIVATE -Wall -Wextra -Werror)
4.3 宏定义(-D)
cmake
target_compile_definitions(app PRIVATE ENABLE_LOG=1)
4.4 头文件目录
cmake
target_include_directories(app PRIVATE ${CMAKE_SOURCE_DIR}/include)
4.5 生成静态库/动态库并链接
cmake
add_library(utils STATIC utils.cpp) # libutils.a
# add_library(utils SHARED utils.cpp) # libutils.so
target_include_directories(utils PUBLIC ${CMAKE_SOURCE_DIR}/include)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE utils)
4.6 PUBLIC / PRIVATE / INTERFACE(必须掌握)
| 关键字 | 对自己生效 | 对依赖自己的目标生效 | 常用场景 |
|---|---|---|---|
| PRIVATE | ✅ | ❌ | 仅当前 target 使用 |
| PUBLIC | ✅ | ✅ | 既自己用也要"传递"给下游 |
| INTERFACE | ❌ | ✅ | 自己不编译(头文件库/导入目标) |
五、工程化:多目录模块化
典型结构:
my_project/
├── CMakeLists.txt
├── include/
│ └── utils.h
├── src/
│ └── main.cpp
└── utils/
├── CMakeLists.txt
├── utils.cpp
└── utils.h
utils/CMakeLists.txt:
cmake
add_library(utils STATIC utils.cpp)
target_include_directories(utils
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/include
)
target_compile_features(utils PUBLIC cxx_std_17)
根目录 CMakeLists.txt:
cmake
cmake_minimum_required(VERSION 3.10)
project(MyProject LANGUAGES CXX)
add_subdirectory(utils)
add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE utils)
六、第三方依赖管理
6.1 优先:find_package(目标导入)
示例:Boost filesystem
cmake
find_package(Boost REQUIRED COMPONENTS filesystem)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE Boost::filesystem)
优先写
Boost::filesystem这种目标,而不是手动用 include/lib 变量。
6.2 Linux 常见:PkgConfig(例如 glib、libcurl 等)
cmake
find_package(PkgConfig REQUIRED)
pkg_check_modules(CURL REQUIRED libcurl)
add_executable(app main.cpp)
target_include_directories(app PRIVATE ${CURL_INCLUDE_DIRS})
target_link_directories(app PRIVATE ${CURL_LIBRARY_DIRS})
target_link_libraries(app PRIVATE ${CURL_LIBRARIES})
若模块支持导入目标(有的环境会提供
PkgConfig::xxx),优先用导入目标;否则用变量也可以。
6.3 不想手装:FetchContent 拉源码构建(fmt、gtest 等)
cmake
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
)
FetchContent_MakeAvailable(fmt)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE fmt::fmt)
FetchContent 适合"开发期依赖",生产环境也可配合锁版本、离线镜像。
七、构建类型(Debug/Release)与多配置生成器差异
7.1 单配置生成器(Make/Ninja)
CMAKE_BUILD_TYPE 生效:
bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
7.2 多配置生成器(VS/Xcode)
CMAKE_BUILD_TYPE 通常不管用,需要:
bash
cmake --build build --config Release -j
八、CMake Presets:把"标准化构建命令"固化下来
在项目根目录新增 CMakePresets.json(示例):
json
{
"version": 3,
"cmakeMinimumRequired": { "major": 3, "minor": 19, "patch": 0 },
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{ "name": "debug", "configurePreset": "debug" },
{ "name": "release", "configurePreset": "release" }
]
}
使用:
bash
cmake --preset debug
cmake --build --preset debug -j
好处:团队统一命令、统一 build 目录结构、统一开关(比如导出 compile_commands)。
九、测试:CTest +(可选)GoogleTest
9.1 最小 CTest
cmake
enable_testing()
add_executable(test_math test/test_math.cpp)
add_test(NAME test_math COMMAND test_math)
运行:
bash
ctest --test-dir build -j
9.2 FetchContent 引入 GoogleTest(常用模板)
cmake
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.14.0
)
FetchContent_MakeAvailable(googletest)
add_executable(unit_tests test/test_main.cpp)
target_link_libraries(unit_tests PRIVATE GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(unit_tests)
十、安装与发布:install + export,让别人能 find_package(你的库)
如果你写的是"库项目",建议做到:
make install/cmake --install能安装- 下游能
find_package(MyLib CONFIG REQUIRED)直接用MyLib::mylib
10.1 库项目示例(简化版)
cmake
cmake_minimum_required(VERSION 3.15)
project(MyUtils LANGUAGES CXX)
add_library(my_utils STATIC src/utils.cpp)
target_include_directories(my_utils
PUBLIC
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_features(my_utils PUBLIC cxx_std_17)
include(GNUInstallDirs)
install(TARGETS my_utils
EXPORT MyUtilsTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT MyUtilsTargets
NAMESPACE MyUtils::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyUtils
)
安装:
bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
sudo cmake --install build --prefix /usr/local
进阶:再加
MyUtilsConfig.cmake/write_basic_package_version_file可实现更完整的包配置。
十一、Linux 常见大坑:运行时找不到动态库(RPATH / LD_LIBRARY_PATH)
现象:编译链接成功,运行时报:
error while loading shared libraries: libxxx.so: cannot open shared object file
排查:
bash
ldd ./build/app
解决思路(按推荐顺序):
- 安装到系统路径 (/usr/lib、/usr/local/lib)并执行
ldconfig(需要 root) - 临时:
export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH - 工程化:设置 RPATH(让可执行文件记住运行时库路径)
常用 RPATH(示意):
cmake
set(CMAKE_BUILD_RPATH "$ORIGIN")
set(CMAKE_INSTALL_RPATH "$ORIGIN")
$ORIGIN表示可执行文件所在目录;实际项目可能需要$ORIGIN/../lib等。
十二、性能优化:Ninja / ccache / 并行构建
- Ninja:增量更快,推荐默认使用;
- ccache:重复编译显著加速;
cmake
# CMake 3.4+ 可用
set(CMAKE_CXX_COMPILER_LAUNCHER ccache)
- 并行:
bash
cmake --build build -j"$(nproc)"
十三、实操案例
案例1:单文件快速工具
cmake
cmake_minimum_required(VERSION 3.10)
project(SimpleTool LANGUAGES CXX)
add_executable(simple_tool main.cpp)
target_compile_features(simple_tool PRIVATE cxx_std_17)
target_compile_options(simple_tool PRIVATE -Wall -Wextra -O0 -g)
案例2:多目录 + Boost
cmake
cmake_minimum_required(VERSION 3.10)
project(FileTool LANGUAGES CXX)
find_package(Boost REQUIRED COMPONENTS filesystem)
add_subdirectory(src/utils)
add_executable(file_tool src/main.cpp)
target_link_libraries(file_tool PRIVATE utils Boost::filesystem)
target_include_directories(file_tool PRIVATE ${CMAKE_SOURCE_DIR}/include)
案例3:库 + 安装导出(见第十章模板)
适合"公共工具库/SDK"。
十四、标准化构建流程
推荐目录:
project/
├── CMakeLists.txt
├── CMakePresets.json # 推荐
├── build/ # .gitignore
├── include/
├── src/
├── test/ # 可选
├── scripts/ # 可选
├── toolchain/ # 可选
└── libs/ # 可选(预编译三方库)
推荐命令(统一 -S/-B):
bash
# 配置
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
# 构建
cmake --build build -j"$(nproc)"
# 测试
ctest --test-dir build -j
# 安装(如果有 install)
sudo cmake --install build --prefix /usr/local
十五、CMake 常用命令速查表
15.1 配置(Configure)
| 目的 | 命令 |
|---|---|
| 基本配置 | cmake -S . -B build |
| 指定生成器 | cmake -S . -B build -G Ninja |
| 指定构建类型 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Release |
| 导出 compile_commands | cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON |
| 查看缓存变量 | cmake -S . -B build -LAH |
15.2 构建(Build)
| 目的 | 命令 |
|---|---|
| 编译 | cmake --build build -j4 |
| 指定目标 | cmake --build build --target my_target -j |
| 列出可构建目标 | cmake --build build --target help |
| 多配置生成器指定配置 | cmake --build build --config Release -j |
15.3 清理(Clean)
| 目的 | 命令 |
|---|---|
| 清理目标 | cmake --build build --target clean |
| 彻底清缓存(最稳) | rm -rf build |
15.4 安装(Install)
| 目的 | 命令 |
|---|---|
| 安装到默认前缀 | sudo cmake --install build |
| 指定安装前缀 | sudo cmake --install build --prefix /usr/local |
15.5 测试(CTest)
| 目的 | 命令 |
|---|---|
| 运行测试 | ctest --test-dir build |
| 并行测试 | ctest --test-dir build -j |
| 失败时输出详细 | ctest --test-dir build --output-on-failure |
15.6 Presets
| 目的 | 命令 |
|---|---|
| 配置 preset | cmake --preset debug |
| 构建 preset | cmake --build --preset debug -j |
十六、避坑指南
16.1 常见错误与解决方案
| 现象 | 原因 | 解决 |
|---|---|---|
xxx.h: No such file |
未加 include 目录 | target_include_directories |
undefined reference |
未链接库/顺序问题 | target_link_libraries,优先导入目标 |
| 找不到编译器 | 未装 g++/clang | 安装编译器 |
Could NOT find Boost |
缺组件 | 安装对应 dev 包 |
| 改了变量不生效 | build cache 仍旧 | 重新 cmake -S/-B 或删 build |
16.2 Debug 体验差(被误开优化)
Debug 推荐:
-O0 -g- Release 推荐:
-O3 -DNDEBUG
建议:不要自己手写 -O3,尽量通过 CMAKE_BUILD_TYPE 管理。
十七、总结
CMake核心价值在于:
-
简化构建配置:用简洁的语法替代晦涩的Makefile,降低维护成本;
-
标准化工程管理:支持模块化、依赖管理、多编译模式,适配大型项目;
-
跨平台兼容:一次配置可在Linux/macOS/Windows生成适配的构建文件;
-
高效可扩展:支持自定义规则、脚本集成,满足个性化构建需求。