引言:从”施工图”到”第一栋楼”
在前九章中,我们的 CMake “施工队长”已经系统学习了从看图纸(Target)、运材料(源文件管理)、调工艺(编译链接控制),到搞质检(测试)、办交房(安装打包)、跑海外工程(交叉编译)的全套本领。但到目前为止,这些知识还像分散在图纸上的标注——我们知道每根钢筋的规格,却还没把它们浇筑成一栋真正能住人的楼。
从这一章开始,我们要进入实战阶段。我会带你完成三个难度递增的真实项目,把前面学过的 Modern CMake 理念逐一落地。第一个项目,我们选择了一个看似朴素、却涵盖面极广的实战场景——命令行工具库(CLI Tool)。它会涉及参数解析、日志系统、单元测试、安装规则,以及 CI/CD 流水线。这就像施工队的第一栋样板房:规模不大,但五脏俱全。
项目需求与技术选型
在动手写代码之前,老练的施工队长绝不会盲目开挖地基。我们先来做需求分析,明确这栋”样板房”到底要解决什么问题。
功能需求
假设我们要做一个名为 filetool 的跨平台命令行工具,它需要支持以下功能:
- 参数解析:支持子命令(如
filetool copy、filetool hash),以及-v/--verbose、-o/--output等选项。 - 日志输出:根据 verbose 级别输出 DEBUG/INFO/WARNING 信息,且支持彩色终端输出。
- 核心操作:文件拷贝、计算 SHA256 哈希值等基础文件操作。
- 可测试性:核心逻辑必须脱离
main()单独可测,方便后续单元测试覆盖。
技术选型
基于”不要重复造轮子”的原则,我们引入两个在业内有口皆碑的第三方库:
- CLI11:一个现代 C++11 编写的头文件优先(header-only)命令行解析库,支持子命令、配置文件、验证器,且与 CMake 集成极佳。
- spdlog:高性能的日志库,同样支持 header-only 模式,也支持编译为静态库。它的 API 设计简洁,且内置彩色控制台输出。
C++ 标准方面,我们选择 C++17——既能使用 std::filesystem 做文件操作,又不对编译器版本提出过高要求。
目录结构设计
在 Modern CMake 的实践中,源码目录(Source Tree)与构建目录(Build Tree)的分离是铁律。我们采用经典的”扁平化 + 模块化”结构:
filetool/
├── CMakeLists.txt # 根配置:项目管理、依赖、安装
├── cmake/
│ └── FetchDependencies.cmake # 第三方依赖获取逻辑(可选拆分)
├── src/
│ ├── main.cpp # 入口:仅负责解析参数并调用核心逻辑
│ ├── core/
│ │ ├── file_ops.cpp # 文件操作实现
│ │ └── file_ops.hpp # 接口声明
│ └── logger.cpp # spdlog 封装初始化
├── include/
│ └── filetool/ # 对外暴露的公共头文件(如果有库化需求)
├── tests/
│ ├── CMakeLists.txt
│ └── test_core.cpp # Catch2 测试用例
├── docs/
│ └── usage.md
└── README.md
这个结构有几个刻意的设计:
src/core/把业务逻辑与main.cpp解耦,方便测试。include/filetool/采用”命名空间式”头文件路径,如果未来想把filetool从可执行文件拆分为库,头文件路径无需改动。cmake/目录存放辅助的.cmake脚本,避免根CMakeLists.txt膨胀。
第三方依赖集成:CLI11 与 spdlog
在 4.3 节和 5.x 节中,我们学过 ExternalProject、FetchContent 和 find_package 三种”采购渠道”。对于这个命令行工具项目,我推荐优先使用 FetchContent——它能把依赖源码在配置阶段就拉下来,并直接通过 add_subdirectory 集成,完美支持目标导出(target export),是最符合 Modern CMake “基于目标” 哲学的做法。
根 CMakeLists.txt 的依赖配置
cmake_minimum_required(VERSION 3.19)
project(filetool VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# ---------------------------------------------------------
# 1. 获取 CLI11
# ---------------------------------------------------------
include(FetchContent)
FetchContent_Declare(
CLI11
GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git
GIT_TAG v2.4.2
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(CLI11)
# ---------------------------------------------------------
# 2. 获取 spdlog(优先 header-only 模式)
# ---------------------------------------------------------
set(SPDLOG_FMT_EXTERNAL OFF CACHE INTERNAL "") # 使用 spdlog 内置的 fmt
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.14.1
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(spdlog)
这里有几个实战细节值得注意:
GIT_SHALLOW TRUE可以极大减少克隆时间,只拉取指定标签的最近一次提交。FetchContent_MakeAvailable是 CMake 3.14+ 提供的便捷命令,相当于FetchContent_Populate+add_subdirectory。- spdlog 默认是 header-only,但如果你在它之前已经通过
find_package(fmt)引入了 fmt,可以通过SPDLOG_FMT_EXTERNAL控制。这里我们保持默认,减少复杂度。
目标定义与链接
add_executable(filetool)
target_sources(filetool PRIVATE
src/main.cpp
src/core/file_ops.cpp
src/logger.cpp
)
target_include_directories(filetool PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(filetool PRIVATE
CLI11::CLI11
spdlog::spdlog
)
# 如果启用 warnings-as-errors(现代 C++ 最佳实践)
if(MSVC)
target_compile_options(filetool PRIVATE /W4 /WX)
else()
target_compile_options(filetool PRIVATE -Wall -Wextra -Wpedantic -Werror)
endif()
请留意这里的 Modern CMake 规范:
- 使用
target_sources而不是直接把所有源文件写在add_executable参数里,这在大型项目中更易维护。 target_include_directories使用PRIVATE,因为可执行文件不需要向外界暴露头文件搜索路径。target_link_libraries直接链接目标名CLI11::CLI11和spdlog::spdlog,而不是操作原始变量。这是”基于目标”思维的核心体现。
单元测试框架集成:Catch2 与 GoogleTest 的抉择
在 7.1 节中我们学过 CTest 的基础用法。现在需要为 filetool 选择一个具体的测试框架。两个主流选择:
- Catch2 v3:现代、轻量、断言可读性极高,且 v3 版本改为非 header-only,编译速度大幅提升。适合中小型项目。
- GoogleTest (gtest):功能最全,有 Mock 框架(gmock),生态庞大。适合大型项目或需要复杂 Mock 的场景。
对于我们的命令行工具,Catch2 v3 是更轻盈的选择。但如果你所在团队已经统一使用 gtest,完全可以用相同的方式替换。
测试子目录配置
在 tests/CMakeLists.txt 中:
# 让测试也能找到 Catch2(如果根目录已经 FetchContent 过,这里会自动复用)
find_package(Catch2 3 CONFIG REQUIRED)
add_executable(test_filetool
test_core.cpp
../src/core/file_ops.cpp # 链接核心逻辑
)
target_include_directories(test_filetool PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/include
)
target_link_libraries(test_filetool PRIVATE Catch2::Catch2WithMain)
# 自动注册到 CTest
include(Catch)
catch_discover_tests(test_filetool)
这里用到了 Catch2::Catch2WithMain,这是 Catch2 v3 提供的便捷目标,自带 main() 函数,你只需写测试用例即可。如果你的根 CMakeLists.txt 中已经把 Catch2 也做了 FetchContent,那么 find_package 会在本地构建树中找到它,无需系统预装。
根 CMakeLists.txt 末尾别忘了启用测试:
enable_testing()
add_subdirectory(tests)
安装与打包配置
一个合格的命令行工具,最终需要被安装到系统的 bin 目录(如 /usr/local/bin 或 C:Program Filesfiletoolbin),让用户在终端里直接输入 filetool 就能运行。在 6.1 节中,我们学过 install() 的基础语法,现在把它落地。
include(GNUInstallDirs)
install(TARGETS filetool
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
# 安装文档和 README
install(FILES README.md docs/usage.md
DESTINATION ${CMAKE_INSTALL_DOCDIR}
)
这里使用了 GNUInstallDirs 模块提供的标准变量(${CMAKE_INSTALL_BINDIR}、${CMAKE_INSTALL_DOCDIR}),确保在不同平台(Linux 的 FHS、Windows、macOS)上都能安装到合理的位置。用户在构建后可以执行:
cmake --build build
cmake --install build --prefix /path/to/install
如果你还想生成一个独立的压缩包(6.3 节 CPack 的内容),可以在根 CMakeLists.txt 末尾追加:
set(CPACK_PACKAGE_NAME "filetool")
set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})
set(CPACK_GENERATOR "TGZ;ZIP") # Linux 用 tar.gz,Windows 用 zip
include(CPack)
CI/CD 集成:GitHub Actions 与 GitLab CI
现代项目离不开持续集成(CI)。我们的目标是:每次提交代码后,自动在 Linux、macOS、Windows 三个平台上完成”配置 → 编译 → 测试 → 安装”的全流程。这里给出 GitHub Actions 的完整配置作为示例,GitLab CI 的逻辑完全类似,只是语法不同。
GitHub Actions 配置
在项目根目录创建 .github/workflows/ci.yml:
name: Build and Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
build_type: [Release, Debug]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
- name: Build
run: cmake --build build --config ${{ matrix.build_type }}
- name: Test
working-directory: build
run: ctest -C ${{ matrix.build_type }} --output-on-failure
- name: Install (Unix)
if: runner.os != 'Windows'
run: cmake --install build --prefix ${{ github.workspace }}/install
- name: Install (Windows)
if: runner.os == 'Windows'
run: cmake --install build --config ${{ matrix.build_type }} --prefix ${{ github.workspace }}/install
这个配置有几个值得玩味的地方:
- 多平台矩阵:同时覆盖三大桌面操作系统,避免”在我机器上能跑”的悲剧。
- 多配置构建:Debug 和 Release 都测。注意 Windows 的多配置生成器(Visual Studio)需要用
--config指定类型,而单配置生成器(Ninja、Unix Makefiles)则依赖-DCMAKE_BUILD_TYPE。 ctest --output-on-failure:一旦测试失败,立即输出详细日志,方便排错。
GitLab CI 配置(供参考)
如果是 GitLab 用户,可在根目录创建 .gitlab-ci.yml:
stages:
- build
- test
variables:
GIT_SUBMODULE_STRATEGY: recursive
build:
stage: build
image: gcc:13
script:
- cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- cmake --build build
test:
stage: test
image: gcc:13
script:
- cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- cmake --build build
- cd build && ctest --output-on-failure
小结:一栋五脏俱全的样板房
通过这个项目,我们把前面学过的 Modern CMake 知识串成了一条完整的生产线:
- 需求与选型:明确第三方依赖(CLI11 + spdlog),并决定用
FetchContent管理。 - 目录结构:采用
src/+tests/+include/的分离结构,符合大型工程规范。 - 目标管理:用
target_sources、target_link_libraries精确控制可执行文件的构建,拒绝全局变量污染。 - 测试集成:引入 Catch2 v3,通过
catch_discover_tests自动对接 CTest。 - 安装打包:利用
GNUInstallDirs和install(TARGETS)完成跨平台部署,并预留 CPack 扩展。 - CI/CD:通过 GitHub Actions 矩阵构建,确保代码在多平台、多配置下始终健康。
当然,这只是一栋”样板房”——它是个可执行文件,还没有涉及库导出、接口设计等更复杂的议题。别急,下一节(10.2)我们将把这栋楼的地下室改造为跨平台共享库,真正进入”开发商模式”,学习如何把代码变成可供他人采购的”建筑材料”。


没有回复内容