包 ID 模式:控制 ABI 和依赖项的可追溯性
正如我们在之前关于确定性构建的博客文章中解释的那样,无法通过其校验和来识别编译后的 C/C++ 工件,相同的源代码会导致不同的二进制文件,因此我们无法就认证二进制文件达成一致的正确结果。
对于软件在其产品中扮演关键角色的许多行业来说,这是一个大问题:航空、医疗、汽车……几乎任何行业,如果其工件未经通知即可被篡改,都会遇到麻烦。
Conan 不依赖于二进制文件的校验和,而是依赖于包 ID 来识别二进制文件,它是一个唯一的标识符,编码了每个包的设置、选项和要求的信息。我们将解释如何通过了解包 ID 来准确地知道部署了哪些库,甚至用于生成工件的源代码。
包 ID 的工作原理
Conan 会针对以下元素的任何组合计算不同的包 ID
-
设置。根据配方中声明的设置的值,将计算不同的包 ID,因此不同的操作系统、编译器、构建类型……将生成不同的 ID。
-
选项。选项的值也将被添加到生成包 ID 中。例如,同一个库在静态构建和动态链接时将获得不同的包 ID。
-
需求。根据为 Conan 客户端配置的包 ID 模式或为特定需求声明的模式,其所有依赖项的完整 Conan 包引用的不同组件可能会影响消费者的包 ID。它是高度可配置的,从只考虑依赖项名称的模式到包括源代码甚至构建环境中任何更改的其他模式。
非常重要的一点是,传递需求(我的依赖项的依赖项)也包含在内,而不仅仅是直接依赖项(
semver_direct_mode
除外)。
注意。- 仅考虑使用 requires
属性或在 requirements()
方法中声明的依赖项,build_requires
不会影响包 ID。
关于 Conan 包引用的快速提醒
在继续之前,我们需要介绍什么是 Conan 包引用以及它的组件是什么。Conan 包引用是构建产品的唯一标识符,这些产品被捆绑到一个包中,它由以下部分组成
-
Conan 引用。它是配方的标识符,例如
fmt/5.3.0@bincrafters/stable
,它包含配方的名称和版本,以及用户/通道信息:<name>/<version>@<user>/<channel>
。Conan v1.10 引入了配方修订版,这是一种在不触及配方引用主要组件的情况下对配方源代码进行版本控制的方法。这是一个完整 Conan 引用的示例
fmt/5.3.0@bincrafters/stable#500ad2e039e90e5aa50b8ceb6a35a3e1
请注意,Conan 会尽可能地将
<version>
组件解释为 SemVer。 -
包 ID。它是包二进制文件的唯一标识符,我们将在本文档的后续部分详细介绍。
-
包修订版。在 Conan v1.10 中引入,包修订版是包内容的哈希值。如前所述,即使使用相同的环境,相同的源代码通常也会生成不同的二进制文件。
鉴于所有这些组件,完整的 Conan 包引用将包含所有这些信息 <name>/<version>@<user>/<channel>#<rrev>:<pkg_id>#<prev>
,它唯一地标识每个 Conan 包构建。
包 ID 模式的重要性
Conan 可以识别每个单独的包构建,并且所有这些信息都可以传播到消费者的包 ID,但这会导致一个很大的缺点:需求(或传递需求)的任何构建都会修改依赖项图中所有包 ID,这些新的 ID 将没有可用的二进制文件,我们需要编译它们。
在某些情况下,这并不方便,因为它会消耗过多的编译时间,并且如果二进制文件是 ABI 兼容的,我们希望利用可用的二进制文件。但是,在其他情况下,这正是我们想要实现的目标:我们不能冒需求更改头文件而不会增加版本的风险。
包 ID 模式的实用性和重要性就在于此,它们允许配置完整包引用中的哪些组件应该被考虑用于计算消费者的包 ID。
这些模式从 unrelated_mode
(不考虑需求中的任何内容)到 package_revision_mode
(所有内容(包括包修订版)都会修改包 ID)不等。并且中间还有许多其他模式。在所有可能性之间选择正确的模式非常重要
- 宽松模式在编译方面不那么密集,将重用更多二进制文件,包 ID 中将考虑更少的来自需求的信息。将无法知道用于生成包的需求的精确修订版,无法确定它是否包含错误修复甚至上游功能
- 更严格的模式将收集更多来自需求的信息,可以知道构建它们的确切源代码,但任何微小更改都需要一个新的二进制文件,并且 CI 中的编译时间可能会显着增加。
为您的项目选择正确的包 ID 模式是一个重要的决定。您应该仔细考虑依赖项的版本控制方案、CI 时间、系统中源代码更改的关键程度(错误修复可能是重大更改吗?)……所有这些因素都可以使用正确的包 ID 模式进行管理。
这些模式可以为 Conan 客户端配置(我们将在本博文中以这种方式使用它),但是全局行为可以被任何配方的任何单个需求覆盖,使用 package_id
方法(更多信息请参见文档)
Conan 默认行为:semver_direct_mode
默认情况下,Conan 使用 semver_direct_mode
,这意味着只要其直接需求版本的主组件不同,它就会计算不同的包 ID。这是一种相当宽松的方法,带有很大的假设:所有依赖项都正确地使用 SemVer 版本控制方案,我的应用程序对新功能或错误修复不敏感,并且忽略需求的选项或设置中的更改是可以的。虽然它可能适合通用库和开源社区,但可能不是公司软件的最佳方法。
让我们使用下面 conanfile.py
中的示例配方来探讨它
from conans import ConanFile
class Library(ConanFile):
name = "name"
version = "version"
settings = "os", "compiler", "build_type"
options = {"shared": [True, False]}
default_options = {"shared": True}
requires = "fmt/5.3.0@bincrafters/stable"
使用 Conan 客户端,我们可以使用命令conan info计算将要生成的包的包 ID
⇒ conan info . --profile=default
...
conanfile.py (name/version)
ID: f38e4ae2fcc1fd3b6f76fde9093cfce7d4d11f94
...
除了需求之外,获得的 ID 还取决于使用的设置和选项的值,这里我们明确告诉 Conan 使用配置文件 default
。如果您想重现本文中出现的相同包 ID 值,可以使用此配置文件
⇒ conan profile show default
Configuration for profile default:
[settings]
os=Macos
arch=x86_64
compiler=apple-clang
compiler.version=10.0
compiler.libcxx=libc++
build_type=Release
[options]
[build_requires]
[env]
如前所述,使用 semver_direct_mode
模式,只有需求的主组件发生更改才会影响包 ID 值
-
如果我们将
fmt
的版本从5.3.0
更改为5.2.1
,我们将获得与我们的 conanfile 相同的包 ID⇒ conan config set general.default_package_id_mode=semver_direct_mode ⇒ conan info . --only id --profile=default # when we require fmt 5.3.0 fmt/5.3.0@bincrafters/stable ID: 853c4b61e2571e98cd7b854c1cda6bc111b8b32c conanfile.py (name/version) ID: 38dbf89d158028a99d09852abf8b8a82ede43714 # when we require fmt 5.2.1 ⇒ conan info . --only id --profile=default fmt/5.2.1@bincrafters/stable ID: 853c4b61e2571e98cd7b854c1cda6bc111b8b32c conanfile.py (name/version) ID: 38dbf89d158028a99d09852abf8b8a82ede43714
-
我们需要更改主组件(更改为
4.1.0
)才能获得不同的 ID⇒ conan info . --only id --profile=default # when we require fmt 4.1.0 fmt/4.1.0@bincrafters/stable ID: 853c4b61e2571e98cd7b854c1cda6bc111b8b32c conanfile.py (name/version) ID: 19d34f4e911e399b2fb93166523221c5e1f14f06
-
但是影响需求包 ID 的更改不会反映在消费者的包 ID 中
# when we require fmt 4.1.0 (shared=True) ⇒ conan info . --only id --profile=default -o fmt:shared=True fmt/4.1.0@bincrafters/stable ID: 95b87e2c9261497d05b76244c015fbde06fe50b3 conanfile.py (name/version) ID: 19d34f4e911e399b2fb93166523221c5e1f14f06
在上面的输出中显示,只有当需求 fmt
的主组件发生更改时,消费者的配方包 ID 才会更改(从 38dbf89d
更改为 19d34f4e
)(尽管 fmt
的包 ID 相同)。并且如果我们修改 fmt
包的选项,它不会更改,与 fmt
对应的包 ID 会更改,但消费者的配方包 ID 不会更改。
使用 semver_direct_mode
,只要主版本不更改,我们就可以根据需要修改依赖项:我们可以修改选项以激活功能或切换行为,我们可以使用不同的链接选项……这一切都取决于库编写器。在我们的库的相同包 ID 下,有很多自由度。我们将无法区分,因为许多配置会导致相同的包 ID。
其他包 ID 模式
还有许多其他包 ID 模式可以使用(请参见完整列表),这里我们将展示其中的一些
-
full_version_mode
:它将考虑 SemVer 版本的所有组件(在下面的示例中,我们正在修改补丁组件)⇒ conan config set general.default_package_id_mode=full_version_mode ⇒ conan info . --only id --profile=default # when we require fmt 5.2.1 fmt/5.2.1@bincrafters/stable ID: 853c4b61e2571e98cd7b854c1cda6bc111b8b32c conanfile.py (name/version) ID: 840962321acb965eeab4e8507bdb9e85c11a06fd ⇒ conan info . --only id --profile=default # when we require fmt 5.2.0 fmt/5.2.0@bincrafters/stable ID: 853c4b61e2571e98cd7b854c1cda6bc111b8b32c conanfile.py (name/version) ID: 8e9392814f9e6f0132c2e383d60364623ca759b5
-
full_package_mode
:包引用中的任何更改(不包括修订版)都会修改消费者配方的包 ID。让我们看看如何在所需的fmt
配方中修改选项会修改其包 ID,以及为消费者包计算新值⇒ conan config set general.default_package_id_mode=full_package_mode ⇒ conan info . --only id --profile=default -o fmt:shared=False # when we require fmt 5.2.0 (shared=False) fmt/5.2.0@bincrafters/stable ID: 853c4b61e2571e98cd7b854c1cda6bc111b8b32c conanfile.py (name/version) ID: 50fb56084639e9d7f970e1c79e36f53b452eb552 ⇒ conan info . --only id --profile=default -o fmt:shared=True # with the same fmt 5.2.0, but changing option value (shared=True) fmt/5.2.0@bincrafters/stable ID: 95b87e2c9261497d05b76244c015fbde06fe50b3 conanfile.py (name/version) ID: 159983fa331b57530730eaf05aedeb3628307264
在您的机器上尝试其他模式,更改需求的版本,并查看消费者的配方包 ID 如何变化。所有这些模式都提供了高级别的自定义,允许对包 ID 进行细粒度的控制。
需求的 Conan 引用的最后两个字段 user
和 channel
对大多数包 ID 模式没有影响,只有 full_recipe_mode
和 full_package_mode
(以及我们将要讨论的修订版模式)会考虑它们。
使用修订版
Conan v1.10.0 引入了配方和包的修订版,尽管该功能是实验性的,但我们非常确定它会继续存在并很快稳定下来。配方修订版 (<rrev>
) 提供了一种在不更改配方本身版本的情况下对配方源代码进行版本控制的方法,而包修订版 (<prev>
) 是一种区分使用完全相同的配方源代码构建的二进制文件的方法(请参见可重复构建)。
在 Conan v1.17.0 中,向可用的包 ID 模式列表中添加了两种新模式,以选择性地考虑需求的完整 Conan 引用 (<ref>#<rrev>:<pkg_id>#<prev>
) 的这些组件。这些模式是
recipe_revision_mode
:它类似于full_package_mode
,但它也考虑了配方修订版。package_revision_mode
:此外,它还考虑了包修订版。
使用这些模式,Conan 会为任何需求变更计算一个新的包 ID,通常情况下这比需要的要多,但这是确保二进制可追溯性和可重复性的最安全方式:只有相同的一组需求以相同的方式配置才能生成相同的包 ID,并且在 package_revision_mode
模式下,只有使用相同的实际二进制文件才能生成相同的包 ID。
recipe_revision_mode
要运行以下示例,我们需要激活修订版并使用需求的不同修订版。请注意,Conan 缓存一次只会存储一个修订版,如果要持久化它们,则需要使用一个 Artifactory 服务器(免费下载 JFrog Artifactory 社区版 for C/C++)或 Bintray。请按照以下步骤操作 recipe_revision_mode
-
为本示例配置 Conan
⇒ conan config set general.revisions_enabled=1 ⇒ conan config set general.default_package_id_mode=recipe_revision_mode
-
检查使用
fmt
需求的一个修订版生成的 ID⇒ git clone https://github.com/bincrafters/conan-fmt.git ⇒ cd conan-fmt ⇒ git checkout 7d9dce3 ⇒ conan export . bincrafters/stable ... fmt/5.3.0@bincrafters/stable: Exported revision: 500ad2e039e90e5aa50b8ceb6a35a3e1
我们可以要求 Conan 计算我们消费者配方(consumer recipe)的包 ID,它将使用我们刚刚导出的
fmt
库的配方。⇒ conan info . --only id --profile=default fmt/5.3.0@bincrafters/stable ID: 853c4b61e2571e98cd7b854c1cda6bc111b8b32c conanfile.py (name/version) ID: 46516d5f2debf0f4b7e55da9e75bfe277d26a1fc
-
我们可以修改
fmt
的配方以生成不同的修订版,并将其导出到 Conan 缓存(它将覆盖现有的,因为缓存中一次只能存在一个修订版)。⇒ git clone https://github.com/bincrafters/conan-fmt.git ⇒ cd conan-fmt ⇒ git checkout 7d9dce3 ⇒ echo "# Add a comment at the end of the file" >> conanfile.py ⇒ conan export . bincrafters/stable ... fmt/5.3.0@bincrafters/stable: Exported revision: 30bb32c064e1c43b70d5cb9e2749e484
如果我们计算消费者配方的包 ID,现在它是一个不同的 ID,并且只有
fmt
包的配方修订版发生了更改。⇒ conan info . --only id --profile=default fmt/5.3.0@bincrafters/stable ID: 853c4b61e2571e98cd7b854c1cda6bc111b8b32c conanfile.py (name/version) ID: 859c7995b3e1554bd4a456aee82a45f0c6ade2f7
package_revision_mode
作为与修订版相关的最后一部分,如果我们尝试使用 package_revision_mode
计算我们配方的包 ID,Conan 也会考虑需求的包修订版。让我们看看如果 fmt
配方的二进制文件不可用会发生什么。
⇒ conan remove fmt/5.3.0@bincrafters/stable -p
⇒ conan config set general.default_package_id_mode=package_revision_mode
⇒ conan info . --only id --profile=default
fmt/5.3.0@bincrafters/stable
ID: 853c4b61e2571e98cd7b854c1cda6bc111b8b32c
conanfile.py (name/version)
ID: Package_ID_unknown
在启用此模式的情况下,Conan 无法计算包 ID,因为它无法知道如果 fmt
包可用,它将使用的包修订版。一旦我们编译了二进制文件,Conan 将能够计算包 ID。
⇒ conan install fmt/5.3.0@bincrafters/stable --profile=default --build fmt
⇒ conan config set general.default_package_id_mode=package_revision_mode
⇒ conan info . --only id --profile=default
fmt/5.3.0@bincrafters/stable
ID: 853c4b61e2571e98cd7b854c1cda6bc111b8b32c
conanfile.py (name/version)
ID: b2110045f8b2598a521adad9753eb610ba4059ee
请记住,我们消费者配方的包 ID 考虑了 fmt
的包修订版,该修订版是使用生成的二进制文件的校验和计算的,因此每次编译都会为该需求获得一个不同的包修订版,并且计算出的包 ID 将不同。这就是为什么你无法获得最后一个示例的相同值的原因,我们也无法获得,每次编译 fmt
库都会有一个新的值。
package_revision_mode
保证是精确的:只有使用相同的库集和构建二进制文件时使用的相同依赖关系图,才能复制包 ID。结论
Conan 包 ID 模式允许精细控制选择依赖项如何影响消费者库的包 ID,如果使用 Conan 包部署应用程序并跟踪这些标识符,则可以控制任何版本中包含哪些库以及它们是如何编译和配置的。
更敏感的软件应该使用更严格的模式,而社区通常会使用宽松的模式,但使用 Conan,可以轻松更改模式,就像我们在帖子中看到的那样,它只是 Conan 设置中的一个值。
通过充分了解包 ID 模式和一些强大的功能,例如 锁定文件,可以设置一个健壮的 CI 机制来协调在任何依赖项发生更改时构建的库。Conan 将知道哪些二进制文件可以重用,哪些需要重新构建,它还会告诉您构建顺序以及哪些库可以并行构建。所有这些信息对于优化构建时间并帮助您加快发布周期都非常有用。