Clojure

依赖扩展

本页面深入探讨了 Clojure 工具和 tools.deps 如何将一组根依赖扩展为传递依赖集。此算法类似于 Maven 依赖扩展(两者都是广度优先树扩展),但在版本选择方面做出了不同的选择。

扩展

扩展输入

  • 初始依赖 - 每个依赖定义为一个库(符号)和坐标(Maven、Git、本地等)

  • 默认依赖 - 如果未提供坐标,则使用库到坐标的映射

  • 覆盖依赖 - 如果找到库,则使用库到坐标的映射

扩展过程是对树中路径队列的循环。初始队列由指向根依赖的单一路径组成。

对于循环的每一步,我们都从队列中提取一条路径(以依赖项结尾)以供考虑。使用默认依赖和覆盖依赖(如果需要)来替换要与依赖项一起使用的坐标。然后,我们考虑此库/坐标并决定是否将其包含或排除在选择中,并记录一个原因代码以供以后理解。如果节点具有子节点,则将它们添加到队列中。

在整个循环中,我们跟踪版本映射中所有已看到的库、版本和选择选择,以及排除映射中的排除项。如果需要,每个考虑事项以及节点是否被包含在内以及原因将在扩展跟踪中记录。

所有选择都在对树的单次遍历期间做出。主线扩展是单线程的,但是检索依赖项的子节点可能需要从外部网络源获取它们(例如,从 Maven 存储库请求 pom)。为了提高性能,使用线程池并行地提前获取元数据。

依赖选择

当考虑选择节点时,如果以下条件成立,则将包含该库:

  • 它是顶级依赖(顶级依赖版本始终获胜)或它是新库或已知库的更新版本(Maven 仅保留第一个找到的版本,无论新旧如何)

  • 并且它未被该节点路径的任何父节点排除(请参阅下面的排除部分)

  • 并且路径中的所有父节点都被选中(请参阅下面的孤儿部分)

如果包含 lib/version,则将在版本映射中将其标记为已选中。

排除

排除项在树中的节点上标记在子节点上,并应用于该子依赖项及其子节点。排除项记录在排除映射中,并在检查是否包含树中该点以下的库时使用。

一个特别棘手的情况是,相同的库和版本在树中的不同点出现,但排除集不同。在这种情况下,用于该库的排除项将是两个排除集的 **交集**。

例如,给定一个类似于下面的树:

A
  B
    C (excl X, Y)
      X
      Y
      Z
  D
    C (excl X)
      X
      Y
      Z

那么 C 将仅排除 X(#{X Y} 和 #{X} 的交集),因为 A-D-C 分支需要该依赖项!

同样重要的是,当考虑路径 A-B-C 时,它只会将 Z 入队作为子依赖项(因为 X 和 Y 被排除)。当考虑 A-D-C 时,它会缩小 C 的排除集并将 Y 标记为 C 的新子项以进行扩展。请注意,无论 A-B-C 还是 A-D-C 首先被考虑,扩展顺序可能不同,但对包含的最终选择将保持一致。

孤儿

在扩展大型树时,我们可能会将特定库版本的子节点入队,然后找到并选择该库的更新版本,该版本具有不同的依赖项集。在这种情况下,原始库版本节点将被取消选中,但其子节点可能仍然在队列中。

当遇到这些子节点时,只有在所有父节点仍然被选中时,它们才会被包含。

例如,给定一个类似于下面的树:

A
  B
    C1
      X
  D
    C2
      Y

跟踪可能显示如下:

  • A - 包含 A,顶级依赖

  • A-B - 包含 B,新库

  • A-D - 包含 D,新库

  • A-B-C1 - 包含 C1,新库(入队 X,Y)

  • A-D-C2 - 包含 C2,更新版本(+ 取消选中 C1)

  • A-B-C1-X - 排除 X,父节点省略

  • A-D-C2-Y - 包含 Y

在某些情况下,子节点可能已经在版本映射中被选中,然后父节点被取消选中。为了捕获这些情况,在扩展后执行孤儿检查以确保所有选中的库都包含父节点。如果不是这种情况,则会剪裁孤儿节点。

A
  B1
    X
  C
    B2
      Z

跟踪

  • A - 包含 A,顶级依赖

  • A-B1 - 包含 B1,新库

  • A-C - 包含 C,新库

  • A-B1-X - 包含 X,新库

  • A-C-B2 - 包含 B2,更新版本(+ 取消选中 B1)

  • A-C-B2-Z - 包含 Z

扩展后,我们将选择 A、X、C、B2 和 Z。但是,在检查每个节点时,我们会发现 X 的父节点 B1 未被包含,因此 X 将被剪裁。

跟踪数据

使用命令行工具时,可以使用选项 -Strace 激活依赖项跟踪,这将发出当前目录中的跟踪数据文件 trace.edn。如果您检查该数据,您会发现一个具有以下键的映射:

  • :log - 被考虑的库节点的日志,它们是否被包含,以及每个原因。日志将是一个映射向量,每个被考虑的节点一个,具有以下键::lib:coord:coord-id:paths:include:reason,以及其他可能的键。

  • :vmap - 版本映射(格式可能会更改)

  • :exclusions - 排除映射(格式可能会更改)

树形打印

可以使用 clj -Stree 或程序 clj -X:deps tree 打印依赖项,该程序 具有更多选项

树是根据跟踪日志构建的,并包含所有被考虑的节点。包含的节点以 . 为前缀。排除的节点以 X 为前缀。行尾将包含原因代码(某些代码被抑制)。当前的一组原因代码(可能会更改)为:

  • :new-top-dep - 包含为顶级依赖(被抑制)

  • :new-dep - 包含为新依赖(被抑制)

  • :same-version - 排除,与当前选定的依赖相同(被抑制)

  • :newer-version - 包含,比以前选择的版本更新

  • :use-top - 排除,与顶级库相同,但不在顶部

  • :older-version - 排除,比以前选择的版本更旧

  • :excluded - 排除,父路径中的节点排除了此库

  • :parent-omitted - 排除,父节点被取消选中

  • :superseded - 排除,此版本被取消选中