目录

软件包版本控制

pub 软件包管理器 的主要工作之一是帮助你使用版本控制。本文档解释了版本控制的历史以及 pub 对它的处理方法。请将其视为高级信息。如果你想更好地了解 pub 的设计方式的原因,请继续阅读。如果你只想使用 pub,则其他文档 将更好地为你服务。

现代软件开发,尤其是 Web 开发,在很大程度上依赖于重复使用大量现有代码。其中包括你过去编写的代码,但也包括来自第三方的代码,从大型框架到小型实用程序库,无所不包。应用程序依赖于数十个不同的软件包和库的情况并不少见。

很难低估它的强大功能。当你看到小型的 Web 初创公司在几周内建立一个拥有数百万用户的网站的故事时,他们能够实现这一目标的唯一原因是开源社区为他们提供了丰富的软件盛宴。

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

名称和数字

#

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

当你确实想要获取这些更改时,你始终可以将你的应用程序指向widgets的较新版本,而无需与那些开发人员协调即可进行此操作。但是,这并不能完全解决问题。

解决共享依赖项

#

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

但考虑以下示例

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. 反过来,它将其发送到版本的collection2.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 会查看所有依赖于它的内容。它收集它们的所有版本约束并尝试同时解决它们。(基本上,它求交它们的范围。)然后它查看已为该软件包发布的实际版本,并选择满足所有这些约束的最佳(最新)版本。

例如,假设我们的依赖项图包含 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 本身。有趣的是,它碰巧对它的版本约束与 widgets 不同。

这意味着你不能仅仅查看 widgets 包来确定它将使用哪个版本的 collection。这取决于上下文。在 my_app 中,widgets 将使用 collection 1.9.9。但在 other_app 中,widgets 将被强制使用 collection 1.4.9,因为 otherapp 对它施加了其他约束。

这就是每个应用都有自己的 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 的一部分,尽管 bookshelfcollection 只有传递依赖关系。

由于 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 版本发布时,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 的版本解决算法,请参阅文章 PubGrub:下一代版本解决。