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模块在支撑。
30.8 避坑指南
最后,分享几个我实际踩过的坑:
- 路径问题:我曾经在
target_include_directories里用了绝对路径,结果换到另一台机器上就编译不过了。后来全部改用${CMAKE_CURRENT_SOURCE_DIR}这种相对路径,问题解决。 - 测试框架版本:有次升级了Catch2的大版本,结果测试代码里一堆API废弃警告。建议在
FetchContent里锁定具体tag,不要用main分支。 - CI中的缓存:GitHub Actions默认每次都是全新环境,构建依赖每次都重新下载。可以用
actions/cache把build/_deps目录缓存起来,能省不少时间。
注意:如果你在Windows上构建,记得在CI中设置好MSVC的环境变量。否则cmake会找不到编译器,报一些莫名其妙的错误。
好了,这一章的内容就到这里。你现在应该能独立搭建一个完整的C++项目了——从目录结构、CMakeLists.txt设计,到测试、打包、CI/CD,一条龙搞定。以后不管接到什么项目,都可以拿这套模板快速起步。
公众号:蓝海资料掘金营,微信deep3321