30、综合实战:构建一个完整的C++项目

终于到了这一章。说实话,前面讲了那么多零散的知识点,什么target、生成器表达式、CPack、CTest,你可能觉得它们各自为战。但这一章,我们要把它们全部串起来。

我会带着你从零开始,设计一个完整的C++项目。这个项目不大,但五脏俱全——有库、有可执行文件、有单元测试、有打包脚本,最后还会配上CI/CD流水线。你想想看,这才是真实项目该有的样子。

本章核心目标:掌握一个现代C++项目的完整构建流程,从CMakeLists.txt设计到自动化发布。

30.1 项目结构设计

我个人习惯,项目一开始先把目录结构定好。结构定了,思路就清晰了。下面是我为这个实战项目设计的布局:

my_project/
├── CMakeLists.txt              # 顶层构建文件
├── cmake/
│   ├── CompilerWarnings.cmake  # 编译器警告配置
│   └── InstallRules.cmake      # 安装规则
├── src/
│   ├── CMakeLists.txt
│   ├── main.cpp
│   └── core/
│       ├── CMakeLists.txt
│       ├── calculator.h
│       └── calculator.cpp
├── tests/
│   ├── CMakeLists.txt
│   └── test_calculator.cpp
├── packaging/
│   └── CPackConfig.cmake       # 打包配置
└── .github/
    └── workflows/
        └── ci.yml              # CI/CD配置

嗯,这里要注意:cmake/ 目录放的是模块化的CMake脚本,这样顶层文件就不会太臃肿。我在项目中遇到过很多次,所有逻辑都塞在一个CMakeLists.txt里,最后改起来想死的心都有。

30.2 顶层CMakeLists.txt设计

顶层文件是整个构建系统的入口。它要做的几件事:指定最低版本、定义项目、设置C++标准、然后包含子目录。说白了,它就是个调度中心。

cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# 引入编译器警告配置
include(cmake/CompilerWarnings.cmake)

# 添加子目录
add_subdirectory(src/core)
add_subdirectory(src)
add_subdirectory(tests)

# 安装规则
include(cmake/InstallRules.cmake)

# 打包配置
include(packaging/CPackConfig.cmake)

你可能会问:为什么要把 src/core 放在 src 前面?因为 src 里的可执行文件依赖 core 库,必须先构建库。这个顺序我踩过坑,曾经因为顺序问题导致链接失败,查了半天才发现是构建顺序不对。

30.3 核心库与可执行文件

先看 src/core/CMakeLists.txt。这里我们构建一个静态库,提供计算器功能:

add_library(core STATIC
    calculator.cpp
    calculator.h
)

target_include_directories(core
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
        $<INSTALL_INTERFACE:include>
)

target_compile_features(core PUBLIC cxx_std_17)

注意那个 $<BUILD_INTERFACE>$<INSTALL_INTERFACE> 的用法。这是生成器表达式,它让库在构建时和安装后使用不同的头文件路径。我记得第一次看到这个写法时觉得好麻烦,但后来发现,没有它,安装后的项目根本找不到头文件。

再看 src/CMakeLists.txt

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE core)

# 应用编译器警告
apply_compiler_warnings(my_app)

# 安装可执行文件
install(TARGETS my_app DESTINATION bin)

这里调用了 apply_compiler_warnings 函数,它来自我们之前引入的 CompilerWarnings.cmake。这个函数是我自己封装的,专门用来统一管理所有目标的警告选项。

30.4 集成测试

测试这块,我推荐用CTest配合一个轻量级测试框架。这里我们用Catch2,因为它只需要一个头文件就能跑起来。

tests/CMakeLists.txt

include(FetchContent)
FetchContent_Declare(
    Catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG v3.4.0
)
FetchContent_MakeAvailable(Catch2)

add_executable(test_calculator test_calculator.cpp)
target_link_libraries(test_calculator PRIVATE core Catch2::Catch2WithMain)

# 注册测试
include(CTest)
add_test(NAME CalculatorTests COMMAND test_calculator)

测试代码示例:

#include <catch2/catch_test_macros.hpp>
#include "calculator.h"

TEST_CASE("Addition works", "[core]") {
    Calculator calc;
    REQUIRE(calc.add(2, 3) == 5);
    REQUIRE(calc.add(-1, 1) == 0);
}

TEST_CASE("Division by zero throws", "[core]") {
    Calculator calc;
    REQUIRE_THROWS_AS(calc.divide(1, 0), std::invalid_argument);
}

运行测试只需要:

mkdir build && cd build
cmake ..
cmake --build .
ctest --output-on-failure

我曾经在一个项目里忘了加 --output-on-failure,结果测试失败了只看到一个红色的FAILED,完全不知道哪里错了。后来养成了习惯,每次跑测试都带上这个参数。

30.5 打包发布

打包用CPack。我们配置生成两种包格式:源码包和二进制包。

packaging/CPackConfig.cmake

set(CPACK_PACKAGE_NAME "MyProject")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A modern C++ calculator library")

# 源码包
set(CPACK_SOURCE_GENERATOR "TGZ")
set(CPACK_SOURCE_IGNORE_FILES
    "/build/"
    "/.git/"
    ".*~"
)

# 二进制包
set(CPACK_GENERATOR "TGZ;DEB")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "developer@mycompany.com")

include(CPack)

打包命令:

cpack --config packaging/CPackConfig.cmake
cpack --config packaging/CPackConfig.cmake -G DEB  # 生成deb包

小技巧:如果你要发布到Linux发行版,DEB包是标配。但别忘了设置好依赖关系,否则用户安装时会报依赖缺失。

30.6 CI/CD配置

最后,我们配置GitHub Actions,实现提交代码后自动构建、测试、打包。

.github/workflows/ci.yml

name: CI Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Configure
      run: cmake -B build -DCMAKE_BUILD_TYPE=Release

    - name: Build
      run: cmake --build build --parallel

    - name: Test
      run: cd build && ctest --output-on-failure

    - name: Package
      run: cmake --build build --target package

    - name: Upload Artifacts
      uses: actions/upload-artifact@v4
      with:
        name: myproject-package
        path: build/*.tar.gz

这个流水线做了四件事:配置、构建、测试、打包。每次你往main分支推送代码,它都会自动跑一遍。我见过很多团队,测试只在本地跑,结果合并后才发现问题。有了CI,这种尴尬就少多了。

30.7 知识体系总览

下面这张图,把整个项目的构建流程串起来了。你可以看到,从源码到最终发布,每一步都有对应的CMake模块在支撑。

CMake项目构建流程总览 源代码 CMake配置 (cmake -B build) 构建 (cmake --build) 测试 (ctest) 打包 (cpack) 发布 (CI/CD) 顶层CMakeLists.txt src/ packaging/ .github/

30.8 避坑指南

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

  • 路径问题:我曾经在 target_include_directories 里用了绝对路径,结果换到另一台机器上就编译不过了。后来全部改用 ${CMAKE_CURRENT_SOURCE_DIR} 这种相对路径,问题解决。
  • 测试框架版本:有次升级了Catch2的大版本,结果测试代码里一堆API废弃警告。建议在 FetchContent 里锁定具体tag,不要用 main 分支。
  • CI中的缓存:GitHub Actions默认每次都是全新环境,构建依赖每次都重新下载。可以用 actions/cachebuild/_deps 目录缓存起来,能省不少时间。

注意:如果你在Windows上构建,记得在CI中设置好MSVC的环境变量。否则cmake会找不到编译器,报一些莫名其妙的错误。

好了,这一章的内容就到这里。你现在应该能独立搭建一个完整的C++项目了——从目录结构、CMakeLists.txt设计,到测试、打包、CI/CD,一条龙搞定。以后不管接到什么项目,都可以拿这套模板快速起步。


公众号:蓝海资料掘金营,微信deep3321