Contents

Package versioning

pub软件包管理器的主要工作之一是帮助您进行版本控制. 本文档对版本控制的历史和发布者的方法进行了一些解释. 将此视为高级信息. 如果您想更好地了解为何以这种方式设计了pub,请继续阅读. 如果您只想使用 pub, 其他文档将为您提供更好的服务.

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

很难低估这是多么强大. 当您看到小型网络创业公司在几周内建立站点并吸引数百万用户的故事时,他们之所以能够实现这一目标,唯一的原因是开放源代码社区为他们提供了软件盛宴.

但这不是免费的:代码重用是一个挑战,尤其是重用不维护的代码. 当您的应用使用别人开发的代码时,他们更改代码会怎样? 他们不想破坏您的应用程序,您当然也不想. 我们通过版本控制解决了这个问题.

A name and a number

当您依赖于一些外部代码时,您不仅会说"我的应用程序使用widgets ",还会说"我的应用程序使用widgets 2.0.5 ".名称和版本号的组合唯一地标识了一个不变的代码块. 更新widgets可以进行他们想要的所有更改,但是他们保证不会触摸任何已经发布的版本. 他们可以输出2.0.63.0.0 ,并且不会影响您,因为您使用的版本未更改.

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

Resolving shared dependencies

当您的依赖关系实际上只是一个依赖关系时,取决于特定的版本可以很好地工作. 如果您的应用程序依赖于一堆程序包,而这些东西又具有它们自己的依赖关系,依此类推,只要这些依赖关系中的任何一个都不重叠 ,它们就可以正常工作.

但是,请考虑以下示例:

dependency graph

因此,您的应用程序使用widgetstemplates ,而这两个都使用collection . 这称为共享依赖项 . 现在,当widgets想要使用collection 2.3.5templates想要collection 2.3.7时会发生什么? 如果他们不同意该怎么办?

Unshared libraries (the npm approach)

One option is to just let the app use both versions of collection. It will have two copies of the library at different versions and widgets and templates will each get the one they want.

这就是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的模型不适合.

Version lock (the dead end approach)

相反,当您依赖软件包时,您的应用程序仅使用该软件包的单个副本. 当您拥有共享的依赖项时,所有依赖于此的依赖项都必须同意使用哪个版本. 如果没有,则会出现错误.

但这实际上并不能解决您的问题. 当得到这个错误,你需要能够解决这个问题. 因此,假设您在上一个示例中已经陷入了这种情况. 您想使用widgetstemplates ,但是它们使用的是不同版本的collection . 你是做什么?

答案是尝试升级其中之一. templatescollection 2.3.7 . 是否有可以升级到该版本的widgets的更高版本?

在许多情况下,答案将是"否". 从开发widgets的人们的角度来看它. 他们想发布一个新版本,对他们的代码进行新的更改,并且他们希望尽可能多的人能够对其进行升级. 如果他们坚持使用当前版本的collection那么使用当前版本widgets任何人都将可以使用这一新版本.

如果他们要升级对collection 依赖,那么升级widgets每个人也都必须这样做, 无论他们是否愿意 . 这很痛苦,因此最终不利于升级依赖项. 这就是所谓的版本锁定 :每个人都想向前发展他们的依赖关系,但是没有人可以迈出第一步,因为它也迫使其他人也这样做.

Version constraints (the Dart approach)

为了解决版本锁定问题,我们放宽了程序包对其依赖项的约束. 如果widgetstemplates都可以指示它们所使用的collection版本的范围 ,那么这为我们提供了足够的摆动空间,可以将依赖项移至较新的版本. 只要它们的范围重叠,我们仍然可以找到一个使它们都满意的版本.

这是bundler遵循的模型,也是pub的模型. 在发布规范中添加依赖项时,可以指定可以接受的版本范围 . 如果widgets的pubspec如下所示:

dependencies:
  collection: '>=2.3.5 <2.4.0'

然后,我们可以选择版本2.3.7进行collection ,然后widgetstemplates的约束都可以由单个具体版本来满足.

Semantic versions

当您将依赖项添加到程序包时,有时会需要指定允许的版本范围. 您怎么知道选择哪个范围? 您需要向前兼容,因此理想情况下,该范围应包含尚未发布的将来版本. 但是,您怎么知道您的软件包将与尚不存在的某些新版本一起使用?

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

dependencies:
  collection: ^2.3.5

为了使这项工作奏效,我们需要提出那组承诺. 幸运的是,其他聪明的人已经完成了全部工作,并将其命名为语义版本控制 .

这描述了版本号的格式,以及当您增加到更高版本号时的确切API行为差异. Pub要求版本必须采用这种格式进行格式化,并且为了与pub社区很好地兼容,您的软件包应遵循其指定的语义. 您应该假设您所依赖的软件包也遵循它. (如果您发现他们没有,请告诉他们的作者!)

尽管语义版本控制不保证1.0.0之前的版本之间具有任何兼容性,但Dart社区约定也应在语义上对待这些版本. 每个数字的解释仅向下移动一个槽:从0.1.20.2.0表示重大更改,转到0.1.3表示新功能,转到0.1.2+1表示更改没有变化. t影响公共API.

现在,我们已经拥有处理版本控制和API演化所需的几乎所有内容. 让我们看看他们如何一起玩,以及酒吧在做什么.

Constraint solving

定义软件包时,您将列出其直接依赖项 -它本身使用的软件包. 对于每个版本,请指定其允许的版本范围. 这些依赖包中的每一个可能依次具有各自的依赖关系(称为传递依赖关系 .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 .

Constraint context

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

以下示例显示了这意味着什么. 假设我们有两个应用程序. 这是他们的发布规范:

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

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

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

other_app包直接取决于collection本身. 有趣的是,它碰巧具有与widgets不同的版本约束.

这意味着您不能仅仅单独查看widgets包来确定它将使用哪个版本的collection . 这取决于上下文. 在my_appwidgets将使用collection 1.9.9 . 但在other_app ,由于otherapp其他约束, widgets将受collection 1.4.9的束缚.

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

Constraint solving for exported dependencies

程序包作者必须仔细定义程序包约束. 请考虑以下情形:

dependency graph

bookshelf包装取决于widgets . 该widgets包,目前为1.2.0,出口collection通过export 'package:collection/collection.dart' ,并且是2.4.0. pubspec文件如下:

name: bookshelf
dependencies:
  widgets:  ^1.2.0
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包的范围:

name: bookshelf
dependencies:
  widgets:  '>=1.2.0 <1.3.0'
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,并且相应的约束也将更新.

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

Lockfiles

因此,一旦pub解决了您应用的版本限制,那又如何? 最终结果是您的应用程序直接或间接依赖的每个程序包的完整列表,以及将与您的应用程序约束一起使用的最佳程序包版本.

Pub接受该文件并将其写到应用程序目录pubspec.lock 文件中. 当pub为您的应用构建.packages文件时,它使用锁定文件来了解要引用的每个软件包的版本. (如果您想知道它选择了哪个版本,可以阅读锁定文件以了解信息.)

pub要做的下一件重要事情是,它停止接触lockfile . 获得应用程序的锁定文件后,发布通知您之前,pub不会对其进行修改. 这个很重要. 这意味着您不会无意间自发地在应用程序中使用新版本的随机软件包. 一旦您的应用程序被锁定,它将保持锁定状态,直到您手动告诉它更新锁定文件为止.

如果您的软件包是用于应用程序的,则将您的锁定文件签入源代码控制系统! 这样,您的团队中的每个人在构建您的应用程序时将使用与每个依赖项完全相同的版本. 部署应用程序时还将使用此方法,以确保生产服务器使用与开发时使用的软件包完全相同的软件包.

When things go wrong

当然,所有这些都假设您的依赖图是完美无瑕的. 即使使用版本范围和pub的约束解决方案以及语义版本控制,也永远无法完全摆脱版本炎的危险.

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

You can have disjoint constraints

假设您的应用使用widgetstemplates并且都使用collection . 但是widgets要求版本在1.0.02.0.0之间,而templates3.0.04.0.0之间. 这些范围甚至不重叠. 没有可行的版本.

You can have ranges that don’t contain a released version

假设将所有约束放到一个共享依赖项上后,剩下的范围就>=1.2.4 <1.2.6 . 这不是一个空范围. 如果存在1.2.4版本的依赖关系,那您将大放异彩. 但是也许他们从来没有发布过它,而是从1.2.31.3.0直接发布了. 您有一个范围,但其中没有任何内容.

You can have an unstable graph

到目前为止,这是pub版本解决过程中最具挑战性的部分. 该过程被描述为建立依赖图,然后解决所有约束并选择版本 . 但这实际上不是那样的. 在选择任何版本之前,如何构建整个依赖关系图? pubspec本身是特定于版本的. 同一软件包的不同版本可能具有不同的依赖关系集.

在选择程序包的版本时,它们会更改依赖关系图本身的形状. 随着图形的变化,可能会更改约束,这可能会导致您选择不同的版本,然后又绕回一圈.

有时,此过程永远不会解决成一个稳定的解决方案. 凝视深渊:

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

在所有这些情况下,没有一组适用于您的应用程序的具体版本,当这种情况发生时,pub将报告错误并告诉您正在发生的事情. 绝对不会让您处于某种奇怪的状态,在这种状态下您认为事情可以进行,但不会.

Summary

That was a lot of information, but here are the key points:

  • 代码重用很棒,但是为了使开发人员能够快速移动,软件包需要能够独立发展.
  • 版本控制就是您启用它的方式. 但是依赖单个具体版本太精确了,并且具有共享依赖项会导致版本锁定.
  • 为了解决这个问题,您需要依赖版本范围 . 然后,Pub遍历您的依赖关系图并为您选择最佳版本. 如果不能,它将告诉您.
  • Once your app has a solid set of versions for its dependencies, that gets pinned down in a lockfile. That ensures that every machine your app is on is using the same versions of all of its dependencies.

如果您想进一步了解pub的版本解决算法,请参阅文章PubGrub:下一代版本解决.

by  ICOPY.SITE