
🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:

文章目录
- 前言
- [一. CTest:CMake 原生自动化测试框架](#一. CTest:CMake 原生自动化测试框架)
-
- [1.1 CTest 核心功能](#1.1 CTest 核心功能)
- [1.2 实战:为 MyMath 数学库添加单元测试](#1.2 实战:为 MyMath 数学库添加单元测试)
-
- [1.2.1 编写测试代码](#1.2.1 编写测试代码)
- [1.2.2 CMakeLists 配置详解](#1.2.2 CMakeLists 配置详解)
- [1.2.3 运行测试](#1.2.3 运行测试)
- [1.3 核心命令总结](#1.3 核心命令总结)
- [二. CPack:一键生成跨平台安装包](#二. CPack:一键生成跨平台安装包)
-
- [2.1 CPack 核心能力](#2.1 CPack 核心能力)
- [2.2 实战:打包 MyMath 动态库与可执行文件](#2.2 实战:打包 MyMath 动态库与可执行文件)
-
- [2.2.1 基础配置与安装规则](#2.2.1 基础配置与安装规则)
- [2.2.2 CPack 打包配置](#2.2.2 CPack 打包配置)
- [2.2.3 生成与测试安装包](#2.2.3 生成与测试安装包)
- [2.3 关键原理:ORIGIN 与动态库运行时查找](#2.3 关键原理:ORIGIN 与动态库运行时查找)
- [2.4 核心命令总结](#2.4 核心命令总结)
- [三. CMake 调用外部命令:以 Protobuf 代码生成为例](#三. CMake 调用外部命令:以 Protobuf 代码生成为例)
-
- [3.1 为什么需要调用外部命令](#3.1 为什么需要调用外部命令)
- [3.2 核心 API:add_custom_command 与 add_custom_target](#3.2 核心 API:add_custom_command 与 add_custom_target)
- [3.3 实战:集成 Protobuf 编译器](#3.3 实战:集成 Protobuf 编译器)
-
- [3.3.1 环境准备与 proto 文件编写](#3.3.1 环境准备与 proto 文件编写)
- [3.3.2 CMakeLists 完整配置与逐行解读](#3.3.2 CMakeLists 完整配置与逐行解读)
- [3.3.3 编译运行验证](#3.3.3 编译运行验证)
- [3.4 外部命令调用最佳实践](#3.4 外部命令调用最佳实践)
- 四、总结与核心考点提炼
- 结尾:
前言
在 C/C++ 开发中,CMake 早已成为跨平台构建的事实标准,但多数开发者仅掌握了基础的编译链接流程,却忽略了 CMake 生态中三个决定项目工程化水平的核心模块:CTest(自动化测试框架) 、CPack(跨平台打包工具)和外部命令集成能力。这三个工具构成了 CMake 完整的 "构建 - 测试 - 交付" 闭环:CTest 让单元测试与构建流程无缝集成,CPack 实现一键生成多格式安装包,外部命令集成则能将代码生成、格式检查等自动化任务纳入构建体系。本文将结合实战案例,从原理到源码逐行拆解,帮你掌握这三个核心技能,打造专业级 CMake 工程。
一. CTest:CMake 原生自动化测试框架
1.1 CTest 核心功能
CTest 是 CMake 内置的测试驱动工具,无需额外安装依赖,支持单元测试、集成测试、性能测试等多种测试类型,能够自动执行测试用例并生成详细的测试报告。其核心优势在于与 CMake 构建系统深度集成,测试用例的编译、运行与项目构建流程完全统一。
1.2 实战:为 MyMath 数学库添加单元测试
我们以一个简单的数学库(包含加法和减法函数)为例,演示如何使用 CTest 进行自动化测试。
1.2.1 编写测试代码
创建test_math.cpp测试文件,使用 C 标准库的assert宏验证函数正确性:
cpp
#include <iostream>
#include "math/math.h"
#include <cassert>
int main() {
// 测试加法函数
assert(add(2, 3) == 5);
std::cout << "加法测试通过" << std::endl;
// 测试减法函数
assert(sub(2, 3) == -1);
std::cout << "减法测试通过" << std::endl;
std::cout << "所有测试通过!" << std::endl;
return 0;
}
1.2.2 CMakeLists 配置详解
在测试目录下创建CMakeLists.txt,完成 CTest 的集成配置:
cmake
cmake_minimum_required(VERSION 3.18)
project(TestMyMath LANGUAGES CXX)
# 1. 开启CTest测试功能(必须在add_test之前调用)
include(CTest)
# 2. 添加测试可执行文件
add_executable(test_math test_math.cpp)
# 3. 查找并链接被测的MyMath库(假设已通过install安装到系统)
find_package(MyMath CONFIG REQUIRED)
target_link_libraries(test_math PRIVATE MyMath::MyMath)
# 4. 将测试用例注册到CTest
add_test(
NAME MathLibraryTest # 测试用例名称(CTest中显示的标识)
COMMAND test_math # 要执行的测试命令
)
关键配置解读:
include(CTest):加载 CTest 模块,启用测试功能,必须放在所有add_test命令之前find_package(MyMath CONFIG REQUIRED):以 Config 模式查找已安装的 MyMath 库add_test():将可执行文件注册为 CTest 测试用例,CTest 会自动执行该程序并根据返回值判断测试是否通过(返回 0 为通过,非 0 为失败)
1.2.3 运行测试
执行标准的 CMake 构建流程后,即可通过 CTest 运行测试:
bash
# 创建构建目录并进入
mkdir build && cd build
# 配置项目
cmake ..
# 编译测试程序
make
# 运行所有测试用例(两种方式等价)
ctest
# 或者
make test
运行结果示例:
Plain
Test project /home/bit/workspace/CMakeClass/test_ctest/build
Start 1: MathLibraryTest
1/1 Test #1: MathLibraryTest .................. Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.00 sec
1.3 核心命令总结
| 命令 | 作用 |
|---|---|
include(CTest) |
加载 CTest 模块,启用测试功能 |
add_test(NAME <name> COMMAND <cmd>) |
注册测试用例到 CTest |
ctest |
执行所有已注册的测试用例 |
make test |
make 方式执行测试(等价于 ctest) |
二. CPack:一键生成跨平台安装包
2.1 CPack 核心能力
CPack 是 CMake 内置的打包工具,能够基于项目的install()规则,自动收集可执行文件、库文件、头文件和文档,生成符合平台规范的安装包。支持的格式包括:
- Linux:DEB、RPM、TGZ、STGZ
- Windows:ZIP、MSI
- macOS:DMG、TGZ
其核心优势在于无需编写复杂的打包脚本,只需配置少量参数即可实现跨平台打包。
2.2 实战:打包 MyMath 动态库与可执行文件
我们继续使用 MyMath 动态库项目,演示如何使用 CPack 生成包含可执行文件和动态库的压缩包。
2.2.1 基础配置与安装规则
首先在顶层CMakeLists.txt中配置安装规则,这是 CPack 打包的基础(CPack 会自动收集所有install()指令指定的文件):
cmake
cmake_minimum_required(VERSION 3.18)
project(TestMyMath LANGUAGES CXX)
# 添加子目录
add_subdirectory(my_lib) # 构建MyMath动态库
add_subdirectory(app) # 构建使用MyMath的可执行文件
# 开启CPack打包功能
include(CPack)
my_lib/CMakeLists.txt(动态库配置与安装):
cmake
# 收集源文件
file(GLOB SRC_LISTS "src/*.cpp")
# 添加动态库目标
add_library(MyMath SHARED ${SRC_LISTS})
# 设置头文件包含路径(构建时和安装时)
target_include_directories(MyMath PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<INSTALL_INTERFACE:include>"
)
# 设置库属性:输出路径、版本号、位置无关代码
set_target_properties(MyMath PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
OUTPUT_NAME MyMath
VERSION 1.2.3
SOVERSION 20
COMPILE_OPTIONS "-fPIC"
)
# 1. 安装动态库文件
install(TARGETS MyMath
LIBRARY DESTINATION lib # 动态库安装到lib目录
)
# 2. 安装头文件
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
DESTINATION include
FILES_MATCHING PATTERN "*.h"
)
app/CMakeLists.txt(可执行文件配置与安装):
cmake
# 收集源文件
file(GLOB SRC_LISTS "*.cpp")
# 添加可执行文件
add_executable(main ${SRC_LISTS})
# 链接MyMath动态库
target_link_libraries(main PRIVATE MyMath)
# 设置可执行文件属性:输出路径、安装时RPATH
set_target_properties(main PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
INSTALL_RPATH "$ORIGIN/../lib" # 关键:运行时动态库查找路径
)
# 安装可执行文件
install(TARGETS main
RUNTIME DESTINATION bin # 可执行文件安装到bin目录
)
2.2.2 CPack 打包配置
在顶层CMakeLists.txt中添加 CPack 的元数据配置:
cmake
# 开启CPack打包功能
include(CPack)
# 设置包名称
set(CPACK_PACKAGE_NAME "MyMathApp")
# 设置包版本号
set(CPACK_PACKAGE_VERSION "1.0.0")
# 设置生成的包格式(TGZ为tar.gz压缩包)
set(CPACK_GENERATOR "TGZ")
2.2.3 生成与测试安装包
执行构建和打包命令:
bash
cd build
cmake ..
make
cpack # 生成安装包
CPack 会在 build 目录下生成MyMathApp-1.0.0-Linux.tar.gz压缩包,解压后目录结构如下:
Plain
MyMathApp-1.0.0-Linux/
├── bin/
│ └── main # 可执行文件
└── lib/
├── libMyMath.so # 动态库
├── libMyMath.so.20
└── libMyMath.so.1.2.3
测试解压后的程序:
bash
cd MyMathApp-1.0.0-Linux/bin
./main
输出结果:
Plain
3 + 4 = 7
3 - 4 = -1
2.3 关键原理:$ORIGIN 与动态库运行时查找
CPack 打包后,可执行文件能够正确找到同包内的动态库,核心在于我们设置了INSTALL_RPATH "$ORIGIN/../lib"。
O R I G I N 是什么? ORIGIN是什么? ORIGIN是什么?ORIGIN是Linux动态链接器ld的特殊变量,表示当前可执行文件所在的目录。当我们设置INSTALL_RPATH "$ORIGIN/.../lib"时,CMake会将这个路径写入可执行文件的RUNPATH` 段中。
程序运行时,动态链接器会按照以下顺序查找动态库:
- 环境变量
LD_LIBRARY_PATH指定的路径 - 可执行文件
RUNPATH段中指定的路径 - 系统默认路径(
/lib、/usr/lib等)
在我们的例子中,$ORIGIN会被动态链接器替换为MyMathApp-1.0.0-Linux/bin/,因此$ORIGIN/../lib就指向了MyMathApp-1.0.0-Linux/lib/,从而正确找到同包内的动态库。
验证 RUNPATH 设置 : 使用readelf命令查看可执行文件的动态段信息:
bash
readelf -d bin/main | grep PATH
输出结果:
Plain
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]
2.4 核心命令总结
| 命令 / 变量 | 作用 |
|---|---|
include(CPack) |
加载 CPack 模块,启用打包功能 |
install(TARGETS ...) |
定义安装规则(CPack 的基础) |
CPACK_PACKAGE_NAME |
设置安装包名称 |
CPACK_PACKAGE_VERSION |
设置安装包版本号 |
CPACK_GENERATOR |
设置生成的包格式 |
INSTALL_RPATH |
设置安装后可执行文件的运行时库查找路径 |
cpack |
执行打包命令 |
make package |
make 方式执行打包(等价于 cpack) |
三. CMake 调用外部命令:以 Protobuf 代码生成为例
3.1 为什么需要调用外部命令
在实际项目中,我们经常需要在构建过程中执行外部工具,例如:
- 使用 Protobuf 编译器
protoc从.proto文件生成 C++ 代码 - 使用 Doxygen 生成文档
- 使用 Clang-Format 格式化代码
- 执行自定义的代码生成脚本
CMake 提供了add_custom_command和add_custom_target两个核心命令,用于将这些外部任务集成到构建流程中。
3.2 核心 API:add_custom_command 与 add_custom_target
add_custom_command
用于定义一个自定义命令,生成指定的输出文件。当其他目标依赖这些输出文件时,CMake 会自动执行该命令。
基本语法:
cmake
add_custom_command(
OUTPUT output1 output2 ... # 命令生成的文件
COMMAND command1 args1 # 要执行的命令
COMMAND command2 args2 # 可以执行多个命令
DEPENDS depend1 depend2 ... # 命令依赖的文件(依赖文件变化时重新执行)
COMMENT "注释信息" # 构建时显示的提示信息
VERBATIM # 确保命令参数正确转义(推荐总是添加)
)
add_custom_target
用于定义一个自定义目标,该目标不生成实际的文件,仅用于执行一系列命令。可以将其作为其他目标的依赖,确保命令在构建时执行。
基本语法:
cmake
add_custom_target(
target_name # 自定义目标名称
DEPENDS depend1 depend2 ... # 依赖的文件或目标
)
3.3 实战:集成 Protobuf 编译器
Protobuf 是 Google 开发的序列化框架,需要使用protoc编译器将.proto接口定义文件编译为 C++ 代码。我们将演示如何将这个过程集成到 CMake 构建流程中。
3.3.1 环境准备与 proto 文件编写
首先安装 Protobuf 库和编译器:
bash
sudo apt update
sudo apt install libprotobuf-dev protobuf-compiler
创建proto/person.proto文件,定义 Person 消息结构:
protobuf
syntax = "proto3";
package example;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
3.3.2 CMakeLists 完整配置与逐行解读
cmake
cmake_minimum_required(VERSION 3.18)
project(ProtocExample LANGUAGES CXX)
# 设置C++标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 1. 查找Protobuf库(要求版本>=3.0)
find_package(Protobuf 3.0 REQUIRED)
# 2. 收集所有proto文件
file(GLOB PROTO_FILES ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto)
# 3. 创建生成代码的输出目录
set(PB_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/pb)
file(MAKE_DIRECTORY ${PB_OUT_DIR})
# 4. 初始化生成的源代码文件集合
set(GEN_SRCS "") # 存储生成的.cc文件
set(GEN_HEADS "") # 存储生成的.h文件
# 5. 为每个proto文件生成对应的C++代码
foreach(PROTO ${PROTO_FILES})
# 5.1 获取proto文件的文件名(不带扩展名)
get_filename_component(BASE_NAME ${PROTO} NAME_WE)
# 5.2 将生成的文件添加到源代码集合
list(APPEND GEN_SRCS "${PB_OUT_DIR}/${BASE_NAME}.pb.cc")
list(APPEND GEN_HEADS "${PB_OUT_DIR}/${BASE_NAME}.pb.h")
# 5.3 定义自定义命令,生成C++代码
add_custom_command(
# 输出文件:protoc生成的.cc和.h文件
OUTPUT "${PB_OUT_DIR}/${BASE_NAME}.pb.cc"
"${PB_OUT_DIR}/${BASE_NAME}.pb.h"
# 执行protoc命令
COMMAND protoc
ARGS --cpp_out=${PB_OUT_DIR} # C++代码输出目录
-I ${CMAKE_CURRENT_SOURCE_DIR}/proto # proto文件搜索路径
${PROTO} # 要编译的proto文件
# 依赖:当proto文件变化时,重新执行命令
DEPENDS ${PROTO}
# 构建时显示的提示信息
COMMENT "从${BASE_NAME}.proto生成C++代码"
# 确保参数正确转义
VERBATIM
)
endforeach()
# 6. 添加自定义目标,触发生成所有proto代码
add_custom_target(generate_protobuf DEPENDS ${GEN_SRCS} ${GEN_HEADS})
# 7. 将生成的代码编译成静态库
add_library(MyProto STATIC ${GEN_SRCS})
# 添加依赖:确保编译MyProto之前先执行generate_protobuf生成代码
add_dependencies(MyProto generate_protobuf)
# 设置静态库的头文件包含路径(供使用者使用)
target_include_directories(MyProto INTERFACE ${PB_OUT_DIR})
# 链接Protobuf库
target_link_libraries(MyProto PUBLIC protobuf::libprotobuf)
# 8. 添加主程序可执行文件
add_executable(main main.cpp)
# 链接我们生成的MyProto静态库
target_link_libraries(main PRIVATE MyProto)
关键配置解读:
find_package(Protobuf 3.0 REQUIRED):查找系统中安装的 Protobuf 库file(GLOB PROTO_FILES ...):自动收集所有 proto 文件foreach(PROTO ${PROTO_FILES}):遍历每个 proto 文件,为每个文件生成对应的编译命令add_custom_command():定义 protoc 编译命令,当 proto 文件变化时自动重新生成代码add_custom_target(generate_protobuf):创建一个总目标,依赖所有生成的代码文件add_dependencies(MyProto generate_protobuf):确保编译静态库之前,代码已经生成完毕
3.3.3 编译运行验证
创建main.cpp文件,测试 Protobuf 的序列化和反序列化功能:
cpp
#include <iostream>
#include <string>
#include "person.pb.h"
int main() {
// 1. 创建Person对象并设置字段
example::Person person;
person.set_name("Bit");
person.set_id(10086);
person.set_email("Bit@example.com");
// 2. 序列化到字符串
std::string serialized_data;
person.SerializeToString(&serialized_data);
std::cout << "序列化后大小: " << serialized_data.size() << " 字节" << std::endl;
// 3. 反序列化
example::Person parsed_person;
parsed_person.ParseFromString(serialized_data);
// 4. 输出解析结果
std::cout << "\n解析结果:" << std::endl;
std::cout << parsed_person.DebugString() << std::endl;
return 0;
}
执行构建和运行:
bash
mkdir build && cd build
cmake ..
make
./main
输出结果:
Plain
序列化后大小: 25 字节
解析结果:
name: "Bit"
id: 10086
email: "Bit@example.com"
3.4 外部命令调用最佳实践
- 总是使用 VERBATIM 参数:确保命令参数在不同平台下正确转义,避免空格和特殊字符导致的问题
- 明确依赖关系 :通过
DEPENDS指定命令依赖的文件,确保依赖变化时命令自动重新执行 - 使用自定义目标管理多个命令:当有多个相关的自定义命令时,创建一个总目标来统一管理
- 将生成的代码编译成库:不要直接将生成的代码添加到主程序的源文件列表中,而是编译成独立的库,提高构建效率
四、总结与核心考点提炼
1. CTest 自动化测试
- 核心流程:
include(CTest)开启测试 → 编译测试可执行文件 →add_test()注册测试用例 →ctest运行测试 - 测试通过标准:测试程序返回值为 0,非 0 则视为失败
- 优势:与 CMake 构建系统深度集成,无需额外工具
2. CPack 跨平台打包
- 基础:CPack 基于
install()规则自动收集文件,因此必须先正确配置安装规则 - 关键:
INSTALL_RPATH "$ORIGIN/../lib"解决打包后动态库查找问题 - 常用格式:TGZ(跨平台压缩包)、DEB/RPM(Linux 安装包)、MSI(Windows 安装包)
3. 外部命令集成
- 核心命令:
add_custom_command()定义生成文件的命令,add_custom_target()定义执行命令的目标 - 依赖管理:通过
DEPENDS和add_dependencies()确保构建顺序正确 - 典型场景:Protobuf 代码生成、文档生成、代码格式化等自动化任务
结尾:
html
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:掌握这三个模块,你就能将 C/C++ 项目的构建、测试、交付流程完全自动化,大幅提升开发效率和项目质量。在实际工程中,这些工具还可以与 CI/CD 系统结合,实现代码提交后自动构建、测试和打包,打造完整的 DevOps 流水线。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
