依赖冲突

在依赖关系图中,当不同的软件包依赖于同一软件包的不同版本时,这称为依赖版本冲突。产生这种情况相对容易。我们通过一个实际例子来看看,首先克隆 examples2 仓库

$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/versioning/conflicts/versions

在此文件夹中,我们有一个小型项目,包含几个软件包:matrix(一个数学库),engine/1.0 视频游戏引擎,它依赖于 matrix/1.0intro/1.0,一个为视频游戏实现开场片头和功能的软件包,它依赖于 matrix/1.1,最后是同时依赖于 engine/1.0intro/1.0game 配方。所有这些软件包实际上都是空的,但足以产生冲突。

digraph conflict { node [fillcolor="lightskyblue", style=filled, shape=box] rankdir="BT" "game/1.0" -> "engine/1.0" -> "matrix/1.0"; "game/1.0" -> "intro/1.0" -> "matrix/1.1"; "matrix/1.0" [fillcolor="orange"]; "matrix/1.1" [fillcolor="orange"]; }


让我们创建依赖关系

$ 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)通过明确定义应使用哪个版本的依赖项来解决冲突,语法如下

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.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

digraph conflict { node [fillcolor="lightskyblue", style=filled, shape=box] rankdir="BT" "game/1.0" -> "engine/1.0" -> "matrix/1.1"; "game/1.0" -> "intro/1.0" -> "matrix/1.1"; { rank = same; edge[ style=invis]; "matrix/1.1" -> "matrix/1.0" ; rankdir = LR; } }


注意

在这种情况下,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 以将其作为直接依赖项引入

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")

digraph conflict { node [fillcolor="lightskyblue", style=filled, shape=box] rankdir="BT" "game/1.0" -> "engine/1.0" -> "matrix/1.0"; "game/1.0" -> "intro/1.0" -> "matrix/1.1"; "game/1.0" -> "matrix/1.2"; "matrix/1.0" [fillcolor="orange"]; "matrix/1.1" [fillcolor="orange"]; "matrix/1.2" [fillcolor="orange"]; { rank = same; edge[ style=invis]; "matrix/1.1" -> "matrix/1.2" ; rankdir = LR; } }


因此安装它将再次引发冲突错误

$ conan install game
...
ERROR: Version conflict: engine/1.0->matrix/1.0, game/1.0->matrix/1.2.

由于这次我们希望尊重 gamematrix 之间的直接依赖关系,我们将定义 force=True 需求特性,以表明此依赖版本也将强制覆盖上游依赖

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", force=True)

这将再次解决冲突(如上面所评论的,请注意在实际应用中这可能意味着 engine/1.0intro/1.0 的二进制文件会丢失,需要重新构建以链接新的强制 matrix/1.2 版本)

$ conan install game
Requirements
    engine/1.0#0fe4e6890766f7b8e21f764f0049aec7 - Cache
    intro/1.0#d639998c2e55cf36d261ab319801c322 - Cache
    matrix/1.2#905c3f0babc520684c84127378fefdd0 - Cache

digraph conflict { node [fillcolor="lightskyblue", style=filled, shape=box] rankdir="BT" "game/1.0" -> "engine/1.0" -> "matrix/1.2"; "game/1.0" -> "intro/1.0" -> "matrix/1.2"; "game/1.0" -> "matrix/1.2"; { rank = same; edge[ style=invis]; "matrix/1.2" -> "matrix/1.0" -> "matrix/1.1" ; rankdir = LR; } }


注意

最佳实践

  • 通过覆盖/强制解决版本冲突通常应该是例外情况,并且应尽可能避免,作为临时解决方案应用。真正的解决方案是推进依赖项的 requires,使其自然收敛到上游依赖项的相同版本。

  • 一个关键的要点是,force 特性会在消费者和所需的软件包之间创建一个直接依赖关系,而 override 则不会,它只会指示 Conan 在软件包已存在于依赖关系图中时优先选择所需的版本。

  • 版本范围也可能产生一些版本冲突,即使 Conan 尝试减少它们。这篇关于版本冲突的常见问题解答讨论了图解决算法和最小化冲突的策略。

覆盖选项

在依赖关系图中出现菱形结构时(如上面所示),不同的配方可能会为上游 options 定义不同的值。在这种情况下,这不会直接导致冲突,而是第一个被定义的值将获得优先级并优先使用。

在上面的示例中,如果 matrix/1.0 既可以是静态库也可以是共享库,并且 engine 决定将其定义为静态库(并非必需,因为这已经是默认设置)

engine/conanfile.py
class Engine(ConanFile):
    name = "engine"
    version = "1.0"
    # Not strictly necessary because this is already the matrix default
    default_options = {"matrix*:shared": False}

警告

在配方中定义选项值没有强有力的保证,请查阅这篇关于依赖项选项值的常见问题解答。推荐的定义选项值的方式是在配置文件中。

同样,intro 配方也会做同样的事情,但它会定义自己需要共享库,并添加一个 validate() 方法,因为出于某种原因,intro 软件包只能针对共享库进行构建,否则会崩溃

intro/conanfile.py
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

遵循同样的原则,下游消费者配方,在本例中是 game conanfile.py,可以定义选项值,并且这些值将获得优先级

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 默认值应该适用于大多数情况,而与这些默认值不同的变体最好在配置文件中定义。