目录

软件包版本控制

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

约束上下文

#

选择包版本时会考虑每个依赖它的包这一事实会产生一个重要的结果:为一个包选择的特定版本是使用该包的应用程序的全局属性。

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

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。有趣的是,它对 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 文章。