37. 10.1 项目一:命令行工具库

引言:从”施工图”到”第一栋楼”

在前九章中,我们的 CMake “施工队长”已经系统学习了从看图纸(Target)、运材料(源文件管理)、调工艺(编译链接控制),到搞质检(测试)、办交房(安装打包)、跑海外工程(交叉编译)的全套本领。但到目前为止,这些知识还像分散在图纸上的标注——我们知道每根钢筋的规格,却还没把它们浇筑成一栋真正能住人的楼。

从这一章开始,我们要进入实战阶段。我会带你完成三个难度递增的真实项目,把前面学过的 Modern CMake 理念逐一落地。第一个项目,我们选择了一个看似朴素、却涵盖面极广的实战场景——命令行工具库(CLI Tool)。它会涉及参数解析、日志系统、单元测试、安装规则,以及 CI/CD 流水线。这就像施工队的第一栋样板房:规模不大,但五脏俱全。

项目需求与技术选型

在动手写代码之前,老练的施工队长绝不会盲目开挖地基。我们先来做需求分析,明确这栋”样板房”到底要解决什么问题。

功能需求

假设我们要做一个名为 filetool 的跨平台命令行工具,它需要支持以下功能:

  • 参数解析:支持子命令(如 filetool copyfiletool 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 节中,我们学过 ExternalProjectFetchContentfind_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 规范:

  1. 使用 target_sources 而不是直接把所有源文件写在 add_executable 参数里,这在大型项目中更易维护。
  2. target_include_directories 使用 PRIVATE,因为可执行文件不需要向外界暴露头文件搜索路径。
  3. target_link_libraries 直接链接目标名 CLI11::CLI11spdlog::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/binC: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 知识串成了一条完整的生产线:

  1. 需求与选型:明确第三方依赖(CLI11 + spdlog),并决定用 FetchContent 管理。
  2. 目录结构:采用 src/ + tests/ + include/ 的分离结构,符合大型工程规范。
  3. 目标管理:用 target_sourcestarget_link_libraries 精确控制可执行文件的构建,拒绝全局变量污染。
  4. 测试集成:引入 Catch2 v3,通过 catch_discover_tests 自动对接 CTest。
  5. 安装打包:利用 GNUInstallDirsinstall(TARGETS) 完成跨平台部署,并预留 CPack 扩展。
  6. CI/CD:通过 GitHub Actions 矩阵构建,确保代码在多平台、多配置下始终健康。

当然,这只是一栋”样板房”——它是个可执行文件,还没有涉及库导出、接口设计等更复杂的议题。别急,下一节(10.2)我们将把这栋楼的地下室改造为跨平台共享库,真正进入”开发商模式”,学习如何把代码变成可供他人采购的”建筑材料”。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……