依赖冲突¶
在依赖关系图中,当不同的包依赖于同一个包的不同版本时,这被称为依赖版本冲突。 产生冲突相对容易。 让我们通过一个实际的例子来看看,首先克隆 examples2 仓库
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/versioning/conflicts/versions
在这个文件夹中,我们有一个小型项目,由几个包组成: matrix
(一个数学库),engine/1.0
视频游戏引擎,它依赖于 matrix/1.0
,intro/1.0
,一个为视频游戏实现介绍片头和功能的包,它依赖于 matrix/1.1
,最后是 game
recipe,它同时依赖于 engine/1.0
和 intro/1.0
。 所有这些包实际上都是空的,但它们足以产生冲突。
让我们创建依赖关系
$ conan create matrix --version=1.0
$ conan create matrix --version=1.1 # note this is 1.1!
$ conan create engine --version=1.0 # depends on matrix/1.0
$ conan create intro --version=1.0 # depends on matrix/1.1
当我们尝试安装 game
时,我们会收到错误
$ conan install game
Requirements
engine/1.0#0fe4e6890766f7b8e21f764f0049aec7 - Cache
intro/1.0#d639998c2e55cf36d261ab319801c322 - Cache
matrix/1.0#905c3f0babc520684c84127378fefdd0 - Cache
Graph error
Version conflict: intro/1.0->matrix/1.1, game/1.0->matrix/1.0.
ERROR: Version conflict: intro/1.0->matrix/1.1, game/1.0->matrix/1.0.
这是一个版本冲突,Conan 不会自动决定如何解决冲突,但用户应显式解决此类冲突。
解决冲突¶
当然,解决此类冲突最直接和最简单的方法是转到依赖项 conanfile.py
并升级它们的 requirements()
,以便它们现在指向相同的版本。 但是,在某些情况下,这可能不切实际,甚至可能无法修复依赖项 conanfile。
对于这种情况,应该是消费 conanfile.py
的一方来解决冲突(在本例中为 game
),方法是使用以下语法显式定义应使用的依赖项版本
class Game(ConanFile):
name = "game"
version = "1.0"
def requirements(self):
self.requires("engine/1.0")
self.requires("intro/1.0")
self.requires("matrix/1.1", override=True)
这被称为 override
(覆盖)。 game
包不直接依赖于 matrix
,此 requires
声明不会引入这样的直接依赖关系。 但是 matrix/1.1
版本将在依赖关系图中向上游传播,覆盖依赖于任何 matrix
版本的包的 requires
,从而强制图的一致性,因为所有上游包现在都将依赖于 matrix/1.1
$ conan install game
...
Requirements
engine/1.0#0fe4e6890766f7b8e21f764f0049aec7 - Cache
intro/1.0#d639998c2e55cf36d261ab319801c322 - Cache
matrix/1.1#905c3f0babc520684c84127378fefdd0 - Cache
注意
在这种情况下,engine/1.0
的新二进制文件不是必需的,但在某些情况下,上述操作可能会失败,并出现 engine/1.0
“二进制文件丢失错误”。 因为以前的 engine/1.0
二进制文件是针对 matrix/1.0
构建的。 如果 package_id
规则和配置定义在依赖项的次要版本更改时应重建 engine
,那么将需要为 engine/1.0
构建新的二进制文件,该二进制文件针对新的 matrix/1.1
依赖项进行构建和链接。
如果 game
直接依赖于 matrix/1.2
会发生什么? 让我们创建版本
$ conan create matrix --version=1.2
现在让我们修改 game/conanfile.py
以将其作为直接依赖项引入
class Game(ConanFile):
name = "game"
version = "1.0"
def requirements(self):
self.requires("engine/1.0")
self.requires("intro/1.0")
self.requires("matrix/1.2")
因此,安装它将再次引发冲突错误
$ conan install game
...
ERROR: Version conflict: engine/1.0->matrix/1.0, game/1.0->matrix/1.2.
因为这次,我们要尊重 game
和 matrix
之间的直接依赖关系,我们将定义 force=True
requirement trait,以表明此依赖项版本也将强制上游的覆盖
class Game(ConanFile):
name = "game"
version = "1.0"
def requirements(self):
self.requires("engine/1.0")
self.requires("intro/1.0")
self.requires("matrix/1.2", force=True)
这将再次解决冲突(如上所述,请注意,在实际应用中,这可能意味着 engine/1.0
和 intro/1.0
的二进制文件将丢失,并且需要构建以链接到新的强制 matrix/1.2
版本)
$ conan install game
Requirements
engine/1.0#0fe4e6890766f7b8e21f764f0049aec7 - Cache
intro/1.0#d639998c2e55cf36d261ab319801c322 - Cache
matrix/1.2#905c3f0babc520684c84127378fefdd0 - Cache
注意
最佳实践
通常应尽可能避免通过覆盖/强制解决版本冲突,而应将其用作临时解决方法。 真正的解决方案是向前移动依赖项
requires
,以便它们自然地收敛到上游依赖项的相同版本。一个关键的要点是
force
trait 将在消费者和所需的包之间创建直接依赖关系,而override
则不会,它只会指示 Conan 在包已在依赖关系图中时优先选择所需的版本。版本范围也可能产生一些版本冲突,即使 Conan 尝试减少它们。 此 关于版本冲突的常见问题解答 讨论了图解析算法和最大程度减少冲突的策略。
覆盖选项¶
当依赖关系图中存在菱形结构(如上面看到的结构)时,不同的 recipe 可能会为上游 options
定义不同的值。 在这种情况下,这不会直接导致冲突,而是将优先级设为首先定义的值,并且该值将占主导地位。
在上面的示例中,如果 matrix/1.0
可以是静态库和共享库,并且 engine
决定将其定义为静态库(实际上没有必要,因为这已经是默认值)
class Engine(ConanFile):
name = "engine"
version = "1.0"
# Not strictly necessary because this is already the matrix default
default_options = {"matrix*:shared": False}
警告
在 recipe 中定义选项值没有强保证,请查看 有关依赖项选项值的常见问题解答。 定义选项值的推荐方法是在 profile 文件中。
并且 intro
recipe 也会做同样的事情,但改为定义它想要一个共享库,并添加一个 validate()
方法,因为由于某种原因,intro
包只能针对共享库构建,否则会崩溃
class Intro(ConanFile):
name = "intro"
version = "1.0"
default_options = {"matrix*:shared": True}
def requirements(self):
self.requires("matrix/1.0")
def validate(self):
if not self.dependencies["matrix"].options.shared:
raise ConanInvalidConfiguration("Intro package doesn't work with static matrix library")
然后,这将导致错误,因为第一个定义选项值的是 engine
(它在 game
conanfile 的 requirements()
方法中首先声明)。 在 examples2 仓库中,转到 “options” 文件夹,并创建不同的包
$ cd ../options
$ conan create matrix
$ conan create matrix -o matrix/*:shared=True
$ conan create engine
$ conan create intro
$ conan install game # FAILS!
...
-------- Installing (downloading, building) binaries... --------
ERROR: There are invalid packages (packages that cannot exist for this configuration):
intro/1.0: Invalid: Intro package doesn't work with static matrix library
遵循相同的原则,下游消费者 recipe,在本例中为 game
conanfile.py 可以定义选项值,这些值将具有更高的优先级
class Game(ConanFile):
name = "game"
version = "1.0"
default_options = {"matrix*:shared": True}
def requirements(self):
self.requires("engine/1.0")
self.requires("intro/1.0")
这将强制 matrix
成为共享库,无论 engine
是否定义了 shared=False
,因为下游消费者始终优先于上游依赖项。
$ conan install game
...
-------- Installing (downloading, building) binaries... --------
matrix/1.0: Already installed!
matrix/1.0: I am a shared-library library!!!
engine/1.0: Already installed!
intro/1.0: Already installed!
注意
最佳实践
作为一般规则,避免在消费者 conanfile.py
中修改或定义依赖项 options
的值。 声明的 options
默认值应该适用于大多数情况,并且可以在 profile 中更好地定义与这些默认值的偏差。