新的 opam 功能:更具表達力的依賴關係

這篇部落格將涵蓋 opam 2.0 相較於 opam 1.2 的另一個改進之處。由於它涵蓋的功能專為套件打包者和儲存庫維護者而設計,並且關於套件定義格式,因此我可能會比之前的主題更技術性一些。

在 opam 1.2 中指定依賴關係

Opam 1.2 已經有一種使用套件和版本公式指定套件依賴關係的高級方法,語法如下

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

表示正在定義的套件依賴於 3.x 系列中的套件 foo,以及 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 變數設定為 false,並且會篩選掉 datakit-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},這將要求套件 foo 的版本對應於變數 var 的值。

這在定義一系列以相同版本號一起發布的套件時很有用:您不必在每次發布時更新每個套件的依賴關係以符合通用版本,而是可以利用 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 格式(以 2.0 格式在 https://opam.ocaml.org/2.0 提供,透過自動轉換),並且只有在 opam 2.0 最終發布後才會進行遷移。歡迎您在自訂儲存庫或釘選套件上進行實驗,但是在您可以將使用上述內容的套件定義貢獻到官方儲存庫之前,還需要多一點耐心。

注意:本文已交叉發布在opam.ocaml.orgocamlpro.com。請前往後者發表評論!