目录

包版本控制

pub 包管理器 帮助你处理版本控制。本指南简要解释了版本控制的历史以及 pub 的方法。

请将此视为高级信息。要了解 pub 为什么这样设计,请继续阅读。如果你想 使用 pub,请查阅 其他文档

现代软件开发,尤其是 Web 开发,严重依赖于重复使用大量现有的代码。这包括你过去写的代码,也包括来自第三方的代码,从大型框架到小型实用程序库,各种各样。一个应用程序依赖于几十个不同的包和库的情况并不少见。

这强大的功能难以言喻。当你看到小型 Web 初创公司在几周内构建一个拥有数百万用户的网站的故事时,他们之所以能够做到这一点,唯一的原因是开源社区为他们提供了大量的软件资源。

但这并非免费:代码重用,尤其是重用你没有维护的代码,存在一个挑战。当你的应用程序使用其他人开发的代码时,如果他们更改了代码会发生什么?他们不希望破坏你的应用程序,你当然也不希望。我们通过 版本控制 来解决这个问题。

名称和编号

#

当依赖于某些外部代码时,你不仅仅是说“我的应用程序使用 widgets ”。你说,“我的应用程序使用 widgets 2.0.5 ”。名称和版本号的组合唯一地标识了一块 不变的 代码。更新 widgets 的人员可以随意进行所有更改,但他们承诺不会修改任何已发布的版本。他们可以发布 2.0.63.0.0 ,这不会对你产生任何影响,因为你使用的版本保持不变。

当你 确实 想要获得这些更改时,你可以随时将你的应用程序指向更新版本的 widgets ,并且你无需与这些开发人员协调即可完成此操作。然而,这并不能完全解决问题。

本指南中讨论的版本号可能与包文件名中设置的版本号不同。它们可能包含 -0-beta 。这些表示法不会影响依赖项解析。

解决共享依赖项

#

当你的依赖 实际上只是一个依赖 时,依赖于特定版本可以正常工作。如果你的应用程序依赖于一堆包,而这些包又拥有它们自己的依赖项等等,那么只要这些依赖项 不重叠 ,这一切都可以正常工作。

考虑以下示例:

dependency graph

因此,你的应用程序使用 widgetstemplates ,而这两个都使用 collection 。这称为 共享依赖项 。现在,当 widgets 想使用 collection 2.3.5templates 想使用 collection 2.3.7 时会发生什么?如果它们对版本不一致怎么办?

非共享库(npm 方法)

#

一种选择是让应用程序同时使用两个版本的 collection 。它将在不同版本下拥有该库的两个副本, widgetstemplates 将分别获得它们想要的那个。

这就是 npm 为 node.js 所做的。它对 Dart 有效吗?考虑以下场景:

  1. collection 定义了一些 Dictionary 类。
  2. widgets 从其 collection 副本 (2.3.5) 获取一个实例。然后将其传递给 my_app
  3. my_app 将字典发送到 templates
  4. 这反过来又将其发送到其 collection 版本 (2.3.7)。
  5. 获取它的方法为此对象具有 Dictionary 类型注解。

就 Dart 而言, collection 2.3.5collection 2.3.7 是完全无关的库。如果你从一个库中获取 Dictionary 类的实例并将其传递给另一个库中的方法,则这是一个完全不同的 Dictionary 类型。这意味着它将无法匹配接收库中的 Dictionary 类型注解。糟糕。

由于这个原因(以及尝试调试具有多个具有相同名称的版本的事物的应用程序带来的麻烦),我们认为 npm 的模型并不适合。

版本锁定(死胡同方法)

#

相反,当你依赖于一个包时,你的应用程序只使用该包的一个副本。当你拥有共享依赖项时,所有依赖于它的内容都必须就使用哪个版本达成一致。如果不一致,则会发生错误。

但这实际上并没有解决你的问题。当你 确实 遇到该错误时,你需要能够解决它。因此,假设你在前面的示例中遇到了这种情况。你想使用 widgetstemplates ,但它们使用的是不同版本的 collection 。你该怎么办?

答案是尝试升级其中一个。 templates 需要 collection 2.3.7 。是否存在你可以升级到的与该版本兼容的更新版本的 widgets

在许多情况下,答案是“否”。从开发 widgets 的人员的角度来看待它。他们希望发布一个包含对 他们 代码的新更改的新版本,并且他们希望尽可能多的人能够升级到它。如果他们坚持使用他们 当前 版本的 collection ,那么任何使用当前版本 widgets 的人都也可以使用这个新版本。

如果他们要升级他们对 collection 的依赖,那么升级 widgets 的每个人也必须升级, 无论他们是否愿意 。这很痛苦,因此最终会降低升级依赖项的积极性。这称为 版本锁定 : 每个人都想向前移动他们的依赖项,但没有人可以迈出第一步,因为这也会迫使其他人这样做。

版本约束(Dart 方法)

#

为了解决版本锁定问题,我们放松了包对其依赖项施加的约束。如果 widgetstemplates 都可以指示它们可以使用的 collection 版本的 范围 ,那么这将给我们足够的回旋余地,可以将我们的依赖项向前移动到更新的版本。只要它们的范围存在重叠,我们仍然可以找到一个使它们都满意的单一版本。

这就是 bundler 遵循的模型,也是 pub 的模型。当你在你的 pubspec 中添加依赖项时,你可以指定一个你可以接受的版本 范围 。如果 widgets 的 pubspec 看起来像这样:

yaml
dependencies:
  collection: '>=2.3.5 <2.4.0'

你可以为 collection 选择版本 2.3.7 。单个具体版本将满足 widgetstemplates 包的约束。

语义版本

#

当你向你的包添加依赖项时,有时你可能希望指定允许的版本范围。你怎么知道要选择哪个范围?你需要向前兼容,因此理想情况下,该范围包含尚未发布的未来版本。但是你怎么知道你的包将与尚不存在的某些新版本一起工作?

为了解决这个问题,你需要就版本号的 含义 达成一致。想象一下,你依赖的包的开发人员说:“如果我们进行任何向后不兼容的更改,那么我们承诺会增加主版本号。” 如果你相信他们,那么如果你知道你的包与他们的 2.3.5 版本一起工作,你就可以依赖它一直工作到 3.0.0 。你可以将范围设置为:

yaml
dependencies:
  collection: ^2.3.5

为了使这能够工作,我们需要提出这套承诺。幸运的是,其他聪明的人已经完成了弄清楚这一切的工作,并将其命名为_语义版本_。

这描述了版本号的格式,以及当你增加到更高版本号时确切的 API 行为差异。Pub 要求版本采用这种格式,为了与 pub 社区良好配合,你的包应该遵循它指定的语义。你应该假设你依赖的包也遵循它。(如果你发现它们没有遵循,请让他们的作者知道!)

尽管语义版本控制并不保证在 1.0.0 之前的版本之间有任何兼容性,但 Dart 社区的约定是同样语义地处理这些版本。每个数字的解释只是向下移动了一个位置:从 0.1.20.2.0 表示重大更改,到 0.1.3 表示新功能,到 0.1.2+1 表示不会影响公共 API 的更改。为简单起见,避免在版本达到 1.0.0 后使用 +

我们现在几乎拥有了处理版本控制和 API 演变所需的所有部分。让我们看看它们是如何协同工作的,以及 pub 做了什么。

约束求解

#

当你定义你的包时,你会列出其 直接依赖项 。这些是你的包使用的包。对于每个包,你都会指定你的包允许的版本范围。然后,这些依赖包可能又会有它们自己的依赖项。这些称为 传递依赖项 。Pub 会遍历这些依赖项并构建应用程序的整个依赖项图。

对于图中的每个包,pub 都会查看所有依赖于它的内容。它会收集所有这些版本约束并尝试同时解决它们。基本上,它会取它们的范围的交集。然后 pub 会查看已为该包发布的实际版本,并选择满足所有这些约束的最新版本。

例如,假设我们的依赖图包含 collection ,并且三个包依赖于它。它们的版本约束是:

>=1.7.0
^1.4.0
<1.9.0

collection 的开发人员已发布了以下版本:

1.7.0
1.7.1
1.8.0
1.8.1
1.8.2
1.9.0

适合所有这些范围的最高版本号是 1.8.2 ,因此 pub 选择了它。这意味着你的应用程序 以及你的应用程序使用的每个包 都将使用 collection 1.8.2

约束上下文

#

选择包版本会考虑 所有 依赖于它的包这一事实具有重要意义: 将为包选择的特定版本是使用该包的应用程序的全局属性

以下示例说明了这意味着什么。假设我们有两个应用程序。以下是它们的 pubspecs:

yaml
name: my_app
dependencies:
  widgets:
yaml
name: other_app
dependencies:
  widgets:
  collection: '<1.5.0'

它们都依赖于 widgets ,其 pubspec 为:

yaml
name: widgets
dependencies:
  collection: '>=1.0.0 <2.0.0'

other_app 包直接依赖于 collection 本身。有趣的部分是,它碰巧对它的版本约束与 widgets 的不同。

这意味着你不能仅仅孤立地查看 widgets 包来确定它将使用哪个版本的 collection 。这取决于上下文。在 my_app 中, widgets 将使用 collection 1.9.9 。但在 other_app 中,由于 otherapp 对它的 其他 约束, widgets 将使用 collection 1.4.9

这就是每个应用程序都有其自己的 package_config.json 文件的原因:为每个包选择的具体版本取决于包含应用程序的整个依赖图。

导出依赖项的约束求解

#

包作者必须仔细定义包约束。考虑以下场景:

dependency graph

bookshelf 包依赖于 widgetswidgets 包(当前版本为 1.2.0)通过 export 'package:collection/collection.dart' 导出 collection ,并且版本为 2.4.0。pubspec 文件如下所示:

yaml
name: bookshelf
dependencies:
  widgets: ^1.2.0
yaml
name: widgets
dependencies:
  collection: ^2.4.0

然后 collection 包更新到 2.5.0 版本。 collection 的 2.5.0 版本包含一个名为 sortBackwards() 的新方法。 bookshelf 可能会调用 sortBackwards() ,因为它是由 widgets 公开的 API 的一部分,尽管 bookshelf 只对 collection 具有传递依赖关系。

由于 widgets 的 API 没有反映在其版本号中,因此使用 bookshelf 包并调用 sortBackwards() 的应用程序可能会崩溃。

导出 API 会导致该 API 被视为在包本身中定义,但当 API 添加功能时,它无法增加版本号。这意味着 bookshelf 无法声明它需要支持 sortBackwards()widgets 版本。

因此,在处理导出的包时,建议包的作者对依赖项的上限和下限设置更严格的限制。在这种情况下, widgets 包的范围应缩小:

yaml
name: bookshelf
dependencies:
  widgets: '>=1.2.0 <1.3.0'
yaml
name: widgets
dependencies:
  collection: '>=2.4.0 <2.5.0'

这意味着 widgets 的下限为 1.2.0, collection 的下限为 2.4.0。当有人发布 collection 的 2.5.0 版本时,pub 将 widgets 更新到 1.3.0 并更新相应的约束。

使用此约定可确保用户拥有两个包的正确版本,即使其中一个不是直接依赖项。

锁定文件

#

因此,一旦 pub 解除了应用程序的版本约束,接下来会发生什么?最终结果是你的应用程序直接或间接依赖的每个包的完整列表,以及最适合你的应用程序约束的该包的最佳版本。

对于每个包,pub 获取该信息,从中计算出 内容哈希 ,并将两者都写入应用程序目录中名为 pubspec.lock 的_锁定文件_。当 pub 为你的应用程序构建 .dart_tool/package_config.json 文件时,它会使用锁定文件来知道要引用每个包的哪些版本。(如果你想知道它选择了哪些版本,你可以阅读锁定文件来找出答案。)

pub 执行的下一个重要操作是 停止修改锁定文件 。一旦你为你的应用程序获得了锁定文件,pub 就会停止修改它,直到你告诉它为止。这一点很重要。这意味着你的应用程序不会在没有意图的情况下自发地开始使用随机包的新版本。一旦你的应用程序被锁定,它就会保持锁定状态,直到你手动告诉它更新锁定文件为止。

如果你的包用于应用程序,你应该_将锁定文件检入你的源代码控制系统!_这样,你的团队中的每个人在构建你的应用程序时都将使用每个依赖项的完全相同的版本。你也可以在部署应用程序时使用它,以便你可以确保你的生产服务器正在使用与你正在开发的完全相同的包。

当出现问题时

#

当然,所有这些都假设你的依赖关系图是完美无缺的。即使有版本范围、pub 的约束求解和语义版本控制,你也永远无法完全避免版本冲突的危险。

你可能会遇到以下问题之一:

你可能有不相交的约束

#

假设你的应用程序使用 widgetstemplates ,两者都使用 collection 。但是 widgets 请求其版本在 1.0.02.0.0 之间,而 templates 需要 3.0.04.0.0 之间的版本。这些范围甚至没有重叠。没有可能工作的版本。

你可能拥有不包含已发布版本的范围

#

假设在将所有对共享依赖项的约束放在一起之后,你有一个狭窄的范围 >=1.2.4 <1.2.6 。它不是一个空范围。如果依赖项有 1.2.4 版本,你就会万事大吉。但也许他们从未发布过那个版本。相反,他们直接从 1.2.3 跳到了 1.3.0 。你的范围内部没有任何东西。

你可能拥有一个不稳定的图

#

这是 pub 版本求解过程中最具挑战性的部分。该过程被描述为_构建依赖图,然后解决所有约束并选择版本_。但它实际上并没有那样工作。在选择 任何 版本之前,你如何才能构建 整个 依赖图?pubspec 本身是特定于版本的。相同包的不同版本可能具有不同的依赖项集。

当你选择包的版本时,它们会改变依赖图本身的形状。随着图的改变,这可能会改变约束,这会导致你选择不同的版本,然后你又回到了同一个循环中。

有时这个过程永远不会稳定到一个稳定的解决方案。凝视深渊:

yaml
name: my_app
version: 0.0.0
dependencies:
  yin: '>=1.0.0'
yaml
name: yin
version: 1.0.0
dependencies:
yaml
name: yin
version: 2.0.0
dependencies:
  yang: '1.0.0'
yaml
name: yang
version: 1.0.0
dependencies:
  yin: '1.0.0'

在所有这些情况下,都没有一组具体的版本适用于你的应用程序,当发生这种情况时,pub 会报告错误并告诉你发生了什么。它绝对不会让你处于某种奇怪的状态,让你认为事情可以工作,但实际上却不行。

总结

#

总而言之:

  • 尽管代码重用有很多优点,但包需要能够独立发展。
  • 版本控制使这种独立性成为可能。依赖于单个具体版本缺乏灵活性。结合共享依赖项,它会导致版本锁定。
  • 为了应对版本锁定,你的包应该依赖于 一系列 版本。然后 pub 会遍历你的依赖图并为你选择最佳版本。如果它无法选择合适的版本,pub 会提醒你。
  • 一旦你的应用程序为其依赖项设置了一组可靠的版本,该集合就会在一个 锁定文件 中固定下来。这确保了运行你的应用程序的每台机器都使用其所有依赖项的相同版本。

要了解有关 pub 版本求解算法的更多信息,请参阅 Medium 上的 PubGrub 文章。