背景介绍
Stack 2.0转而使用Pantry库来管理依赖关系。Pantry做了很多事情,但它的核心是高度关注可重现性。它的想法是,通过一个完全合格的包规范,你应该总是得到相同的源代码。作为一个例子,https://example.com/foo.tar.gz 不会是一个完全合格的软件包规格,因为该tarball中的内容可以默默地改变而不被发现。相反,在Pantry中,你会指定类似的东西。
size: 9526
url: https://github.com/snoyberg/filelock/archive/97e83ecc133cd60a99df8e1fa5a3c2739ad007dc.tar.gz
cabal-file:
size: 1571
sha256: d97c2ee2b4f0c72b35cbaf04ad37cda2e9e6a2eb1e162b5c6ab084acb94f4634
name: filelock
version: 0.1.1.2
sha256: 78332e0d964cb2f24fdbb6b07c2a6a84a029c4fe540a0435993c85ad58eab051
pantry-tree:
size: 584
sha256: 19914e8fb09ffe2116cebb8b9d19ab51452594940f1e3770e01357b874c65767
当然,用手写这些东西是很繁琐和烦人的,所以Stack使用Pantry来为你生成这些值,并把它们放在一个锁文件中。
单独的。Stack长期以来一直支持在你的源代码中包含hpack的package.yaml 文件,以及自动生成一个.cabal 文件的能力。对于hpack,有两个怪癖我们需要注意:
- 它生成的cabal文件从一个版本到另一个版本都有变化。其中一些变化可能在语义上是有意义的。至少,每个新版本都会在 cabal 文件的注释中标明不同的 hpack 版本。
- hpack的生成是一个高度注重I/O的活动,要看一个包中的所有文件。此外,正如我最近被提醒的那样,它可以参考你试图构建的特定包之外的文件,但在同一个Git仓库或tarball内。
最后,Stack和Pantry对两种不同类型的包进行了明确的区分。不可变的包是我们可以假设永远不会改变的东西。这些将是以下的一种:
- Hackage上的一个包,由名称、版本号和Hackage的修订信息指定。
- 一个由文件路径或URL给出的tarball或ZIP文件。虽然这些绝对可以随时间变化,但Pantry明确建议只使用不可变的包。而存储在锁文件中的哈希值和文件大小提供了对变化的保护。
- 一个Git或Mercurial仓库,由一个提交指定。
另一方面,可变包是以文件系统上的文件形式存储的包。这些是你在本地项目中正在处理的包。可重复性在这里就不那么重要了。我们允许Stack定期检查所有这些文件的时间戳和哈希值,并确定何时需要重建。
冲突
关于如何用Stack和hpack来管理你的包,已经有一段时间的争论了。问题很简单:你是否将生成的cabal 文件存储在 repo 中?两个方向都有坚实的论据:
- 你不应该存储这个文件,因为一般来说,生成的文件不应该存储在版本库中。这可能会导致不必要的差异,以及当人们使用不同的hpack版本时,文件在不同的生成内容之间来回跳动的 "提交战"。
- 你应该存储该文件,因为为了最大限度的重现性,我们要确保我们有相同的cabal文件作为构建的输入。另外,对于使用没有内置hpack支持的构建工具的人来说,有一个cabal文件存在会更方便。
多年来,我和许多不同的人断断续续地讨论过这个问题,在Stack 2之前,我个人已经确定了第一个方法:不存储cabal文件。然后我开始在Pantry上工作。
早期的Pantry
在Pantry开发的早期,我做了一个决定,把重点放在可重复性上。我很快就遇到了hpack的问题。我需要能够很容易地知道一个包的名称和版本,但我唯一的代码路径是解析cabal文件。为了支持hpack文件,我需要把整个包的内容写到文件系统中,在生成的目录上运行hpack,然后解析生成的文件。
(我也许可以在解析hpack YAML文件时直接想出一些古怪的办法,但那感觉就像一个麻烦事)。
每次Stack或Pantry需要知道一个软件包的名称/版本时,执行这些步骤都是非常昂贵的,所以我拒绝了这个选择。我也考虑过缓存生成的cabal文件,但由于生成的文件内容会随着版本的变化而变化,我没有走这条路,因为这违反了可重复性。
目前的储藏室
Stack 2.0的一个早期测试者抱怨了这个变化。虽然hpack对可变的本地包工作得很好,但它对不可变的包不再起作用了。如果你有一个包含软件包的Git仓库,但该仓库不包括生成的cabal文件,而你想使用该仓库作为额外的dep,事情会失败。Stack 1没有出现这种情况,所以这被认为是功能上的退步(正确)。
然而,Stack 2的目标是实现Stack 1没有实现的缓存和可重复性目标。如果有人记得,Stack 1有一个不好的倾向,那就是经常重新克隆Git仓库,比你认为的要频繁得多。Pantry的缓存最终解决了这个问题,而且是通过重现性来实现的。
我最初的建议是要求改变所有作为extra-deps使用的Git repos,以包括生成的cabal文件。然而,在与测试人员进一步讨论后,我们最终还是改变了 Pantry。我们增加了缓存生成的 cabal 文件的能力(根据所使用的 hpack 版本而定)。我对此感到不安,但最终它似乎运作良好,并让我们保留了我们想要的功能。因此,我们在Stack 2的Pantry中提供了这个功能,并继续建议人们不要包含生成的cabal文件。
问题出现了
不幸的是,事情远不是那么美好。现在我意识到这种情况至少有三个问题:
- 延续之前的问题:使用不支持hpack的构建工具的人仍然不适合使用这些仓库。
- 正如在问题#4906中提出的,由于Pantry是如何处理Megarepos中的子目录的,在某些情况下,cabal文件的生成会对额外的步骤失败。
- 锁定文件经常因改变生成的cabal文件而被损坏。如果你使用一个新版本的Stack,使用不同版本的hpack,它将生成一个不同的cabal文件,这将改变锁文件中与软件包相关的哈希值。这可能会在团队之间造成很大的挫折,并破坏了锁文件的整个目的。
对于第二个和第三个问题,可能有解决方案。但对于第一个问题,肯定是没有解决办法的,除非再次加入cabal文件。
变化
基于所有这些,我建议我们做出以下改变:
- 立即开始:更新文档和Stack模板,建议检查生成的cabal文件。这将涉及一些小的文档改进,以及从一些
.gitignore文件中删除*.cabal。 - 在Pantry和Stack的下一个版本中,当一个不可变的包不包括
.cabal文件时,添加一个警告。基本上参考这篇博文,并警告说这样做可能会破坏锁文件。 - 就个人而言。我将开始把生成的cabal文件包括在我的repos中。因为我有一堆这样的文件,我会很感谢人们发送PR来修改我的.gitignore文件,并在你发现它们时加入生成的文件。
对于那些真正反对包括生成的cabal文件的人来说,也不是一无是处。对于这些情况,我的建议很简单:将生成的文件保留在你的仓库之外,然后用stack sdist 生成一个源码压缩包,作为额外的部署使用。这基本上反映了你上传软件包到Hackage所要遵循的stack upload 步骤。
接下来的步骤
使之成为现实所需的改动很小,我很乐意自己做这些改动。我将为这个话题开放一个短暂的讨论期,可能是一周左右,取决于讨论的情况。