24. 6.4 发布到包管理器

导语

在前三节中,我们系统学习了 install() 规则的配置、目标导出与 Config 包的制作,以及 CPack 打包系统的实战应用。至此,你的项目已经能够生成规范的安装包和可复用的 CMake 配置。然而,在 Modern C++ 生态中,仅仅生成安装包已经不够了

如今的 C++ 开发者更习惯通过包管理器来获取依赖:一句 vcpkg install、一个 conanfile.txt 引用,或者一次 hunter_add_package 调用,就能自动下载、编译并链接第三方库。如果你的库无法被这些主流包管理器识别和安装,那么它的分发效率将大打折扣。

本节作为第六章的收官,将带你跳出”单一项目”的视角,学习如何将你的 CMake 项目发布到三大主流 C++ 包管理器——vcpkgConanHunter。我们将手把手编写 portfile.cmakeconanfile.py,并演示 Hunter 的集成方式,让你的项目真正融入现代 C++ 生态。

vcpkg 端口文件编写

Microsoft 的 vcpkg 是目前社区活跃度最高的 C++ 包管理器之一。要将你的 CMake 项目发布到 vcpkg,核心工作是编写端口(Port)文件。一个最小化的端口通常包含两个文件:

  • vcpkg.json:描述包的元数据(名称、版本、依赖、特性等)。
  • portfile.cmake:定义如何下载源码、配置 CMake、构建、安装,以及修复路径等。

vcpkg.json 结构详解

vcpkg.json 采用 JSON 格式,vcpkg 2.0 版本后强烈推荐使用 manifest 模式。以下是一个示例,假设我们要发布名为 mygraphics 的图形库:

{
  "name": "mygraphics",
  "version": "1.2.0",
  "description": "A lightweight 2D graphics library built on Modern CMake",
  "homepage": "https://github.com/yourname/mygraphics",
  "license": "MIT",
  "dependencies": [
    {
      "name": "fmt",
      "version>=": "9.1.0"
    },
    {
      "name": "sdl2",
      "platform": "!(uwp | android)"
    }
  ],
  "features": {
    "opengl": {
      "description": "Enable OpenGL backend",
      "dependencies": [
        "opengl"
      ]
    },
    "tests": {
      "description": "Build test suite",
      "dependencies": [
        "catch2"
      ]
    }
  },
  "default-features": []
}

关键字段说明:

  • version:支持语义化版本。vcpkg 也支持 version-semverversion-dateversion-string 等精确类型。
  • dependencies:可声明版本约束(version>=)和平台过滤(platform)。
  • features:定义可选组件(如 opengl),用户可通过 vcpkg install mygraphics[opengl] 启用。

portfile.cmake 完整示例

portfile.cmake 是端口的构建脚本。vcpkg 提供了一系列辅助函数(以 vcpkg_* 开头),大幅简化了流程。下面的示例展示了从 GitHub Release 下载、CMake 构建到安装补丁的完整过程:

# portfile.cmake for mygraphics
vcpkg_from_github(
    OUT_SOURCE_PATH SOURCE_PATH
    REPO yourname/mygraphics
    REF v1.2.0
    SHA512 0a1b2c3d4e5f...  # 下载文件的校验和,必须准确
    HEAD_REF main
)

# 检查并拉取依赖(可选,通常 vcpkg.json 已声明)
vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS
    FEATURES
        opengl    MYGRAPHICS_ENABLE_OPENGL
        tests     MYGRAPHICS_BUILD_TESTS
)

# 配置 CMake
vcpkg_cmake_configure(
    SOURCE_PATH "${SOURCE_PATH}"
    OPTIONS
        ${FEATURE_OPTIONS}
        -DMYGRAPHICS_BUILD_EXAMPLES=OFF
        -DCMAKE_REQUIRE_FIND_PACKAGE_fmt=ON
)

# 构建并安装
vcpkg_cmake_install()

# 修复 CMake 目标导出文件中的绝对路径(关键步骤!)
vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/mygraphics)

# 移除 debug 版本的 include 目录(节省空间)
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")

# 安装 LICENSE 文件
file(INSTALL "${SOURCE_PATH}/LICENSE" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright)

# 安装 usage 提示文件(可选但推荐)
file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")

几个必须掌握的细节:

  1. vcpkg_cmake_config_fixup:vcpkg 要求所有 CMake 配置文件必须位于 share/<port> 下。此命令会自动将 lib/cmake/xxx 中的 *Config.cmake 文件移动到正确位置,并修正内部的相对路径引用。
  2. file(REMOVE_RECURSE …/debug/include):debug 版本的 include 头文件与 release 完全一致,保留会造成重复,这是 vcpkg 的规范要求。
  3. SHA512:首次编写时可以先留空,运行 vcpkg install 时 vcpkg 会提示正确的哈希值。

本地测试端口

编写完成后,建议先在本地 overlay 端口目录测试,避免直接提交到官方仓库:

# 目录结构
# /my-overlay/
#   └── mygraphics/
#       ├── portfile.cmake
#       ├── vcpkg.json
#       └── usage

# 测试安装
vcpkg install mygraphics --overlay-ports=/path/to/my-overlay

测试成功后,你就可以向 microsoft/vcpkg 官方仓库提交 Pull Request,让整个社区都能通过 vcpkg install mygraphics 使用你的库。

Conan recipe 编写基础

Conan 是另一款跨平台、去中心化的 C++ 包管理器,其核心是 Python 编写的 conanfile.py(或简化的 conanfile.txt)。与 vcpkg 的”端口”概念不同,Conan 的 recipe 更强调构建逻辑的可编程性

最小可运行的 conanfile.py

对于 CMake 项目,Conan 2.x 推荐继承 ConanFile 并显式定义 layout()generate()build() 等方法。以下是一个完整的 recipe 模板:

from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps
from conan.tools.files import copy, get
from conan.tools.scm import Git
import os

class MyGraphicsConan(ConanFile):
    name = "mygraphics"
    version = "1.2.0"
    license = "MIT"
    url = "https://github.com/yourname/mygraphics"
    description = "A lightweight 2D graphics library"
    topics = ("graphics", "2d", "cmake")

    # 二进制包兼容的标准设置
    settings = "os", "compiler", "build_type", "arch"
    options = {
        "shared": [True, False],
        "fPIC": [True, False],
        "with_opengl": [True, False],
    }
    default_options = {
        "shared": False,
        "fPIC": True,
        "with_opengl": True,
    }

    # 导出源码补丁(如果有)
    exports_sources = "CMakeLists.txt", "src/*", "include/*", "cmake/*"

    def config_options(self):
        # Windows 下通常不需要 fPIC
        if self.settings.os == "Windows":
            del self.options.fPIC

    def configure(self):
        if self.options.shared:
            self.options.rm_safe("fPIC")

    def requirements(self):
        # 声明依赖
        self.requires("fmt/9.1.0")
        if self.options.with_opengl:
            self.requires("opengl/system")

    def layout(self):
        # 使用 CMake 默认布局,支持 multi-config generator
        cmake_layout(self)

    def generate(self):
        # 生成 CMakeDeps 和 CMakeToolchain
        deps = CMakeDeps(self)
        deps.generate()
        tc = CMakeToolchain(self)
        tc.variables["MYGRAPHICS_ENABLE_OPENGL"] = self.options.with_opengl
        tc.generate()

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

    def package(self):
        # 拷贝 License
        copy(self, "LICENSE", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses"))
        # 调用 CMake install
        cmake = CMake(self)
        cmake.install()

    def package_info(self):
        # 暴露给消费者的 CMake 目标名
        self.cpp_info.set_property("cmake_file_name", "MyGraphics")
        self.cpp_info.set_property("cmake_target_name", "MyGraphics::MyGraphics")
        self.cpp_info.libs = ["mygraphics"]
        if self.settings.os in ["Linux", "FreeBSD"]:
            self.cpp_info.system_libs.append("m")

关键点解析:

  • settings vs optionssettings 是项目外部环境的描述(如编译器版本、OS),通常不由 recipe 作者修改;options 是库本身的构建特性(如是否编译为动态库),由消费者通过 -o mygraphics:shared=True 指定。
  • generate():Conan 2.x 的核心改进。它会生成 conan_toolchain.cmake 和依赖的 *-config.cmake 占位文件,确保你的 find_package() 能在隔离环境中正确工作。
  • package_info():定义了消费者通过 find_package(MyGraphics) 时应找到的 target 名称。这里务必与你项目安装的 MyGraphicsConfig.cmake 中定义的目标名保持一致,否则会出现”找到包但找不到目标”的链接错误。

构建与上传到私有仓库

在源码目录放置上述 conanfile.py 后,执行以下命令:

# 创建包(本地缓存)
conan create . --build=missing

# 指定 profile(例如 C++17)
conan create . --build=missing -s compiler.cppstd=17

# 上传到远程仓库(如 Artifactory 或 ConanCenter)
conan remote add myrepo https://artifactory.yourcompany.com/artifactory/api/conan/conan-local
conan upload mygraphics/1.2.0 -r myrepo

对于闭源商业项目,Conan 的私有仓库支持使其成为比 vcpkg 更灵活的选择。

Hunter 包管理集成

Hunter 是一个”以 CMake 为中心”的去中心化包管理器,它的最大特点是完全无外部依赖——使用者只需要在 CMakeLists.txt 中引入 Hunter 的初始化脚本,剩下的工作全部由 CMake 完成。

Hunter 的工作原理

Hunter 通过维护一个名为 huntergate 的 CMake 模块实现自举。在配置阶段,它会根据 CMakeLists.txt 中声明的依赖,自动下载并构建外部库,所有构建产物缓存于 ~/.hunter 目录。

在项目中启用 Hunter

首先,在你的项目根目录放置 cmake/HunterGate.cmake(可从 Hunter 官方仓库获取)。然后修改根 CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(MyProject LANGUAGES CXX)

# --- Hunter 初始化 ---
include("cmake/HunterGate.cmake")
HunterGate(
    URL "https://github.com/cpp-pm/hunter/archive/v0.24.18.tar.gz"
    SHA1 "3b9c76f1558fd1256c4b8c6854c3c7c620df9b42"
)

# --- 声明依赖 ---
hunter_add_package(fmt)
find_package(fmt CONFIG REQUIRED)

hunter_add_package(Boost COMPONENTS system filesystem)
find_package(Boost CONFIG REQUIRED system filesystem)

# --- 你的目标 ---
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt Boost::system Boost::filesystem)

对于库作者而言,如果你想让自己的库也能通过 hunter_add_package(mygraphics) 被他人使用,需要向 Hunter 官方仓库提交一个 Hunter 包定义 PR。这个定义本质上也是一个 CMake 脚本,告诉 Hunter 如何下载和构建你的项目。

创建 Hunter 包定义(供上游贡献)

Hunter 的包定义位于其官方仓库的 cmake/projects/<Name>/hunter.cmake 路径下。以下是为 mygraphics 添加 Hunter 支持的示例:

# cmake/projects/mygraphics/hunter.cmake
include(hunter_add_version)
include(hunter_cacheable)
include(hunter_download)
include(hunter_pick_scheme)

hunter_add_version(
    PACKAGE_NAME mygraphics
    VERSION "1.2.0"
    URL "https://github.com/yourname/mygraphics/archive/v1.2.0.tar.gz"
    SHA1 "a1b2c3d4e5f6..."  # 压缩包哈希
)

hunter_pick_scheme(DEFAULT url_sha1_cmake)  # 使用标准 CMake 构建方案
hunter_cacheable(mygraphics)
hunter_download(PACKAGE_NAME mygraphics)

此外,你还需要在 Hunter 主仓库的 cmake/configs/default.cmake 中为新包添加默认版本:

hunter_default_version(mygraphics VERSION 1.2.0)

Hunter 的优势在于对消费者极度友好:开发者不需要安装 Python、Node 或任何包管理器二进制,只要有一个能上网的 CMake,就能自动拉取依赖。但其缺点是上游贡献门槛略高,且对新版本的支持速度通常慢于 vcpkg 和 Conan。

三种方案选型建议

至此,我们已经完整走通了 vcpkg、Conan 和 Hunter 的发布流程。下表从库作者和消费者两个维度进行对比,帮助你在实际项目中做出选择:

维度 vcpkg Conan Hunter
生态与仓库 微软官方维护,中心化仓库,社区活跃 ConanCenter + 私有 Artifactory,去中心化 去中心化,依赖社区 PR 合并
消费者体验 需要安装 vcpkg 工具,支持 manifest 模式 需要安装 Conan (Python),功能最丰富 纯 CMake,零额外工具安装
库作者工作 编写 portfile.cmake + vcpkg.json 编写 conanfile.py,逻辑最灵活 向 Hunter 仓库提交 cmake 定义
二进制缓存 支持,通过 GitHub Packages 原生支持,可部署私有缓存服务器 本地缓存于 ~/.hunter
适用场景 开源库、跨平台项目、Visual Studio 用户 企业闭源项目、复杂配置矩阵 CMake 极客、嵌入式、限制外网工具的环境

结语

发布到包管理器是现代 C++ 库从”能用”走向”易用”的关键一步。通过本节的学习,你已经掌握了:

  • 为 vcpkg 编写 vcpkg.jsonportfile.cmake,并处理路径修复和特性开关;
  • 为 Conan 编写 conanfile.py,利用 CMakeToolchainCMakeDeps 实现现代化的 CMake 集成;
  • 在项目中引入 Hunter,以及为 Hunter 生态贡献包定义的流程。

至此,第六章”安装、打包与发布”已全部完结。从 install() 的基础规则,到导出目标与 Config 包,再到 CPack 生成系统安装包,最终融入 vcpkg/Conan/Hunter 包管理生态——你的 CMake 项目现在已经具备了工业级的分发能力。

在下一章中,我们将进入测试与质量保障的领域,学习如何使用 CTest 搭建自动化测试框架、集成代码覆盖率分析与静态检查工具。敬请期待!

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……