第二十一章:构建系统与包管理:CMake深入、Conan、vcpkg与CI/CD集成

说实话,构建系统这事儿,很多C++开发者都低估了它的重要性。我见过太多团队,代码写得漂亮,结果一换机器就编译不过,或者依赖库版本冲突到让人崩溃。嗯,今天我们就来把这套东西彻底捋清楚。

21.1 CMake深入:自定义命令与模块

CMake大家都用过,但大多数人只停留在add_executabletarget_link_libraries这个层面。其实CMake的能力远不止这些。

21.1.1 自定义命令

我在项目中遇到过这样一个场景:需要把一些protobuf文件在编译前自动生成C++代码。这时候add_custom_command就派上用场了。

cmake_minimum_required(VERSION 3.20)
project(MyProject)

# 自定义命令:在编译前生成代码
add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/config.h
    COMMAND ${CMAKE_COMMAND} -E echo "// Auto-generated" > ${CMAKE_CURRENT_BINARY_DIR}/generated/config.h
    COMMAND ${CMAKE_COMMAND} -E echo "#define VERSION \"1.0.0\"" >> ${CMAKE_CURRENT_BINARY_DIR}/generated/config.h
    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/version.txt
    COMMENT "Generating config header..."
)

add_custom_target(generate_config DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated/config.h)
add_dependencies(MyExecutable generate_config)

这里有个坑:OUTPUT路径一定要写对,否则CMake会认为文件没生成,每次都要重新执行。我曾经因为这个浪费了半天时间。

21.1.2 编写自己的CMake模块

如果你在多个项目里重复使用同一套CMake逻辑,那就该把它封装成模块了。我习惯在项目根目录下建一个cmake/文件夹,专门放自定义模块。

# cmake/FindMyLibrary.cmake
# 查找自定义库的模块

find_path(MyLibrary_INCLUDE_DIR
    NAMES mylibrary/mylibrary.h
    PATHS /usr/local/include /opt/mylibrary/include
)

find_library(MyLibrary_LIBRARY
    NAMES mylibrary
    PATHS /usr/local/lib /opt/mylibrary/lib
)

if(MyLibrary_INCLUDE_DIR AND MyLibrary_LIBRARY)
    set(MyLibrary_FOUND TRUE)
    add_library(MyLibrary::MyLibrary UNKNOWN IMPORTED)
    set_target_properties(MyLibrary::MyLibrary PROPERTIES
        IMPORTED_LOCATION "${MyLibrary_LIBRARY}"
        INTERFACE_INCLUDE_DIRECTORIES "${MyLibrary_INCLUDE_DIR}"
    )
else()
    set(MyLibrary_FOUND FALSE)
endif()

mark_as_advanced(MyLibrary_INCLUDE_DIR MyLibrary_LIBRARY)

然后在主CMakeLists.txt里这样用:

list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
find_package(MyLibrary REQUIRED)
target_link_libraries(MyExecutable PRIVATE MyLibrary::MyLibrary)
我的习惯:模块命名遵循Find<PackageName>.cmake的规范,这样别人一看就知道是干什么的。另外,记得用mark_as_advanced隐藏内部变量,别让它们污染CMake GUI。

21.2 Conan包管理器

Conan是目前C++生态里最成熟的包管理器之一。说白了,它就像Python的pip或者Node.js的npm,专门解决C++依赖管理这个老大难问题。

21.2.1 基本使用

先装好Conan:

pip install conan

创建一个conanfile.txt

[requires]
fmt/8.1.1
spdlog/1.10.0
gtest/1.11.0

[generators]
CMakeDeps
CMakeToolchain

[options]
fmt:shared=False
spdlog:shared=False

然后执行:

conan install . --output-folder=build --build=missing

在CMake里集成:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MyApp)

# Conan生成的toolchain
include(${CMAKE_BINARY_DIR}/conan_toolchain.cmake)

find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt spdlog::spdlog)
注意:Conan的profile配置很重要。我遇到过好几次因为profile没配好,导致在Linux上编译出了Windows的库。建议每个项目都显式指定profile:conan install . -pr=default

21.2.2 创建自己的Conan包

如果你写了一个通用库,想分享给团队用,可以把它打包成Conan包。创建一个conanfile.py

from conan import ConanFile

class MyLibraryConan(ConanFile):
    name = "mylibrary"
    version = "1.0.0"
    license = "MIT"
    author = "Your Name"
    url = "https://github.com/yourname/mylibrary"
    description = "A useful library"
    settings = "os", "compiler", "build_type", "arch"
    options = {"shared": [True, False], "fPIC": [True, False]}
    default_options = {"shared": False, "fPIC": True}
    generators = "CMakeToolchain", "CMakeDeps"

    def source(self):
        git = Git(self)
        git.clone(url="https://github.com/yourname/mylibrary.git", target=".")

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

    def package(self):
        cmake = CMake(self)
        cmake.install()

    def package_info(self):
        self.cpp_info.libs = ["mylibrary"]

打包并上传:

conan create . mylibrary/1.0.0@user/testing
conan upload mylibrary/1.0.0 -r=myremote

21.3 vcpkg使用

vcpkg是微软推出的C++包管理器,和Conan的思路不太一样。vcpkg更偏向于「系统级」的包管理,它会直接把库安装到本地,然后通过CMake的find_package来使用。

21.3.1 安装与配置

git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh  # Linux/macOS
# 或者 .\bootstrap-vcpkg.bat  # Windows

./vcpkg integrate install  # 全局集成

安装常用库:

./vcpkg install fmt spdlog boost-asio nlohmann-json

在CMake中使用:

cmake_minimum_required(VERSION 3.20)
project(MyApp)

# 指定vcpkg toolchain
set(CMAKE_TOOLCHAIN_FILE /path/to/vcpkg/scripts/buildsystems/vcpkg.cmake)

find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt spdlog::spdlog nlohmann_json::nlohmann_json)
我的建议:如果你团队里Windows开发者多,vcpkg会更友好。如果跨平台需求强,Conan更灵活。我个人两个都用,看项目需求选。

21.4 CI/CD集成

构建系统搞定了,包管理也配好了,接下来就是让这一切自动化。CI/CD是保证代码质量的关键一环。

21.4.1 GitHub Actions示例

下面是一个完整的CI配置,支持多平台、多编译器:

# .github/workflows/build.yml
name: C++ Build and Test

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

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        compiler: [gcc, clang, msvc]
        build_type: [Debug, Release]
        exclude:
          - os: windows-latest
            compiler: gcc
          - os: windows-latest
            compiler: clang
          - os: macos-latest
            compiler: msvc

    runs-on: ${{ matrix.os }}

    steps:
    - uses: actions/checkout@v3

    - name: Setup vcpkg
      uses: lukka/run-vcpkg@v10
      with:
        vcpkgDirectory: '${{ github.workspace }}/vcpkg'
        vcpkgGitCommitId: 'latest'

    - name: Configure CMake
      run: |
        cmake -B build \
          -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
          -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake

    - name: Build
      run: cmake --build build --config ${{ matrix.build_type }}

    - name: Test
      run: ctest --test-dir build --output-on-failure

    - name: Upload artifacts
      uses: actions/upload-artifact@v3
      with:
        name: ${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.build_type }}
        path: build/

21.4.2 缓存策略

CI最怕的就是每次都要重新编译所有依赖。我建议用缓存来加速:

- name: Cache vcpkg
  uses: actions/cache@v3
  with:
    path: |
      vcpkg/installed
      vcpkg/packages
    key: ${{ runner.os }}-vcpkg-${{ hashFiles('vcpkg.json') }}
    restore-keys: |
      ${{ runner.os }}-vcpkg-

- name: Cache CMake build
  uses: actions/cache@v3
  with:
    path: build
    key: ${{ runner.os }}-cmake-${{ hashFiles('CMakeLists.txt') }}-${{ hashFiles('src/**') }}
    restore-keys: |
      ${{ runner.os }}-cmake-
避坑指南:我曾经因为缓存key设计不合理,导致Debug和Release的缓存互相覆盖,编译出来全是错的。后来我学乖了,把build_type也加到key里。

21.5 知识体系总览

下面这张图把本章的核心内容串起来了。你想想看,从本地开发到CI/CD,整个链路其实就是一个闭环。

构建系统与包管理知识体系 CMake 构建系统 包管理 CI/CD 集成 自定义命令 自定义模块 Toolchain Presets Conan vcpkg 依赖管理 版本控制 GitHub Actions GitLab CI 缓存策略 多平台构建 核心目标 一次配置,到处构建 | 依赖可控,版本可追溯 | 自动化测试,持续集成

21.6 实战建议

说了这么多,最后给几条实在的建议:

  • 别贪多:刚开始用Conan或vcpkg,先管好三五个核心依赖就行。我见过有人一口气列了50个依赖,结果一半都用不上。
  • 版本锁定:无论是Conan的conanfile.txt还是vcpkg的vcpkg.json,一定要锁定版本号。否则某天依赖库一升级,你的代码可能就炸了。
  • CI先跑起来:哪怕只是简单的编译检查,也比没有强。我习惯在项目第一天就配好CI,后面再慢慢加测试和部署。
  • 本地和CI保持一致:用同样的toolchain、同样的依赖版本。我踩过最大的坑就是本地用Conan,CI用vcpkg,结果两个版本的库行为不一样。

一句话总结:构建系统和包管理不是锦上添花,而是C++工程的基石。花时间把这套东西搞扎实了,后面开发效率能翻倍。


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