CMake 实战练习
本文将演示如何使用 CMake 管理一个中等复杂度的 C++ 项目,从创建项目到编译和运行的完整流程。
内容涵盖基本配置、库链接、单元测试、自定义命令以及跨平台交叉编译等高级特性。
CMake 构建流程
下图展示了 CMake 从源码到最终可执行文件的完整构建流程:
项目概述
我们将构建一个包含主程序、静态库和单元测试的 C++ 项目。
库中提供数学运算功能(加法、减法、乘法),主程序调用这些功能,测试代码验证其正确性。
项目目录结构
MyProject/
├── CMakeLists.txt # 根构建配置(项目定义、全局设置)
├── src/
│ ├── main.cpp # 主程序入口
│ ├── CMakeLists.txt # src 子目录构建配置(库 + 可执行文件)
│ ├── lib/
│ │ ├── module1.cpp # 数学模块一:加法、减法
│ │ └── module2.cpp # 数学模块二:乘法
│ └── include/
│ └── mylib.h # 公共头文件(函数声明)
└── tests/
├── test_main.cpp # 单元测试用例
└── CMakeLists.txt # 测试子目录构建配置
Out-of-Source Build(源外构建)是 CMake 的最佳实践:所有构建产物(Makefile、目标文件、可执行文件)都放在独立的 build 目录中,不会污染源码目录。
创建 CMakeLists.txt 文件
CMakeLists.txt 是 CMake 的核心配置文件,采用层级结构,根目录和每个子目录各有一个。
根目录 CMakeLists.txt
根配置定义项目级信息,并通过 add_subdirectory 管理子目录。
实例
project(MyProject VERSION 1.0) # 定义项目名称和版本号
# 设置 C++ 标准为 C++11,并设为强制要求
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加全局头文件搜索路径(PROJECT_SOURCE_DIR 为项目根目录)
include_directories(${PROJECT_SOURCE_DIR}/src/include)
# 添加子目录,CMake 将依次处理其中的 CMakeLists.txt
add_subdirectory(src) # 处理库和可执行文件
add_subdirectory(tests) # 处理测试
src 目录 CMakeLists.txt
此文件定义库目标(MyLib)和可执行文件目标(MyExecutable),并建立依赖关系。
实例
add_library(MyLib STATIC
lib/module1.cpp
lib/module2.cpp
)
# 为 MyLib 指定公开头文件路径(PUBLIC 表示使用者也会继承此路径)
target_include_directories(MyLib PUBLIC ${CMAKE_SOURCE_DIR}/src/include)
# 创建可执行文件目标 MyExecutable
add_executable(MyExecutable main.cpp)
# 将 MyLib 链接到可执行文件(PRIVATE 表示仅 MyExecutable 需要)
target_link_libraries(MyExecutable PRIVATE MyLib)
PRIVATE 和 PUBLIC 的区别:PRIVATE 表示依赖仅当前目标需要,PUBLIC 表示依赖会传递给链接了当前目标的其他目标。
tests 目录 CMakeLists.txt
测试配置使用 Google Test 框架,需要先查找并引入 GTest 包。
实例
# 在 MyExecutable 构建完成后打印提示信息
add_custom_command(
TARGET MyExecutable
POST_BUILD # 构建完成后执行
COMMAND ${CMAKE_COMMAND} -E echo "MyExecutable 构建完成!"
COMMENT "打印构建完成信息" # 构建日志中显示的说明
)
自定义目标(add_custom_target)
自定义目标是一个独立的目标,不产生输出文件,用于执行特定任务。
实例
# 创建 run 目标,执行编译后的程序
add_custom_target(run
COMMAND ${CMAKE_BINARY_DIR}/src/MyExecutable
DEPENDS MyExecutable # 依赖 MyExecutable,确保先编译
COMMENT "运行 MyExecutable"
)
运行自定义目标:
# 在 build 目录下执行 make run # 或 cmake --build . --target run
自定义命令和自定义目标的区别:add_custom_command 附着在某个目标上,在目标构建的特定阶段触发;add_custom_target 是一个独立的目标,需要显式指定才会执行。
跨平台和交叉编译
CMake 的核心优势之一就是跨平台支持,可以轻松切换目标平台和架构。
指定目标平台
通过 CMAKE_SYSTEM_NAME 变量可以指定目标平台。
# 为 Linux 平台交叉编译 cmake -DCMAKE_SYSTEM_NAME=Linux .. # 为 Windows 平台交叉编译 cmake -DCMAKE_SYSTEM_NAME=Windows ..
使用工具链文件
对于复杂的交叉编译场景,推荐使用工具链文件集中管理所有平台相关配置。
首先创建工具链文件 toolchain.cmake:
实例
# ARM Linux 交叉编译工具链配置
# 指定目标系统
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
# 指定交叉编译器路径(根据实际环境调整)
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
# 指定目标系统的根文件系统路径(可选,用于查找库和头文件)
set(CMAKE_FIND_ROOT_PATH /path/to/arm/sysroot)
# 只在目标 sysroot 中查找程序(不在主机系统中查找)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# 只在目标 sysroot 中查找库文件
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# 只在目标 sysroot 中查找头文件
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
使用工具链文件进行构建:
# 在 build 目录下执行 cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake .. cmake --build .
工具链文件将平台相关的编译器、链接器、库路径等配置集中管理,使得切换目标平台只需指定不同的工具链文件,无需修改 CMakeLists.txt。
构建类型与常用选项
CMake 支持多种构建类型,通过 CMAKE_BUILD_TYPE 变量控制。
| 构建类型 | 编译选项 | 适用场景 |
|---|---|---|
| Debug | -g(调试符号) | 开发调试阶段,需要断点和变量查看 |
| Release | -O2 -DNDEBUG(优化 + 去除断言) | 生产发布,追求运行速度 |
| RelWithDebInfo | -O2 -g(优化 + 调试符号) | 需要性能分析或调试优化的发布版本 |
| MinSizeRel | -Os(体积优化) | 嵌入式或对体积敏感的场景 |
指定构建类型:
# Debug 构建 cmake -DCMAKE_BUILD_TYPE=Debug .. # Release 构建 cmake -DCMAKE_BUILD_TYPE=Release ..
