版本控制简介
到目前为止,我们一直在使用固定版本的 requires,例如 requires = "zlib/1.2.12"。但有时依赖项会演进,会有新版本发布,使用者希望尽可能轻松地更新到这些新版本。
虽然总是可以通过编辑 conanfiles 并显式更新版本号来实现,但 Conan 中有一些机制允许在不修改配方(recipes)的情况下进行此类更新。
版本范围
requires 可以使用语法 pkgname/[version-range-expression] 来表示对给定包的某个版本范围的依赖。让我们看一个示例。请先克隆源码以重现此项目。你可以在 GitHub 上的 examples2 仓库 中找到它们。
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/consuming_packages/versioning
我们可以看到其中包含
from conan import ConanFile
class CompressorRecipe(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain", "CMakeDeps"
def requirements(self):
self.requires("zlib/[~1.2]")
该 requires 包含了表达式 zlib/[~1.2],这意味着“大约”是 1.2 版本。也就是说,它可以解析为 zlib/1.2.8、zlib/1.2.11 或 zlib/1.2.12,但不会解析为类似 zlib/1.3.0 的版本。在所有匹配的可用版本中,版本范围总是会选择最新的一个。
如果我们执行 conan install,将会看到类似以下内容
$ conan install .
Graph root
conanfile.py: .../conanfile.py
Requirements
zlib/1.2.12#87a7211557b6690ef5bf7fc599dd8349 - Downloaded
Resolved version ranges
zlib/[~1.2]: zlib/1.2.12
如果我们尝试改用 zlib/[<1.2.12],这意味着我们希望使用低于 1.2.12 的版本,但该版本被排除在外,因此满足该范围的最新版本将是 zlib/1.2.11。
$ conan install .
Resolved version ranges
zlib/[<1.2.12]: zlib/1.2.11
这同样适用于其他类型的需求,例如 tool_requires。让我们在配方中添加一个。
from conan import ConanFile
class CompressorRecipe(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain", "CMakeDeps"
def requirements(self):
self.requires("zlib/[~1.2]")
def build_requirements(self):
self.tool_requires("cmake/[>3.10]")
我们看到它解析为了最新的可用 CMake 包,且版本至少为 3.11。
$ conan install .
...
Graph root
conanfile.py: .../conanfile.py
Requirements
zlib/1.2.12#87a7211557b6690ef5bf7fc599dd8349 - Cache
Build requirements
cmake/3.27.9#f305019023c2db74d1001c5afa5cf362 - Downloaded
Resolved version ranges
cmake/[>3.10]: cmake/3.27.9
zlib/[~1.2]: zlib/1.2.12
修订版本(Revisions)
当包的创建者对包的配方或源代码进行了一些更改,但没有升级 version 来反映这些更改时,会发生什么?Conan 有一种跟踪这些修改的内部机制,称为修订版本(revisions)。
配方修订版本是一个哈希值,它与包名和版本一起显示,格式为 pkgname/version#recipe_revision 或 pkgname/version@user/channel#recipe_revision。配方修订版本是配方内容及其源代码的哈希值。因此,如果配方、其关联文件或该配方打包的源代码发生任何更改,都会创建一个新的配方修订版本。
你可以使用 conan list 命令列出已有的修订版本。
$ conan list "zlib/1.2.12#*" -r=conancenter
conancenter
zlib
zlib/1.2.12
revisions
82202701ea360c0863f1db5008067122 (2022-03-29 15:47:45 UTC)
bd533fb124387a214816ab72c8d1df28 (2022-05-09 06:59:58 UTC)
3b9e037ae1c615d045a06c67d88491ae (2022-05-13 13:55:39 UTC)
...
修订版本总是解析为最新的(按创建或上传到服务器的时间顺序)。虽然这不是常见的做法,但也可以在 conanfile 中显式锁定给定的配方修订版本,如下所示:
def requirements(self):
self.requires("zlib/1.2.12#87a7211557b6690ef5bf7fc599dd8349")
然而,这种机制在创建新修订版本时维护和更新可能会很繁琐,因此通常情况下不建议这样做。
锁文件(Lockfiles)
使用版本范围,以及在不升级版本的情况下为给定包创建新修订版本的能力,使得更新变得快速、自动且方便,而无需编辑配方。
但在某些情况下,还需要提供一组不可变且可复现的依赖项。此过程称为“锁定”,实现它的机制就是“锁文件”(lockfile)。锁文件是一个包含固定依赖项列表的文件,指定了确切的版本和修订版本。因此,例如,锁文件永远不会包含带有表达式的版本范围,而只会包含已固定的依赖项。
锁文件可以被视为给定依赖关系图在某个时间点的快照。这样的快照必须是“可实现的”,也就是说,它需要是一个可以从 conanfile 配方中实际复现的状态。而此锁文件可以在稍后的时间点使用,以强制保持相同的状态,即使此时已经有了新创建的包版本。
让我们看看锁文件的实际应用。首先,让我们在示例中将依赖锁定到 zlib/1.2.11。
def requirements(self):
self.requires("zlib/1.2.11")
然后,让我们捕获一个锁文件。
conan lock create .
-------- Computing dependency graph ----------
Graph root
conanfile.py: .../conanfile.py
Requirements
zlib/1.2.11#4524fcdd41f33e8df88ece6e755a5dcc - Cache
Generated lockfile: .../conan.lock
让我们看看锁文件 conan.lock 的内容。
{
"version": "0.5",
"requires": [
"zlib/1.2.11#4524fcdd41f33e8df88ece6e755a5dcc%1650538915.154"
],
"build_requires": [],
"python_requires": []
}
现在,让我们恢复原始的 requires 版本范围。
def requirements(self):
self.requires("zlib/[~1.2]")
并运行 conan install .,它默认会找到 conan.lock,并运行等效的 conan install . --lockfile=conan.lock。
conan install .
Graph root
conanfile.py: .../conanfile.py
Requirements
zlib/1.2.11#4524fcdd41f33e8df88ece6e755a5dcc - Cache
请注意版本范围是如何不再被解析的,它并没有获取 zlib/1.2.12 依赖项(尽管它在允许的范围 zlib/[~1.2] 内),因为 conan.lock 锁文件强制其保持在 zlib/1.2.11 及其确切的修订版本上。