第二十一章:构建系统与包管理:CMake深入、Conan、vcpkg与CI/CD集成
说实话,构建系统这事儿,很多C++开发者都低估了它的重要性。我见过太多团队,代码写得漂亮,结果一换机器就编译不过,或者依赖库版本冲突到让人崩溃。嗯,今天我们就来把这套东西彻底捋清楚。
21.1 CMake深入:自定义命令与模块
CMake大家都用过,但大多数人只停留在add_executable和target_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 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)
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-
21.5 知识体系总览
下面这张图把本章的核心内容串起来了。你想想看,从本地开发到CI/CD,整个链路其实就是一个闭环。
21.6 实战建议
说了这么多,最后给几条实在的建议:
- 别贪多:刚开始用Conan或vcpkg,先管好三五个核心依赖就行。我见过有人一口气列了50个依赖,结果一半都用不上。
- 版本锁定:无论是Conan的
conanfile.txt还是vcpkg的vcpkg.json,一定要锁定版本号。否则某天依赖库一升级,你的代码可能就炸了。 - CI先跑起来:哪怕只是简单的编译检查,也比没有强。我习惯在项目第一天就配好CI,后面再慢慢加测试和部署。
- 本地和CI保持一致:用同样的toolchain、同样的依赖版本。我踩过最大的坑就是本地用Conan,CI用vcpkg,结果两个版本的库行为不一样。
一句话总结:构建系统和包管理不是锦上添花,而是C++工程的基石。花时间把这套东西搞扎实了,后面开发效率能翻倍。
公众号:蓝海资料掘金营,微信deep3321