
CMake 进阶:CTest 自动化测试与 CPack 跨平台打包实战
前面章节我们已经彻底掌握了 CMake 的核心逻辑:如何用 find_package 查找第三方库、如何用 add_library 构建自己的库、如何用 target_link_libraries 管理依赖。但一个成熟的项目,光 "能编译运行" 是不够的,必须经过 "严格测试" 和 "标准化打包发布" 两个环节。
这就是我们要补的最后两块拼图:CTest(测试) 和 CPack(打包) 。本节将结合之前的 Protobuf 实战案例,手把手教你如何在 CMakeLists.txt 中配置测试与打包,特别是针对你自己编写的库(自研库) 打包时的关键注意事项。
CTest:CMake 内置的自动化测试框架
CTest 是 CMake 自带的测试驱动工具,它不是测试框架 ,而是一个测试执行器。它的核心逻辑非常简单:
我们在 CMake 中用 add_test() 注册一个测试(通常是一个可执行程序)。
CTest 运行这个程序,只看程序的返回码(Exit Code):
- 返回
0→ 测试 通过(Passed) - 返回非
0→ 测试 失败(Failed)
下面我们为 Protobuf 项目添加测试,我们沿用之前的 cmake_proto 项目,为它添加测试。
Step 1:新建测试文件 test_main.cpp
cpp
#include <iostream>
#include "person.pb.h"
int main() {
// 1. 构建测试数据
example::Person person;
person.set_name("TestUser");
person.set_id(10086);
person.set_email("test@example.com");
// 2. 序列化
std::string data;
bool serialize_ok = person.SerializeToString(&data);
if (!serialize_ok) {
std::cerr << "Serialization failed!" << std::endl;
return 1; // 测试失败,返回非0
}
// 3. 反序列化
example::Person parsed_person;
bool parse_ok = parsed_person.ParseFromString(data);
if (!parse_ok) {
std::cerr << "Parse failed!" << std::endl;
return 1;
}
// 4. 验证数据一致性
if (parsed_person.name() != "TestUser" || parsed_person.id() != 10086) {
std::cerr << "Data mismatch!" << std::endl;
return 1;
}
std::cout << "All tests passed successfully!" << std::endl;
return 0; // 测试通过,返回0
}
Step 2:修改 CMakeLists.txt 开启测试
只需在原有的 Protobuf 配置基础上,添加 3 行核心代码:
bash
cmake_minimum_required(VERSION 3.18)
project(ProtoExample)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 1. 查找Protobuf
find_package(Protobuf REQUIRED)
include_directories(${Protobuf_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_BINARY_DIR})
set(PROTO_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/proto)
file(MAKE_DIRECTORY ${PROTO_OUT_DIR})
set(GEN_SRCS "")
file(GLOB PROTO_FILES ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto)
foreach(PROTO_FILE ${PROTO_FILES})
get_filename_component(BASE_NAME ${PROTO_FILE} NAME_WE)
set(GEN_CC ${PROTO_OUT_DIR}/${BASE_NAME}.pb.cc)
set(GEN_H ${PROTO_OUT_DIR}/${BASE_NAME}.pb.h)
add_custom_command(
OUTPUT ${GEN_CC} ${GEN_H}
COMMAND protoc --cpp_out=${PROTO_OUT_DIR} -I${CMAKE_CURRENT_SOURCE_DIR}/proto ${PROTO_FILE}
VERBATIM
)
list(APPEND GEN_SRCS ${GEN_CC})
endforeach()
# 2. 生成静态库
add_library(MyProto STATIC ${GEN_SRCS})
target_include_directories(MyProto PUBLIC ${Protobuf_INCLUDE_DIRS})
target_link_libraries(MyProto PUBLIC ${Protobuf_LIBRARIES})
# 3. 生成主程序
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE MyProto)
# ===================== 以下是 CTest 核心配置 =====================
# 4. 开启测试支持(必须写)
enable_testing()
# 5. 生成测试可执行文件
add_executable(test_proto test_main.cpp)
target_link_libraries(test_proto PRIVATE MyProto)
# 6. 注册测试:名字 -> 命令
# 格式:add_test(NAME <测试名> COMMAND <可执行文件>)
add_test(NAME Proto_Serialize_Test COMMAND test_proto)
Step 3:运行测试
bash
mkdir build && cd build
cmake ..
make
# 运行测试
ctest
# 或者用详细模式查看输出
ctest -v
预期输出:
bash
Test project /xxx/cmake_proto/build
Start 1: Proto_Serialize_Test
1/1 Test #1: Proto_Serialize_Test ............. Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
CTest 常用命令【命令章节有详细说明】
| 命令 | 作用 |
|---|---|
ctest |
运行所有测试 |
ctest -v |
详细模式,显示测试输出 |
ctest -R <正则> |
只运行匹配的测试(如 ctest -R Proto) |
ctest --output-on-failure |
只在测试失败时输出详细日志 |
CPack:CMake 内置的跨平台打包工具
CPack 是 CMake 的打包工具,它负责把我们写好的程序和库,打包成跨平台的安装包(如 .deb, .rpm, .exe, .tar.gz, .dmg)。
⚠️ 关键大前提:
CPack 打包的内容 = install() 命令安装的内容! CPack 不会凭空打包,它会扫描你在 CMakeLists.txt 中用 install() 指定的文件,然后把它们打包成安装包。
所以,打包我们的自研库,必须先写好 install() 规则。
下面我们进行实战:为 Protobuf 项目打包(含自研库)
Step 1:完善 CMakeLists.txt 增加安装规则
我们需要指定:
- 安装哪个可执行文件。
- 安装哪个库文件(静态库 / 动态库)。
- 安装哪个头文件(给开发者用)。
- 配置 CPack 基本信息。
bash
cmake_minimum_required(VERSION 3.18)
project(ProtoExample)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Protobuf REQUIRED)
include_directories(${Protobuf_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_BINARY_DIR})
set(PROTO_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/proto)
file(MAKE_DIRECTORY ${PROTO_OUT_DIR})
set(GEN_SRCS "")
file(GLOB PROTO_FILES ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto)
foreach(PROTO_FILE ${PROTO_FILES})
get_filename_component(BASE_NAME ${PROTO_FILE} NAME_WE)
set(GEN_CC ${PROTO_OUT_DIR}/${BASE_NAME}.pb.cc)
set(GEN_H ${PROTO_OUT_DIR}/${BASE_NAME}.pb.h)
add_custom_command(
OUTPUT ${GEN_CC} ${GEN_H}
COMMAND protoc --cpp_out=${PROTO_OUT_DIR} -I${CMAKE_CURRENT_SOURCE_DIR}/proto ${PROTO_FILE}
VERBATIM
)
list(APPEND GEN_SRCS ${GEN_CC})
endforeach()
# 生成静态库
add_library(MyProto STATIC ${GEN_SRCS})
target_include_directories(MyProto PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}> # 构建时路径
$<INSTALL_INTERFACE:include> # 安装时路径 (关键:告诉用户头文件在 include)
)
target_link_libraries(MyProto PUBLIC ${Protobuf_LIBRARIES})
# 生成主程序
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE MyProto)
# ===================== CTest 配置 =====================
enable_testing()
add_executable(test_proto test_main.cpp)
target_link_libraries(test_proto PRIVATE MyProto)
add_test(NAME Proto_Serialize_Test COMMAND test_proto)
# ===================== CPack 核心配置 =====================
# 1. 安装规则 (Install Rules) ------ 这是打包的基础!
# 安装可执行文件到 bin 目录
install(TARGETS myapp
RUNTIME DESTINATION bin # Windows: bin/, Linux: bin/
)
# 2. 安装自研库到 lib 目录
# 注意:静态库用 ARCHIVE,动态库用 LIBRARY
install(TARGETS MyProto
ARCHIVE DESTINATION lib # 静态库 .a/.lib
LIBRARY DESTINATION lib # 动态库 .so/.dylib
INCLUDES DESTINATION include # 导出头文件路径
)
# 3. 安装头文件 (供外部使用 MyProto 的人必须有 .h 文件)
# 我们需要把生成的 .pb.h 文件安装出去
install(FILES ${PROTO_OUT_DIR}/person.pb.h
DESTINATION include/proto # 安装到 include/proto 目录
)
# 4. CPack 配置 (Package Configuration)
include(CPack) # 引入打包模块
# 5. 设置包基本信息 (必须设置,否则包名会是 unknown)
set(CPACK_PACKAGE_NAME "MyProtoExample")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Protobuf CMake Demo Package")
set(CPACK_PACKAGE_VENDOR "MyCompany")
# 6. 设置生成器 (Generators) ------ 决定打包成什么格式
# 推荐:ZIP (通用) + DEB (Ubuntu) + RPM (CentOS) + NSIS (Windows)
if(WIN32)
set(CPACK_GENERATOR "ZIP;NSIS")
elseif(APPLE)
set(CPACK_GENERATOR "ZIP;DragNDrop")
elseif(UNIX)
# Linux 通常同时生成 deb 和 rpm
set(CPACK_GENERATOR "ZIP;DEB;RPM")
endif()
# Linux 特定配置
if(UNIX AND NOT APPLE)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "dev@mycompany.com")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
endif()
运行打包命令
bash
cd build
cmake ..
# 方式一:直接运行 cpack (推荐)
cpack
# 方式二:构建目标 (make package)
make package
打包结果(build 目录下):
MyProtoExample-1.0.0-Linux.tar.gz(通用压缩包)MyProtoExample-1.0.0-Linux.deb(Debian 包)MyProtoExample-1.0.0-Linux.rpm(RPM 包)
自研库打包的 4 个致命注意事项
"自己写的库,字节要用到的程序都要打包,这些注意点体现在哪?"
答案是:全部体现在顶层 CMakeLists.txt 的 install() 和 CPack 配置中。 以下是开发库时必须遵守的 4 条铁律:
注意点 1:路径必须使用 INSTALL_INTERFACE
错误写法:
bash
# 这样写只有你自己编译时能用,别人打包后用你的库会找不到头文件
target_include_directories(MyProto PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
正确写法(使用生成器表达式):
bash
target_include_directories(MyProto PUBLIC
# $<BUILD_INTERFACE> : 只有在构建本项目时有效
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
# $<INSTALL_INTERFACE> : 只有在安装后使用本库时有效 (路径是相对于安装前缀的)
$<INSTALL_INTERFACE:include>
)
解释: 告诉 CMake,当用户安装你的库后,头文件在 include 目录下。
注意点 2:区分 RUNTIME / LIBRARY / ARCHIVE
CMake 对不同类型文件安装路径有严格区分,不能乱填:
| 文件类型 | 关键字 | 示例 | 含义 |
|---|---|---|---|
| 可执行文件 | RUNTIME |
.exe, 无后缀 |
安装到 bin |
| 动态库 | LIBRARY |
.so, .dylib |
安装到 lib |
| 静态库 | ARCHIVE |
.a, .lib |
安装到 lib |
配置示例:
bash
# 安装动态库
install(TARGETS MyProto
LIBRARY DESTINATION lib # Linux: lib/libMyProto.so
ARCHIVE DESTINATION lib # Windows: lib/MyProto.lib
)
注意点 3:必须安装头文件(.h/.hpp)
这是最容易遗漏的点! 如果不安装头文件,用户拿到你的 .so 或 .a 文件,因为没有 .h,根本无法调用你的库接口。
bash
# 安装头文件到 include 目录
install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/
DESTINATION include
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp"
)
# 如果是生成的头文件,也要单独安装
install(FILES ${PROTO_OUT_DIR}/person.pb.h DESTINATION include/proto)
注意点 4:处理 RPATH(动态库运行时路径)
如果我们的项目是动态库 ,打包后在其他机器运行可能会报 error while loading shared libraries。解决方法是在打包前设置 RPATH,告诉程序去哪里找动态库。
bash
# 在生成可执行文件时设置 RPATH
if(UNIX AND NOT APPLE)
set_target_properties(myapp PROPERTIES
INSTALL_RPATH "$ORIGIN/../lib" # 假设可执行文件在 bin,库在 lib
BUILD_WITH_INSTALL_RPATH TRUE
)
endif()
所以完整的 CMake 项目生命周期大概是:
- 编写代码 :写
.cpp、.h、.proto。 - 配置构建 :写
CMakeLists.txt,用add_library/add_executable。 - 测试 :写
test_*.cpp,开启enable_testing(),用ctest验证功能。 - 安装 :写
install(TARGETS ...),指定安装路径。 - 打包 :引入
CPack,设置CPACK_*变量,用cpack生成跨平台安装包。
通过这一套流程,你真正实现了从 "代码开发" 到 "测试验证" 再到 "跨平台发布" 的全流程闭环,这才是工业级 CMake 开发的标准形态。
通用 CMake 模板工程(含测试 + 打包 + 自研库)
下面是一份开箱即用的 CMake 模板,覆盖了「项目结构 → 库构建 → 测试 → 安装 → 打包」全流程,你可以直接复制到新项目中使用。
一、标准项目目录结构
bash
my_project/
├── CMakeLists.txt # 顶层 CMake 配置(总入口)
├── include/ # 公共头文件(对外暴露的接口)
│ └── my_lib/
│ └── my_lib.h
├── src/ # 库源码
│ ├── CMakeLists.txt
│ ├── add.cpp
│ └── sub.cpp
├── app/ # 可执行程序(调用库)
│ ├── CMakeLists.txt
│ └── main.cpp
├── tests/ # 测试代码
│ ├── CMakeLists.txt
│ └── test_my_lib.cpp
└── cmake/ # (可选)自定义 CMake 模块
└── MyLibConfig.cmake.in
二、顶层 CMakeLists.txt(总入口)
bash
cmake_minimum_required(VERSION 3.18)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# ===================== 全局配置 =====================
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 输出目录统一管理(可选,让构建产物更整洁)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# ===================== 子模块 =====================
add_subdirectory(src) # 构建库
add_subdirectory(app) # 构建可执行程序
add_subdirectory(tests) # 构建测试
# ===================== 测试支持(CTest) =====================
enable_testing()
include(CTest)
# ===================== 打包支持(CPack) =====================
include(CPack)
# 包基本信息
set(CPACK_PACKAGE_NAME "MyProject")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A CMake template project with library, app, test and package.")
set(CPACK_PACKAGE_VENDOR "YourName/YourCompany")
set(CPACK_PACKAGE_CONTACT "your@email.com")
# 生成器配置(跨平台)
if(WIN32)
set(CPACK_GENERATOR "ZIP;NSIS")
elseif(APPLE)
set(CPACK_GENERATOR "ZIP;DragNDrop")
elseif(UNIX)
set(CPACK_GENERATOR "ZIP;DEB;RPM")
endif()
# Linux 特定配置
if(UNIX AND NOT APPLE)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER ${CPACK_PACKAGE_CONTACT})
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
endif()
三、库模块 src/CMakeLists.txt
bash
# 收集源码
file(GLOB_RECURSE LIB_SOURCES "*.cpp")
file(GLOB_RECURSE LIB_HEADERS "${PROJECT_SOURCE_DIR}/include/my_lib/*.h")
# 构建静态库(或 SHARED 动态库)
add_library(MyLib STATIC ${LIB_SOURCES} ${LIB_HEADERS})
# 头文件路径(关键:区分构建时/安装时)
target_include_directories(MyLib
PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# 链接依赖(如果有第三方库)
# find_package(XXX REQUIRED)
# target_link_libraries(MyLib PUBLIC XXX::XXX)
# ===================== 安装规则(打包基础) =====================
# 安装库文件
install(TARGETS MyLib
EXPORT MyLibTargets
ARCHIVE DESTINATION lib # 静态库 .a/.lib
LIBRARY DESTINATION lib # 动态库 .so/.dylib
RUNTIME DESTINATION bin # Windows DLL
INCLUDES DESTINATION include
)
# 安装头文件
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/
DESTINATION include
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp"
)
# 导出目标配置(让别人能 find_package(MyLib))
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE MyLib::
DESTINATION lib/cmake/MyLib
)
# 生成 Config.cmake(让 find_package 能找到)
include(CMakePackageConfigHelpers)
configure_package_config_file(
${PROJECT_SOURCE_DIR}/cmake/MyLibConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
INSTALL_DESTINATION lib/cmake/MyLib
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake
DESTINATION lib/cmake/MyLib
)
四、可执行程序模块 app/CMakeLists.txt
bash
# 收集源码
file(GLOB_RECURSE APP_SOURCES "*.cpp")
# 构建可执行文件
add_executable(my_app ${APP_SOURCES})
# 链接自研库
target_link_libraries(my_app PRIVATE MyLib)
# ===================== RPATH 配置(动态库运行时路径) =====================
if(UNIX AND NOT APPLE)
set_target_properties(my_app PROPERTIES
INSTALL_RPATH "$ORIGIN/../lib" # 可执行文件在 bin,库在 lib
BUILD_WITH_INSTALL_RPATH TRUE
)
endif()
# ===================== 安装规则 =====================
install(TARGETS my_app
RUNTIME DESTINATION bin
)
五、测试模块 tests/CMakeLists.txt
bash
# 收集测试源码
file(GLOB_RECURSE TEST_SOURCES "*.cpp")
# 构建测试可执行文件
add_executable(test_my_lib ${TEST_SOURCES})
# 链接库
target_link_libraries(test_my_lib PRIVATE MyLib)
# 注册测试(CTest)
add_test(NAME MyLib_Add_Test COMMAND test_my_lib)
add_test(NAME MyLib_Sub_Test COMMAND test_my_lib)
六、测试示例 tests/test_my_lib.cpp
cpp
#include <cassert>
#include "my_lib/my_lib.h"
int main() {
// 测试加法
assert(add(2, 3) == 5);
// 测试减法
assert(sub(5, 2) == 3);
return 0;
}
七、使用流程(一键构建 + 测试 + 打包)
bash
# 1. 创建构建目录
mkdir build && cd build
# 2. 配置项目
cmake ..
# 3. 编译
make -j$(nproc)
# 4. 运行测试
ctest -v
# 5. 打包(生成安装包)
cpack
# 6. (可选)安装到系统
sudo make install