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.debMathTools-1.0.0-Linux.rpmMathTools-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图

下面这张图展示了从源码到发布包的完整流程:

CMake + CPack 全流程构建图 源码组织 src/ tests/ docs/ CMake 配置 CMakeLists.txt 构建 cmake --build 库文件 .a/.so 单元测试 Google Test 文档生成 Doxygen CPack 打包 DEB / RPM / ZIP / TGZ / DMG 发布:多平台安装包

避坑指南

最后,分享几个我踩过的坑:

  • 路径问题:我曾经在 install() 里写死了绝对路径,结果换台机器就编译不过。后来改用生成器表达式,再也没出过问题。
  • 测试默认关闭:有次我忘了 option(BUILD_TESTS ON),结果 CI 里测试根本没跑,bug 上线了才发现。现在我的所有项目测试默认都是开的。
  • CPack 生成器选择:在 Linux 上生成 DEB 包时,记得设置 CPACK_DEBIAN_PACKAGE_MAINTAINER,否则会报错。我第一次打包时被这个坑了半小时。
  • Doxygen 路径:如果 Doxygen 没装,find_package(Doxygen) 会报错。所以我一般用 option 控制,只在需要时开启文档生成。

好了,这一章的内容就到这里。你跟着走一遍,应该能掌握从源码到发布包的全流程。记住,工具是死的,流程是活的。每个项目都有自己的特点,但核心思想不变:自动化、可重复、跨平台。