30、实战案例:构建一个完整的C++项目:从源码组织、单元测试、文档生成到多平台打包发布的全流程演练
终于到了这一章。说实话,前面讲了那么多CMake的语法、CPack的配置、测试框架的集成,都是零散的知识点。这一章,我们要把它们串起来,做一个真正的、完整的项目。
我个人习惯,每学一个新工具,一定要亲手搭一个完整的项目出来。光看文档是不够的,你得踩坑,得调试,得看着那些绿色的测试通过、红色的失败,最后看到安装包生成出来——那种成就感,才是学习的动力。
好,我们开始。
项目概览:我们要做什么?
假设我们要写一个简单的数学工具库,叫 MathTools。它提供几个基础函数:加法、阶乘、斐波那契数列。听起来很简单对吧?但我们要把它做成一个“正经”的项目:
- 有清晰的目录结构
- 有单元测试
- 能生成API文档
- 能打包成deb、rpm、zip等格式
- 能在Linux、macOS、Windows上构建
嗯,这里要注意:很多初学者觉得“小项目没必要搞这么复杂”。我在项目中遇到过太多次了——项目一开始只有几百行代码,大家随便写写,半年后变成几万行,没人敢重构,因为没测试,没文档,连编译脚本都是手写的。所以,从一开始就养成好习惯,真的不亏。
核心原则:一个专业的C++项目,从第一天起就应该有CMakeLists.txt、有测试、有打包配置。哪怕只有两个函数,也要走完整个流程。
第一步:目录结构设计
我个人习惯的目录结构是这样的:
MathTools/
├── CMakeLists.txt # 顶层CMake
├── src/ # 源码
│ ├── CMakeLists.txt
│ ├── math_tools.h
│ └── math_tools.cpp
├── tests/ # 单元测试
│ ├── CMakeLists.txt
│ └── test_math_tools.cpp
├── docs/ # 文档配置
│ └── Doxyfile.in
├── cmake/ # 自定义CMake模块
│ └── FindDoxygen.cmake
└── packaging/ # CPack配置
└── CPackConfig.cmake
为什么这么分?说白了,就是让每个目录职责单一。你想想看,如果所有东西都堆在根目录,三个月后你自己都找不到哪个文件是干嘛的。
第二步:顶层CMakeLists.txt
顶层文件负责定义项目、设置C++标准、引入子目录。我一般这样写:
cmake_minimum_required(VERSION 3.16)
project(MathTools VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 选项:是否构建测试
option(BUILD_TESTS "Build unit tests" ON)
option(BUILD_DOCS "Build documentation" OFF)
# 添加子目录
add_subdirectory(src)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
if(BUILD_DOCS)
find_package(Doxygen REQUIRED)
add_subdirectory(docs)
endif()
# 打包配置
include(cmake/CPackConfig.cmake)
这里有个小技巧:option 默认值设为 ON,这样开发者 clone 下来直接 cmake -B build 就能跑测试。我曾经见过一个项目,测试默认是关的,结果大家都不跑测试,bug 攒了一堆才被发现。
第三步:源码实现与库构建
src/CMakeLists.txt 负责编译库:
add_library(MathTools
math_tools.h
math_tools.cpp
)
target_include_directories(MathTools
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
# 安装规则
install(TARGETS MathTools
EXPORT MathToolsTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
install(FILES math_tools.h DESTINATION include)
代码本身很简单,但注意 $<BUILD_INTERFACE> 和 $<INSTALL_INTERFACE> 的用法。这是CMake的生成器表达式,说白了就是:编译时用源码路径,安装后用安装路径。这个坑我踩过——以前我直接写死了路径,结果安装后头文件路径全乱了。
math_tools.h 内容:
#pragma once
namespace mathtools {
int add(int a, int b);
unsigned long long factorial(int n);
unsigned long long fibonacci(int n);
}
第四步:单元测试集成
测试我用 Google Test。为什么选它?因为生态好,CMake 集成方便,而且我在项目中用了好多年,没出过什么大问题。
tests/CMakeLists.txt:
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip
)
FetchContent_MakeAvailable(googletest)
add_executable(test_math_tools test_math_tools.cpp)
target_link_libraries(test_math_tools PRIVATE MathTools gtest_main)
add_test(NAME MathToolsTests COMMAND test_math_tools)
测试代码示例:
#include <gtest/gtest.h>
#include "math_tools.h"
TEST(MathToolsTest, Add) {
EXPECT_EQ(mathtools::add(2, 3), 5);
EXPECT_EQ(mathtools::add(-1, 1), 0);
}
TEST(MathToolsTest, Factorial) {
EXPECT_EQ(mathtools::factorial(0), 1);
EXPECT_EQ(mathtools::factorial(5), 120);
}
TEST(MathToolsTest, Fibonacci) {
EXPECT_EQ(mathtools::fibonacci(0), 0);
EXPECT_EQ(mathtools::fibonacci(10), 55);
}
小提示:用 FetchContent 下载 Google Test 是最省心的方式。不需要手动安装,CMake 会自动处理。我第一次用的时候还傻傻地手动编译安装,后来发现一行 FetchContent_MakeAvailable 就搞定了。
第五步:文档生成
文档我用 Doxygen。配置很简单,在 docs/CMakeLists.txt 里:
configure_file(Doxyfile.in ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile @ONLY)
add_custom_target(docs
COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating API documentation with Doxygen"
VERBATIM
)
然后在源码里加注释:
/**
* @brief 计算两个整数的和
* @param a 第一个加数
* @param b 第二个加数
* @return a + b 的结果
*/
int add(int a, int b);
运行 cmake --build . --target docs,就能生成漂亮的HTML文档。我个人习惯把文档生成放在CI里,每次提交自动更新。
第六步:CPack打包配置
这是重头戏。CPack 可以生成多种格式的安装包。我一般这样配置:
# cmake/CPackConfig.cmake
set(CPACK_PACKAGE_NAME "MathTools")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A simple math utility library")
set(CPACK_PACKAGE_VENDOR "Your Company")
# 生成器
set(CPACK_GENERATOR "ZIP;TGZ")
if(WIN32)
set(CPACK_GENERATOR "NSIS;ZIP")
elseif(APPLE)
set(CPACK_GENERATOR "DragNDrop;TGZ")
else()
set(CPACK_GENERATOR "DEB;RPM;TGZ")
endif()
# DEB 特定配置
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "developer@example.com")
set(CPACK_DEBIAN_PACKAGE_SECTION "libs")
# RPM 特定配置
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries")
# 安装路径
set(CPACK_PACKAGING_INSTALL_PREFIX "/usr/local")
include(CPack)
然后构建并打包:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
cpack --config build/CPackConfig.cmake
你会看到生成了 MathTools-1.0.0-Linux.deb、MathTools-1.0.0-Linux.rpm、MathTools-1.0.0-Linux.tar.gz 等文件。
注意:生成 DEB 和 RPM 包需要系统安装相应的工具(dpkg-deb、rpmbuild)。在 macOS 上生成 DMG 需要安装 hdiutil(系统自带)。Windows 上生成 NSIS 安装包需要安装 NSIS 工具。
第七步:多平台构建实战
我建议用 CMake 的预设(Presets)来管理不同平台的构建配置。创建一个 CMakePresets.json:
{
"version": 3,
"configurePresets": [
{
"name": "linux-release",
"displayName": "Linux Release",
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/build/linux-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"BUILD_TESTS": "ON"
}
},
{
"name": "macos-release",
"displayName": "macOS Release",
"generator": "Xcode",
"binaryDir": "${sourceDir}/build/macos-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "windows-release",
"displayName": "Windows Release",
"generator": "Visual Studio 17 2022",
"binaryDir": "${sourceDir}/build/windows-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}
然后一行命令就能配置:
cmake --preset linux-release
说实话,Presets 是我近几年最喜欢的 CMake 特性。以前我要写一堆脚本去切换不同的生成器,现在一个 JSON 文件全搞定。
全流程SVG图
下面这张图展示了从源码到发布包的完整流程:
避坑指南
最后,分享几个我踩过的坑:
- 路径问题:我曾经在
install()里写死了绝对路径,结果换台机器就编译不过。后来改用生成器表达式,再也没出过问题。 - 测试默认关闭:有次我忘了
option(BUILD_TESTS ON),结果 CI 里测试根本没跑,bug 上线了才发现。现在我的所有项目测试默认都是开的。 - CPack 生成器选择:在 Linux 上生成 DEB 包时,记得设置
CPACK_DEBIAN_PACKAGE_MAINTAINER,否则会报错。我第一次打包时被这个坑了半小时。 - Doxygen 路径:如果 Doxygen 没装,
find_package(Doxygen)会报错。所以我一般用option控制,只在需要时开启文档生成。
好了,这一章的内容就到这里。你跟着走一遍,应该能掌握从源码到发布包的全流程。记住,工具是死的,流程是活的。每个项目都有自己的特点,但核心思想不变:自动化、可重复、跨平台。