新的 opam 功能:更具表达力的依赖项

这篇博客将介绍 opam 2.0 相较于 opam 1.2 的另一个改进方面。我可能比之前的文章更具技术性,因为它涵盖了专门针对打包人员和仓库维护人员的功能,以及关于包定义格式的方面。

在 opam 1.2 中指定依赖项

Opam 1.2 已经提供了一种高级的方式来指定包依赖项,使用包和版本的公式,语法如下:

depends: [
  "foo" {>= "3.0" & < "4.0~"}
  ( "bar" | "baz" {>= "1.0"} )
]

这意味着正在定义的包依赖于 foo 包(位于 3.x 系列中)以及 barbaz 中的一个,后者版本至少为 1.0。有关完整文档,请参阅 此处

但是,这仅允许对于给定包而言是静态的依赖项。

Opam 1.2 引入了 buildtestdoc “依赖项标志”,这些标志可以为依赖项提供一些具体信息(例如,test 依赖项仅在请求包测试时才需要)。这些标志必须出现在版本约束之前,例如 "foo" {build & doc & >= "3.0"}

Opam 2.0 中的扩展

Opam 2.0 对依赖项标志进行了概括,并通过允许混合过滤器(即基于 opam 变量的公式)与版本约束来使依赖项规范更加具有表达力。如果该公式成立,则强制执行依赖项,否则将其丢弃。

有关更详细的说明,请参阅 opam 2.0 手册

此外,由于编译器现在是包,因此所需的 OCaml 版本现在也通过此机制来表示,通过对(虚拟)包 ocaml 的依赖项,例如 depends: [ "ocaml" {>= "4.03.0"} ]。这将替换对 available: 字段和 ocaml-version 开关变量的使用。

条件依赖项

例如,这使得在给定依赖项上添加一个关于操作系统的条件变得非常容易,使用内置变量 os

depends: [ "foo" {>= "3.0" & < "4.0~" & os = "linux"} ]

这里,如果操作系统不是 Linux,则根本不需要 foo。我们还可以使用更复杂的公式来更具体地指定其他操作系统

depends: [
  "foo" { "1.0+linux" & os = "linux" |
          "1.0+osx" & os = "darwin" }
  "bar" { os != "osx" & os != "darwin" }
]

这意味着 Linux 和 OSX 分别需要 foo 版本 1.0+linux1.0+osx,而其他系统需要 bar,任何版本。

依赖项标志

1.2 中使用的依赖项标志不再需要,并被可以在版本规范中任何位置出现的变量所取代。以下变量通常在这些地方非常有用

  • with-testwith-doc:替换 testdoc 依赖项标志,在请求包测试或文档时为 true
  • 同样,build 的行为与之前类似,将依赖项限制为“构建依赖项”,这意味着如果依赖项发生变化,则不需要重建包
  • dev:此布尔变量在“开发”包中保持 true,即绑定到非稳定源(版本控制系统或如果包固定到没有已知校验和的存档)的包。dev 源通常需要一个额外的准备步骤(例如 autoconf),该步骤可能拥有自己的依赖项。

使用 opam config list 来查看预定义变量的列表。请注意,with-testwith-docbuild 变量并非在所有地方都可用:前两个仅允许在 depends:depopts:build:install: 字段中使用,而最后一个特定于 depends:depopts: 字段。

例如,datakit.0.9.0 包有

depends: [
  ...
  "datakit-server" {>= "0.9.0"}
  "datakit-client" {with-test & >= "0.9.0"}
  "datakit-github" {with-test & >= "0.9.0"}
  "alcotest" {with-test & >= "0.7.0"}
]

在运行 opam install datakit.0.9.0 时,with-test 变量设置为 falsedatakit-clientdatakit-githubalcotest 依赖项将被过滤掉:它们将不需要。使用 opam install datakit.0.9.0 --with-testwith-test 变量为 true(仅针对该包,不会启用命令行上未列出的包的测试!)。在这种情况下,依赖项解析为

depends: [
  ...
  "datakit-server" {>= "0.9.0"}
  "datakit-client" {>= "0.9.0"}
  "datakit-github" {>= "0.9.0"}
  "alcotest" {>= "0.7.0"}
]

这将被正常处理。

计算版本

不仅可以使用变量作为条件,还可以使用它们来计算版本值:"foo" {= var} 是允许的,并将要求与变量 var 的值相对应的包 foo 的版本。

例如,这对于定义一系列一起发布并具有相同版本号的包非常有用:无需在每次发布时更新每个包的依赖项以匹配公共版本,你可以利用 version 包变量来表示“与当前包版本相同的其他包”。例如,foo-client 可以具有以下内容

depends: [ "foo-core" {= version} ]

甚至可以在版本中使用变量插值,例如,比上面更详细地指定操作系统特定的版本

depends: [ "foo" {= "1.0+%{os}%"} ]

这将扩展 os 变量,解析为 1.0+linux1.0+darwin 等。

回到我们的 datakit 示例,我们可以利用它并将其重写为更通用的

depends: [
  ...
  "datakit-server" {>= version}
  "datakit-client" {with-test & >= version}
  "datakit-github" {with-test & >= version}
  "alcotest" {with-test & >= "0.7.0"}
]

由于 datakit-* 包遵循相同的版本控制,这避免了在每个新版本上重写 opam 文件,从而每次都存在出错的风险。

作为旁注,这些变量与现在在 build: 字段中使用的变量一致,并且 build-test: 字段现在已弃用。因此,同一个 datakit opam 文件的另一部分

build:
  ["ocaml" "pkg/pkg.ml" "build" "--pinned" "%{pinned}%" "--tests" "false"]
build-test: [
  ["ocaml" "pkg/pkg.ml" "build" "--pinned" "%{pinned}%" "--tests" "true"]
  ["ocaml" "pkg/pkg.ml" "test"]
]

现在最好写成

build: ["ocaml" "pkg/pkg.ml" "build" "--pinned" "%{pinned}%" "--tests" "%{with-test}%"]
run-test: ["ocaml" "pkg/pkg.ml" "test"]

这避免了为了更改选项而进行两次构建。

结论

希望这种对依赖项表达力的扩展能够使打包人员的生活更轻松;欢迎您对您个人使用情况的反馈。

请注意,官方仓库仍然使用 1.2 格式(通过自动转换,在 https://opam.ocaml.org/2.0 中作为 2.0 提供服务),并且仅在 opam 2.0 最终发布后才会迁移。欢迎您在自定义仓库或固定包上进行实验,但需要稍等一段时间才能将利用上述内容的包定义贡献给 官方仓库

注意:这篇文章同时发布在 opam.ocaml.orgocamlpro.com 上。请前往后者查看评论!