跳转至

CPMUtil

CPMUtil 是 CPM 的一个封装(wrapper),旨在减少样板代码,并添加有用的实用函数,让依赖管理变得轻而易举。

全局选项

  • CPMUTIL_FORCE_SYSTEM(默认 OFF):要求所有 CPM 依赖项使用系统包。
  • 你可以为每个包单独覆盖此选项:包特定覆盖
  • CPMUTIL_FORCE_BUNDLED(默认在 MSVC 和 Android 上为 ON,其他平台为 OFF):要求所有 CPM 依赖项使用捆绑包。
  • 你可以为每个包单独覆盖此选项:包特定覆盖
  • CPMUTIL_PATCH_DIR(默认 ${PROJECT_SOURCE_DIR}/.patch):包中使用的补丁所在路径。存储为 <PATCH DIR>/json-package-name/0001-patch-name.patch 等。
  • CPM_SOURCE_CACHE(默认 ${PROJECT_SOURCE_DIR}/.cache/cpm):已下载依赖项的存储位置。

关于

CPMUtil 通过在 JSON 文件 cpmfile.json 中定义依赖项,并调用 AddJsonPackage 来工作。这些依赖项通常至少必须定义:

  • 仓库和 Git 托管平台
  • 要下载的发布产物、提交或标签归档
  • 下载产物的 SHA512 校验和

并且可以可选地定义其他属性,例如:

  • 系统包的最低版本
  • 系统包使用的包名(若未定义,则默认为 JSON 键名)
  • 树内源代码补丁
  • 传递给 CMake 的选项
  • 传递给 find_package 的选项

例如:

{
  "fmt": {
      "repo": "fmtlib/fmt",
      "version": "12.1.0",
      "hash": "f0da8...23f2",
      "min_version": "8",
      "options": [
          "FMT_TEST ON"
      ],
      "patches": [
         "0001-disable-reference-copy.patch"
      ]
  }
}

调用 AddJsonPackage(fmt):

  • 搜索名为 fmt 且版本为 8 或更高的系统包(find_package(fmt 8))
  • 如果找到,则使用系统包,并将其缓存以供将来使用
  • 如果未找到:
  • 从 GitHub Archive 下载 fmt 12.1.0 到 .cache/cpm/fmt/12.1.0
  • 验证哈希值
  • 将 0001-disable-reference-copy.patch 补丁应用到源代码树
  • 将 FMT_TEST 设置为 ON
  • 将下载的目录添加到 CMake
  • 现在,将来的 find_package(fmt) 调用将使用下载的包

CPMUtil 可以定义两种类型的包:标准包和预构建的 CI 包。不过,有些属性对这两种类型都是通用的。

通用属性

以下 JSON 属性同时用于标准包和 CI 包。

  • package:find_package 用于检查系统包是否存在的包名。
  • 如果未设置,默认为 JSON 键名
  • repo:包所在的 Git 仓库(如适用)。
  • version:包的版本。此项必填。
  • 通常应为 10 位 Git 提交哈希或 Git 标签。
  • 标签必须是完整限定的,并包含前缀和后缀,例如 boost-1.88.0
  • git_host:包所在的 Git 托管平台(如适用)。默认值为 github.com。

标准包

普通包(如前面的 fmt 示例)必须还要定义:

  • hash:下载产物的 SHA512 哈希值。CPMUtil 通常会为你计算此值;如果没有,请使用 tools/cpmutil.sh package hash <JSON key>
  • min_version:如果希望使用系统包,此项为所需的最低版本。

还可以选择定义:

  • url:从原始 URL 下载。
  • artifact:GitHub/Forgejo/Gitea 发布产物。需要设置有效的 repo。
  • numeric_version:替换产物和版本定义中的 %NUMERIC_VERSION%;参见 产物命名勘误。

以下为可选的额外定义:

  • source_subdir:包含 CMakeLists.txt 的子目录,用于配置项目。适用于 zstd 这类项目。
  • bundled:强制使用捆绑包。适用于系统包损坏或不存在的包;例如外部片段着色器或数据归档。
  • 请注意,这会与 CPMUTIL_FORCE_SYSTEM 产生冲突;因此,在使用非库归档时,最好的做法可能是允许用户手动下载并解压归档,然后指定一个本地目录。
  • find_args:传递给 find_package 的额外参数,例如 MODULE
  • patches:要应用于下载源代码的树内补丁数组。参见 #补丁。
  • options:在配置包之前应用的 CMake 选项数组,例如 "FMT_TEST ON"。

版本管理

从 Git 仓库获取时,通常有三种版本管理方式:

  • 提交哈希
  • Git 标签
  • 发布产物

当设置了 repo 时,version 字段可以设置为任何 commitish 值,包括提交哈希或 Git 标签。对于产物而言,version 必须是 Git 标签,并且 artifact 必须设置为附加到该标签的发布产物。许多仓库会特意为其发布产物的文件名设置版本号;为此,CPMUtil 允许你将版本号植入产物名称中。要做到这一点,请在产物名称中添加 %VERSION%;CPMUtil 随后会自动用 version 字段替换 %VERSION%。这意味着在更改版本时,你只需更新版本字段,而无需更新产物!

产物命名勘误

补丁

预构建的 CI 包

用法

附录:缓存存储

附录:制作补丁

附录:包识别列表

附录:打包者须知

网络沙箱

非沙箱模式

附录:依赖包

示例:Vulkan

附录:模块路径包

示例:OpenSSL

附录:添加 Qt

附录:包特定覆盖

附录:供应链安全

校验和计算

缓存

不可变提交哈希

以 Boost 为例。Boost 1.90.0 的构建产物是 boost-1.90.0-cmake.tar.xz,对应的标签是 boost-1.90.0。因此,我们可以将构建产物设置为 %VERSION%-cmake.tar.xz:

"boost": {
    "repo": "boostorg/boost",
    "version": "boost-1.90.0",
    "artifact": "%VERSION%-cmake.tar.xz"
}

该构建产物随后会解析为 boost-1.90.0-cmake.tar.xz。

构建产物命名勘误

虽然 %VERSION% 替换对于打包良好的项目通常已经足够,但偶尔也会遇到一些有问题的包。以 Vulkan Validation Layers 为例:

  • 标签(version):vulkan-sdk-1.4.341.0
  • 构建产物:android-binaries-1.4.341.0.zip

尝试在构建产物定义中加入版本替换在这里是行不通的! 在这种情况下,你必须使用前面描述的 numeric_version 字段;我们可以将 numeric_version 设置为 1.4.341.0,并在构建产物和版本字段中加入 %NUMERIC_VERSION% 替换:

"vulkan-validation-layers": {
    "artifact": "android-binaries-%NUMERIC_VERSION%.zip",
    "repo": "KhronosGroup/Vulkan-ValidationLayers",
    "version": "vulkan-sdk-%NUMERIC_VERSION%",
    "numeric_version": "1.4.341.0"
},

因此,artifact 会解析为 android-binaries-1.4.341.0.zip,version 会解析为 vulkan-sdk-1.4.341.0。CPMUtil 的自动更新器也会考虑到这一点,并且只更新 numeric_version!

补丁

CPMUtil 能够对下载的包就地应用源码树补丁。这些补丁在 JSON 中定义为名称数组,最好采用 git-format-patch 的 <4 digit number>-patch-name.patch 命名方案。

它们存储在 <CPMUTIL_PATCH_DIR>/<json-key> 中(请记住,CPMUTIL_PATCH_DIR 默认为 $ROOT/.patch);例如,boost 的补丁会位于 .patch/boost。假设我们制作了三个补丁并希望添加它们;在 Boost 的 JSON 定义中,我们会添加:

"patches": [
  "0001-fix-clang-cl-compilation.patch",
  "0002-fix-msvc-arm64-compilation.patch",
  "0003-fix-bsd-linking.patch"
]

然后,当 Boost 被下载时,CPMUtil 会按照定义顺序将这些补丁应用到源码树中(复合/依赖补丁也是可以的!)。请注意,当你添加、移除或修改补丁时,CPMUtil 会使已下载的缓存失效并重新获取源码。

要了解如何制作补丁,请参阅 附录:制作补丁。

预构建 CI 包

CI 包的定义和用法在不久的将来可能会发生变化。

CI 包本质上是对库的预构建二进制发行版。它们存在的原因有几点:

  • 为通常缺少静态库的系统包创建静态库,例如 Qt/SDL
  • 减少对很少变化的外部依赖项的重复编译工作,例如 SDL
  • 专门为你的项目创建精简后的预构建包,以减小二进制体积,例如 FFmpeg

CPMUtil 专门设计用于处理一小部分预构建 CI 包;具体来说,就是那些遵循 crueter-ci 规范 格式的包。

要使用它们,你必须在包定义中添加 ci: true。除了通用属性外,CI 包还定义以下内容:

  • name:构建产物的名称前缀(必填),例如 openssl
  • extension:构建产物的扩展名(默认为 tar.zst)
  • disabled_platforms:此仓库中未构建的受 CPMUtil 支持的平台。
  • 这一点可能会发生变化。

请注意,package 在这里可能会被移除。

示例:

"sdl2": {
    "repo": "crueter-ci/SDL2",
    "package": "SDL2",
    "min_version": "2.26.4",
    "ci": true,
    "version": "2.32.10-a65111bd2d",
    "artifact": "SDL2"
},

用法

一旦你在 cpmfile.json 中定义好包,只需调用 AddJsonPackage(<JSON key>),然后就可以继续了!具体说明因包而异,因此从这里开始就靠你自己了。

如果你只关心基本用法,可以停止阅读。对于更高级的用例和包管理,请阅读这些附录。

附录:缓存存储

默认情况下,CPMUtil 将下载的包存储在 .cache/cpm 中(参见 CPM_SOURCE_CACHE)。其中存储的子目录是包的 find_package 名称的小写表示;例如,一个 package: "VulkanHeaders" 的 vulkan-headers 定义将存储在 .cache/cpm/vulkanheaders。

在这些子目录中,会为每个单独的版本创建额外的目录,它们直接对应于各自的 version 字段。CI 包无条件使用 <platform>-<architecture>-<version>。

要查看给定包的缓存目录,请使用 tools/cpmutil.sh package dir <JSON key>。

附录:制作补丁

CPMUtil 有一个专门用于制作补丁的命令。建议你安装 Git 和命令行编辑器,但 CPMUtil 在两者都没有的情况下也能工作。要制作补丁,请遵循以下步骤,并注意你包的 JSON 键:

  • 清理并重新获取你的包:tools/cpmutil.sh package reset <package>
  • 对包源码进行任何必要的修改。
  • 你可以通过 tools/cpmutil.sh package dir <package> 访问包源码目录。
  • 创建补丁:tools/cpmutil.sh package patch <package>
  • 按照屏幕上的提示操作。如果你安装了 Git,会打开一个编辑器,让你输入提交信息。如果没有安装,只需输入一行描述。

就这样完成了!CPMUtil 会自动创建并命名补丁,并将其添加到 JSON 定义中的补丁列表中。

附录:包标识列表

CPMUtil 会创建三个依赖列表,这些列表在使用 AddPackage 或类似方法的地方生成。每个列表都按添加顺序排列。

  • CPM_PACKAGE_NAMES:由 CPMUtil 包含的包名称
  • CPM_PACKAGE_URLS:包的项目/仓库页面 URL
  • CPM_PACKAGE_SHAS:每个包的短版本标识符
  • 如果该包是作为系统包包含的,则会在其后附加 (system)
  • 无法推断版本的包将保留为 unknown。

若要查看如何在应用程序中实现此功能的示例,请参阅 Eden 的实现:

附录:打包者须知

如果你正在打包一个使用 CPMUtil 的项目,请务必阅读本节!

网络沙盒

对于沙盒环境(例如 Gentoo、nixOS),你必须事先将所有依赖安装到系统中,并设置 -DCPMUTIL_FORCE_SYSTEM=ON。如果缺少某个依赖,那就动手创建它!

或者,如果 CPMUtil 拉取的某个包没有合适的方式安装或使用系统版本,请单独下载它,并传入 -D<PackageName>_CUSTOM_DIR=/path/to/downloaded/dir。

非沙盒环境

对于其他环境(AUR、MPR 等),CPMUtil 会为你处理一切,包括当项目的某些依赖在你发行版的仓库中缺失时。参见 eden-git 中的示例。

附录:依赖包

考虑以下场景:我的项目同时拉取了 Vulkan Headers 和 Vulkan Utility Libraries。为了让两者都能干净地编译,它们的版本必须匹配。然而,用户可能已安装 Vulkan Headers,但没有安装 Vulkan Utility Libraries!这可能导致版本不匹配——Utility Libraries 所期望的 Vulkan Headers 版本远高于用户已安装的版本。

为了解决这个问题,CPMUtil 提供了一个 AddDependentPackages 命令。它接受一个 JSON 包键列表,这些键所对应的包必须全部安装到系统中,或者全部捆绑在一起。

示例:Vulkan

使用前面提到的 Vulkan 示例:

"vulkan-headers": {
    "repo": "KhronosGroup/Vulkan-Headers",
    "package": "VulkanHeaders",
    "min_version": "1.4.317",
    "version": "v1.4.342"
},
"vulkan-utility-libraries": {
    "repo": "KhronosGroup/Vulkan-Utility-Libraries",
    "package": "VulkanUtilityLibraries",
    "version": "v1.4.342"
}

在 CMake 中:

AddDependentPackages(vulkan-headers vulkan-utility-libraries)

可能的情况:

  • 用户已同时安装 Vulkan Headers 和 Vulkan Utility Libraries,且两者版本都足够新。
  • 配置过程无任何问题。
  • 用户两者均未安装,或已安装的 Vulkan Headers 版本过旧。
  • 配置过程无任何问题。
  • 用户已安装有效的 Vulkan Headers,但没有安装 Vulkan Utility Libraries。
  • CPMUtil 会提示用户要么强制捆绑 Vulkan Headers,要么安装 Vulkan Utility Libraries。
  • 用户两者都已安装,但 Vulkan Headers 过旧。
  • CPMUtil 会提示用户安装有效版本的 Vulkan Headers,或强制捆绑 Vulkan Utility Libraries。

附录:模块路径包

有时候,预构建的 CI 包可能以这样的方式打包:它被设计用于系统安装的上下文中(例如 pkgconfig 或 CMakeConfig 文件)。在这种情况下,CPMUtil 通常无法配置下载的子目录。为了解决这个问题,你可以使用 AddJsonPackage 的 MODULE_PATH 模式,它会将下载的源码目录添加到 CMAKE_MODULE_PATH 中。

示例:OpenSSL

假设某个 OpenSSL CI 包被打包为包含其 CMake 配置文件,而不是根目录下的 CMakeLists.txt;在这种情况下,你应该调用:

AddJsonPackage(NAME openssl MODULE_PATH)

此时还需要 NAME 参数,因为其解析方式与标准的单参数函数签名不同。

从这里开始,调用 find_package(OpenSSL) 将使用捆绑的 OpenSSL。

附录:添加 Qt

如果你想使用自定义的 Qt 构建,CPMUtil 提供了一个便捷函数,允许你添加 Qt 构建。此用法和设置可能会有变更。

关于如何构建自定义 Qt 的示例,请参见 crueter-ci/Qt。要向你的项目添加 Qt 构建,请使用 AddQt(<repository> <version>),例如:

AddQt(QDash-CI/Qt 6.11.1)

然后,调用 find_package(Qt6 ...),它将从你下载的源码中拉取 Qt。

附录:包特定覆盖项

CPMUtil 为每个包定义了三个变量;这些变量可以由用户或在你的 CMake 中覆盖。package 既指 JSON 中的 package 值,也可以在该值未设置时指包的 JSON 键(参见 通用属性 中的 package):

  • <package>_FORCE_BUNDLED:强制捆绑该包。这与 CPMUTIL_FORCE_BUNDLED 效果相同,但仅对该包生效。
  • <package>_FORCE_BUNDLED:强制使用系统包,如果找不到则失败。这与 SYSTEM 效果相同,但仅对该包生效。
  • <package>_CUSTOM_DIR:指向该包已解压副本的路径。CPMUtil 不会尝试下载该包,而是使用自定义目录。
  • 示例请参见 CPMUtil 的测试用例

此外,在 CMake 中,你可以向 AddJsonPackage 命令添加 FORCE_BUNDLED_PACKAGE ON——注意你需要使用 模块路径包 中所描述的 NAME <key> 语法。这将覆盖所有其他覆盖项,包括 CPMUTIL_FORCE_SYSTEM 和 <package>_FORCE_SYSTEM——请谨慎使用!

附录:供应链安全

许多包管理器都面临供应链安全问题,尤其是对现有包或归档的静默覆盖,例如标签滑动(tag sliding)和构建产物覆盖(artifact overwriting)。CPMUtil 有三种方法可以防范这种情况。

校验和

CPMUtil 要求标准包提供 SHA512 校验和,并且很快也会要求 CI 包提供。如果攻击者或被入侵的账户滑动标签、覆盖发布产物,或以其他方式试图破坏 CPMUtil 可能获取的任何内容,CPMUtil 将不允许下载继续! 这意味着你构建系统的使用者只有在构建产物的内容与配置时完全一致的情况下才能下载该产物——任何改动都会被 CPMUtil 拒绝。

缓存

CPMUtil 使用一套可变缓存系统,默认存储在 .cache/cpm 中。依赖会被下载并解压到这里,而且可以无限复用。这意味着,即使某个软件包被入侵,只要你本地已有缓存副本,就完全不必担心!

不可变提交哈希

CPMUtil 能够为其制品使用不可变的 Git 提交哈希。这些哈希(除非发生 SHA1 碰撞)对供应链攻击完全免疫——除非整个根服务器被入侵,进而提供被感染的制品或源代码;如果真的发生这种情况,那就需要担心更大的问题了。这意味着,一旦你将某个软件包的版本设置为 Git 提交哈希,它将永远保持不变。如果你想确保使用者永远不会因为制品被入侵而遭遇哈希不匹配所导致的下载失败,这一点会非常有用。

不过请注意,这会使该软件包与 CPMUtil 内置的自动更新器不兼容,因此你将需要手动更新该软件包。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 eg/Eglantine