跳到主要内容

第 22 章:OCaml 平台(The OCaml Platform)

原文:Anil Madhavapeddy and Yaron Minsky, Real World OCaml: Functional Programming for the Masses, Second Edition, Chapter 22。维护者已确认本书为开源书籍,可翻译并发布用于学习研究。

到目前为止,在第 II 部分中,我们已经介绍了许多可以用来构建较大规模 OCaml 程序的库和技术。现在我们通过考察一组工具来收尾这一部分,这些工具可用于编辑、编译、测试、编写文档以及发布你自己的项目。

OCaml 社区已经开发出一套现代工具,能够与 Visual Studio Code 这类 IDE 集成,生成 API 文档,并支持持续集成(CI)、单元测试、模糊测试等现代软件工程实践。你只需要指定项目元数据,例如库依赖和编译器版本,接下来我们要介绍的 OCaml 平台工具就会替你完成大量繁重工作。

注:使用基于源码的 Opam 包管理器

opam 是 OCaml 社区使用的官方包管理器和元数据打包格式。在前面的章节中,我们已经用它安装过 OCaml 库;接下来会更仔细地看看如何在完整项目中使用 opam。读到本书这里时,你几乎肯定已经做过这件事;但如果你是直接跳到本章,请先确保已经初始化 opam 的全局状态。

$ opam init

默认情况下,opam 不需要任何特殊用户权限,并会把它安装的所有文件存放在 ~/.opam 下。如果你初始化 opam 时没有预装 OCaml 编译器,那么当前构建的 OCaml 编译器也会放在那里。

你可以维护多个开发环境,每个环境安装不同的包和编译器;每个这样的环境都叫作一个 “switch”。默认 switch 可以在 ~/.opam/default 下找到。运行 opam switch 可以查看所有可用的沙箱环境:

$ opam switch
# switch compiler description
default ocaml.4.13.1 default

22.1 一个 Hello World OCaml 项目(A Hello World OCaml Project)​

我们先创建一个示例 OCaml 项目,并在其中四处看看。Dune 有一个基础的内置命令,可以初始化一个适合作为起点的项目模板。

$ dune init proj hello
Success: initialized project component named hello

Dune 会创建一个 hello/ 目录,并用一个 OCaml 项目骨架填充它。这个示例项目包含一些基础元数据,足以让我们进一步学习前面章节中已经用过的 opam 包管理器和 dune 构建工具。

22.1.1 设置 Opam 本地 Switch(Setting Up an Opam Local Switch)​

接下来需要为这个项目准备一个合适的环境,让其中可以使用 dune 和其他库依赖。最好的办法是通过 opam switch create 命令创建一个新的 opam 沙箱。如果给这个命令指定一个项目目录参数,它就会创建一个“本地 switch”,把所有依赖存放在该目录下,而不是放在 ~/.opam 下。这是一种方便的方式,可以把所有构建依赖和源代码都放在文件系统中的同一个位置。

现在为 hello world 项目创建一个本地 switch:

$ cd hello
$ opam switch create .

这会调用 opam 安装项目依赖。在本例中,因为初始化项目时没有指定更多内容,所以依赖只有 OCaml 编译器和 dune。本地 switch 的所有文件都会出现在工作目录的 _opam/ 下。例如,你可以在其中找到刚刚编译出的 dune 二进制文件:

$ ./_opam/bin/dune --version
3.0.2

随着项目增长,opam 会在本地 switch 中安装其他二进制文件和库,因此你需要把这个 switch 加入命令行路径。可以使用 opam env 把正确目录加入当前 shell 的本地路径,从而调用本地安装的工具:

$ eval $(opam env)

如果你不想修改 shell 配置,也可以通过 opam exec 调用命令。它会为命令行剩余部分指定的子命令修改路径。

$ opam exec -- dune build

这会在加入 opam 环境变量后执行 dune build,因此它会选中项目中本地构建的 dune。命令行中的双短横线是常见 Unix 约定,表示让 opam 停止解析自己的可选参数,把后面的内容留给即将执行的命令,避免互相干扰。

22.1.2 选择 OCaml 编译器版本(Choosing an OCaml Compiler Version)​

创建 switch 时,opam 会分析项目依赖,并选择与它们兼容的最新 OCaml 编译器。不过有时你会想选择某个特定版本的 OCaml 编译器,也许是为了保证可重现性,或者为了使用某项特定功能。可以使用 opam switch list-available 获取所有可用编译器的完整列表。

ocaml-system 4.13.1 The OCaml compiler
(system version, from outside
of opam)
ocaml-base-compiler 4.13.1 Official release 4.13.1
ocaml-variants 4.13.1+options Official release of OCaml 4.13.1

实际列表中会有更多版本,但注意这里出现了三类不同的 OCaml 编译器包。

ocaml-system 是 opam 用来表示你机器上已经预装的 OCaml 编译器版本的名称。这个编译器总是安装很快,因为无需为它编译任何东西。创建 system switch 所需的唯一条件,是系统上已经安装正确版本的 OCaml,例如通过 apt 或 Homebrew 安装,并在创建 switch 时把同一版本作为额外参数传入。

例如,如果你已经安装了 OCaml 4.13.1,那么运行下面的命令会使用系统编译器:

$ opam switch create . 4.13.1

另一方面,如果没有安装该系统编译器,那么编译器就需要从头构建。在这种情况下,上面的命令会选择 ocaml-base-compiler 包,其中包含完整的 OCaml 编译器源码。它会比 ocaml-system 花更长时间,但在版本选择上更灵活。opam switch create 的默认行为是根据项目元数据计算最新支持的编译器版本,并把它用于本地 switch。

如果你总是希望本地安装某个特定编译器,可以把包描述写得更精确:

$ opam switch create . ocaml-base-compiler.4.13.1

有时,你还需要为编译器添加自定义配置选项,比如 flambda 优化器。处理这一点涉及两个包:ocaml-variants 会检测各种 ocaml-option 包是否存在,并据此启用配置标志。例如,要构建一个带 flambda 的编译器,可以这样写:

$ opam switch create . ocaml-variants.4.13.1+options ocaml-option-flambda

你可以指定多个 ocaml-option 包,以覆盖项目所需的所有定制。使用下面的命令可以查看完整的 option 包集合:

$ opam search ocaml-option

22.1.3 OCaml 项目的结构(Structure of an OCaml Project)​

在第 5 章“文件、模块与程序(Files, Modules, and Programs)”中,我们看过一个带有少量 OCaml 模块的简单程序是什么样子。现在来查看 hello/ 应用中的文件集合,考察一个更完整的项目结构。

.
|-- dune-project
|-- hello.opam
|-- lib
| |-- dune
|-- bin
| |-- dune
| `-- main.ml
`-- test
|-- dune
`-- hello.ml

关于这个结构,有几点值得注意:

  • dune-project 文件标记项目根目录,并用于写下项目的一些关键元数据,稍后会更详细讨论。

  • hello.opam 文件包含把这个软件注册为 opam 项目的元数据。后面会看到,我们不需要手工编辑它,因为可以通过 dune 生成这个文件。

  • 这里有三个源目录,每个目录都有自己的 dune 文件,用于指定代码库对应部分的构建参数。对于一个主要是可执行程序,而不是可复用库的项目来说,lib、bin、test 这组三个目录很合理。这种情况下,你可能会这样使用它们:

    • lib 目录包含大部分源代码。
    • bin 目录在 lib 中的代码之上放一个很薄的包装层,用来真正启动可执行程序。
    • test 目录包含 lib 的大部分测试。按照第 18 章“测试(Testing)”中的建议,测试与源码放在不同目录中。

现在我们更详细地讨论这个结构的不同部分。

22.1.4 定义模块名(Defining Module Names)​

一对匹配的 ml 和 mli 文件会定义一个 OCaml 模块,模块名来自文件名,并首字母大写。模块名是在 OCaml 代码中引用名称的唯一方式。

在骨架项目的 lib/ 目录中创建一个 Msg 模块:

$ echo 'let greeting = "Hello World"' > lib/msg.ml
$ echo 'val greeting : string' > lib/msg.mli

有效的 OCaml 模块名不能包含短横线,也不能包含下划线以外的其他特殊字符。如果你需要复习文件与模块如何交互,请回到第 5 章“文件、模块与程序(Files, Modules, and Programs)”。

22.1.5 把模块集合定义为库(Defining Libraries as Collections of Modules)​

一个或多个 OCaml 模块可以聚合成一个库(library),这样就能用单个名称方便地打包多个依赖。项目通常会把应用的业务逻辑放入库中,而不是直接放入可执行二进制文件,因为除了提升可复用性之外,这还会让测试和文档编写更容易。

库通过在目录中放置 dune 文件来定义,例如 lib/dune 中为我们生成的文件:

(library
(name hello))

Dune 会把这个目录中的所有 OCaml 模块都视为 hello 库的一部分。对于更高级的项目,这个行为可以通过 modules 字段覆盖。默认情况下,dune 还会把库暴露为包裹在单个 OCaml 模块之下的形式,而 name 字段决定该模块的名称。

在我们的示例项目中,msg.ml 位于 lib/dune 定义的 hello 库里。因此,新定义模块的用户可以通过 Hello.Msg 访问它。你可以在第 26 章“编译器前端:解析与类型检查(The Compiler Frontend: Parsing and Type Checking)”中进一步了解包裹库和模块别名。虽然我们的 hello 库目前只包含一个 Msg 模块,但在更大的项目中,每个库包含多个模块很常见。hello 库中的其他模块可以直接引用 Msg。

当决定要链接哪些库时,必须在 dune 文件中引用库名,而不是单个模块名。安装 ocamlfind 后,可以在命令提示符下通过 ocamlfind list 查询当前 switch 中已经安装的库:

$ ocamlfind list
afl-persistent (version: 1.2)
alcotest (version: 1.5.0)
alcotest.engine (version: 1.5.0)
alcotest.stdlib_ext (version: 1.5.0)
angstrom (version: 0.15.0)
asn1-combinators (version: 0.2.6)
...

如果 dune 的库定义中存在 public_name 字段,它会决定该库公开暴露的名称。公共库名就是其他项目使用你的库时在 libraries 字段中指定的名称。如果没有公共名称,则定义出的库只在当前 dune 项目内部可见。因为这是一个简单的独立库,所以 lib/dune 文件中的 (libraries) 字段为空。

22.1.6 为库编写测试用例(Writing Test Cases for a Library)​

下一步是在 test/dune 中为我们的库定义一个测试用例。在第 18 章“测试(Testing)”中,我们展示了如何使用 inline test 机制把测试嵌入库中。本节会展示如何使用 dune 的 test stanza 创建一个仅用于测试的可执行程序。当你不使用 inline test 时,这种方式很有用。

先在 test/hello.ml 中把测试写成一个简单断言。

let () = assert (String.equal Hello.Msg.greeting "Hello World")

我们可以使用 dune 的 test stanza 构建一个可执行二进制文件。当你调用 dune runtest 时,这个二进制文件会被运行,同时也会运行库中定义的任何 inline test。我们还会加入对本地定义的 hello 库的依赖,以便访问它。test/dune 文件如下:

(test
(libraries hello)
(name hello))

通过 dune runtest 运行测试后,可以在项目 checkout 的 _build/default/test/ 中找到构建产物。

$ ls -la _build/default/test
total 992
drwxr-xr-x 7 avsm staff 224 27 Feb 16:13 .
drwxr-xr-x 9 avsm staff 288 27 Feb 15:23 ..
drwxr-xr-x 4 avsm staff 128 27 Feb 15:23 .hello.eobjs
drwxr-xr-x 3 avsm staff 96 27 Feb 16:12 .merlin-conf
-r-xr-xr-x 1 avsm staff 495766 27 Feb 16:13 hello.exe
-r--r--r-- 1 avsm staff 64 27 Feb 16:13 hello.ml
-r--r--r-- 1 avsm staff 28 27 Feb 15:23 hello.mli

我们刻意在 lib/ 和 test/ 中都定义了名为 hello.ml 的文件。在 OCaml 库 hello(位于 lib/)旁边定义一个可执行程序 hello.exe(位于 test/)完全没问题。

22.1.7 构建可执行程序(Building an Executable Program)​

最后,我们希望能从命令行实际使用这个 hello world。这在 bin/dune 中定义,方式与测试用例非常相似。

(executable
(public_name hello)
(name main)
(libraries hello)))

bin/dune 文件旁边必须有一个 bin/main.ml,它表示可执行程序的入口模块。只有这个模块,以及它依赖的模块和库,会被链接进可执行程序。与库类似,这里的 (name) 字段必须遵循 OCaml 模块命名约定;public_name 字段表示安装到系统上的二进制文件名,只需要是一个有效的 Unix 或 Windows 文件名。

现在尝试修改 bin/main.ml,让它引用我们的 Hello.Msg 模块:

let () = print_endline Hello.Msg.greeting

可以使用 dune exec 和可执行程序的本地名称,在本地构建并执行这个命令。也可以在 _build/default/bin/main.exe 中找到构建出的可执行文件。

$ dune build
$ dune exec -- bin/main.exe
Hello World

如果更方便,也可以引用可执行程序的公共名称。

$ dune exec -- hello
Hello World

本章前面提到的 dune exec 和 opam exec 命令可以相互嵌套,因此你可以使用双短横线指令把它们追加在一起,用来分隔命令。

$ opam exec -- dune exec -- hello --args

在与持续集成系统集成时,这是一种相当常见的做法,因为这些系统通常需要对 opam 和 dune 进行系统化脚本调用。本章稍后会讨论这个主题。

22.2 设置集成开发环境(Setting Up an Integrated Development Environment)​

既然已经看过 OCaml 项目的基本结构,现在就该设置集成开发环境了。IDE 对 OCaml 尤其有用,因为它能让你利用 OCaml 丰富类型系统所提取出的信息。一个好的 IDE 会提供浏览接口文档、查看代码推断类型、跳转到外部模块定义等功能。

22.2.1 使用 Visual Studio Code(Using Visual Studio Code)​

对于刚接触 OCaml 的用户,推荐的 IDE 是 Visual Studio Code,并搭配 OCaml Platform 插件。该插件使用 Language Server Protocol 与你的 opam 和 dune 环境通信。你只需要通过 opam 安装 OCaml LSP server:

opam install ocaml-lsp-server

安装后,VS Code 的 OCaml 插件会询问你要使用哪个 opam switch。只使用默认 switch 就足以开始构建并浏览接口。

注:什么是 Language Server Protocol?

Language Server Protocol 定义了一种通信标准,用于在编辑器或 IDE 与特定语言的服务器之间通信。这个语言服务器会提供自动补全、定义搜索、引用索引以及其他需要语言工具专门支持的功能。

这使得编程语言工具链只需实现一次所有这些功能,就能干净地集成进如今多种多样的 IDE 环境,甚至还能超越传统桌面环境,进入 Jupyter 这样的 Web 笔记本。

由于 OCaml 已经有一个完整且成熟的 LSP server,你会发现,只要安装了 ocaml-lsp-server,越来越多 IDE 就可以开箱即用地支持 OCaml。它会与本书使用过的各种工具自动集成,例如检测 opam switch、调用 dune 规则,等等。

22.2.2 浏览接口文档(Browsing Interface Documentation)​

OCaml LSP server 知道如何与 dune 交互,并检查构建产物,例如带类型的 .cmt 接口文件。因此,只要在 VS Code 中打开本地项目,就足以激活所有功能。试着打开 bin/main.ml,其中会看到对 hello 库的调用。

let () = print_endline Hello.Msg.greeting

首先构建项目,以生成类型标注文件。然后把鼠标悬停在 Hello.Msg.greeting 函数上,应该会看到一些关于这个函数及其参数的文档弹出。这些信息来自 hello 库中写入 msg.mli 接口文件的文档字符串(docstring)。

把 msg.mli 接口文件修改成下面这样,加入一些签名文档:

(** This is a docstring, as it starts with "**", as opposed to normal
comments that start with a single star.

The top-most docstring of the module should contain a description
of the module, what it does, how to use it, etc.

The function-specific documentation located below the function
signatures. *)

(** This is the docstring for the [greeting] function.

A typical documentation for this function would be:

Returns a greeting message.

{4 Examples}

{[ print_endline greeting ]} *)
val greeting : string

文档字符串会由 odoc 工具解析,用于从一组 opam 包生成 HTML 和 PDF 文档。如果你希望自己的代码被别人使用,或者只是几个月后的自己使用,也应该花时间为 OCaml 签名文件加上文档标注。预览 HTML 文档的一种简单方法,是用 dune 在本地构建:

$ opam install odoc
$ dune build @doc

这会把 HTML 文件留在 _build/default/_doc/_html 中,你可以用 Web 浏览器正常查看。

22.2.3 自动格式化源代码(Autoformatting Your Source Code)​

随着你开发更多 OCaml 代码,你会发现把代码格式化为统一风格很方便。ocamlformat 工具可以帮助你在 VS Code 中轻松完成这件事。

$ echo 'version=0.20.1' > .ocamlformat
$ opam install ocamlformat.0.20.1

.ocamlformat 文件控制可用的自动格式化选项,并固定使用的工具版本。你可以在任何时候升级到更新的 ocamlformat 版本,但这是一个手动过程,以避免上游发布在没有你干预的情况下自动重新格式化项目代码。可以通过 ocamlformat --help 查看格式化选项,大多数时候默认设置就很好。

配置好 ocamlformat 后,你既可以在 VS Code 中格式化项目,默认快捷键是 shift-alt-F,也可以运行:

$ dune build @fmt

这会在构建目录中生成一组重新格式化后的文件,你可以像在测试章节中那样用 dune promote 接受它们。

22.3 在线发布你的代码(Publishing Your Code Online)​

设置好 IDE 后,你很快就会开发出有用的 OCaml 代码,并想与他人分享。接下来我们会介绍如何定义 opam 包、设置持续集成以及发布代码。

22.3.1 定义 Opam 包(Defining Opam Packages)​

参与开源 OCaml 生态时,源代码树中真正必须存在的唯一元数据文件是一个 opam 文件。每个 opam 文件都会定义一个包(package),也就是一组 OCaml 库、可执行二进制文件或应用数据。每个 opam 包都可以定义对其他 opam 包的依赖,并包含项目的构建与测试说明。当你最终发布这个包,而其他人输入 opam install hello 时,安装的就是这些内容。

一组 opam 文件可以存放在一个 opam repository 中,从而创建一个包数据库。OCaml 生态的中心仓库位于 ocaml/opam-repository。用于操作 opam 文件的官方工具,也是非唯一工具,就是我们在本书中一直使用的同名 opam 包管理器。

注:OCaml 模块、库和包该如何命名?

大多数时候,模块名、库名和包名都相同。但也有一些理由让这些名称彼此不同:

  • 有些库会暴露多个顶层模块,因此需要为这一组模块使用不同名称。
  • 即使库只有一个顶层模块,你也可能希望库名不同于模块名,以避免库级别的名称冲突。
  • 如果一个包把多个库和/或二进制文件组合在一起,包名可能不同于库名。

在更大的项目中工作时,理解模块、库和包之间的区别很重要。这类项目在单个代码库中可能轻易拥有数千个模块、数百个库和几十个 opam 包。

22.3.2 从 Dune 生成项目元数据(Generating Project Metadata from Dune)​

示例项目中的 hello.opam 文件目前是空的,但你不需要手写它。相反,我们可以使用 dune 构建系统定义项目元数据,并让 opam 文件自动生成。由 dune 构建的 OCaml 项目,其根目录中会有一个 dune-project 文件,用来定义项目元数据。在我们的示例项目中,它以如下内容开头:

(lang dune 3.0)

上面这一行表示构建文件所使用的语法版本,不是 dune 二进制文件的实际版本。dune 最好的特性之一,是它会对旧元数据保持向前兼容。通过定义当前使用的 dune 语言版本,未来版本的 dune 会尽力模拟当前行为,直到你选择升级项目。

dune-project 文件的其余部分定义了其他有用的项目元数据:

(name hello)
(documentation "https://username.github.io/hello/")
(source (github username/hello))
(license ISC)
(authors "Your name")
(maintainers "Your name")
(generate_opam_files true)

这里的字段都表示项目元数据,范围从文本描述到项目 URL,再到其他 opam 包依赖。继续编辑上面的元数据,使其反映你自己的详细信息,然后构建项目:

$ dune build

构建命令会更新源代码树中的 hello.opam 文件,让它与你的修改保持同步。dune-project 文件的最后一部分包含项目所依赖的其他包的信息。

(package
(name hello)
(synopsis "A short description of the project")
(description "A short description of the project")
(depends
(ocaml (>= 4.08.0))
(alcotest :with-test)
(odoc :with-doc)))

这里的 (package) stanza 指的是 opam 包,包括名称和依赖规格。相比之下,dune 文件引用的是 ocamlfind 库,因为这些库代表 OCaml 代码的编译单元,而 opam 包是范围更广的包数据集合。

注意,依赖规格还可以包含版本信息。opam 的关键特性之一是,每个仓库都包含同一个包的多个版本。opam CLI 内置一个约束求解器,会找出所有与当前项目兼容的依赖版本。因此,当你添加依赖时,可以根据项目对该包的使用方式指定必要的下限和上限版本约束。with-test 和 with-doc 是进一步的约束,分别表示只为测试和文档生成添加这些依赖。

定义好 opam 和 dune 依赖后,可以运行各种 lint 命令检查元数据是否一致。

$ opam dune-lint
$ opam lint

opam-dune-lint 插件会检查 dune 文件中的 ocamlfind 库和 opam 包是否匹配,如果发现不匹配,还会提出修复建议。opam lint 会对项目中的 opam 文件运行额外检查。

22.3.3 设置持续集成(Setting up Continuous Integration)​

定义好项目元数据后,就是开始把项目托管到网上的好时机。两个最流行的平台是 GitHub 和 GitLab。为简单起见,本章余下部分会假设你使用 GitHub,不过也鼓励你了解替代方案,为自己的需求找到最佳解决方案。

创建 GitHub 仓库并把代码推送上去后,你还可以添加一个 OCaml GitHub Action。它会安装 OCaml 平台工具,并在各种架构和操作系统上运行你的代码。完整文档可以在 GitHub Marketplace 的 Set up OCaml 页面找到。配置 action 很简单,只需在项目中添加一个类似下面的 .github/workflows/test.yml 文件:

name: Hello world workflow
on:
pull_request:
push:
jobs:
build:
strategy:
matrix:
os:
- macos-latest
- ubuntu-latest
- windows-latest
ocaml-compiler:
- 4.13.x
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use OCaml ${{ matrix.ocaml-compiler }}
uses: ocaml/setup-ocaml@v2
with:
ocaml-compiler: ${{ matrix.ocaml-compiler }}
- run: opam install . --deps-only --with-test
- run: opam exec -- dune build
- run: opam exec -- dune runtest

这个 workflow 文件会在 Windows、macOS 和 Linux 上,使用 OCaml 4.13 的最新补丁版本运行你的项目。注意,它还会在所有这些不同操作系统上运行你前面定义的测试用例。你可以大量定制这些持续集成 workflow,因此请查阅在线文档了解更多选项。

22.3.4 其他约定(Other Conventions)​

你可能还希望向项目中添加其他一些文件,以符合常见约定:

  • Makefile 包含常见动作的目标,例如 all、build、test 或 clean。使用 VS Code 时不需要它,但一些其他操作系统包管理器可能会受益于它的存在。
  • LICENSE 定义你的代码按什么条款提供。示例项目默认使用宽松的 ISC 许可证,除非你对项目有特定计划,否则这通常是一个安全默认值。
  • README.md 是用 Markdown 格式编写的库或应用介绍。
  • .gitignore 文件包含 OCaml 工具生成文件的模式,让这些文件可以被 Git 版本控制软件忽略。如果你不熟悉 Git,可以看一看 GitHub 的 git hello world 之类的教程。

22.3.5 把代码发布到 Opam Repository(Releasing Your Code into the Opam Repository)​

一旦持续集成通过,你就可以尝试为项目打一个 release 标签,并与其他用户分享它了。OCaml 平台提供了一个名为 dune-release 的便利工具,可以为你自动化这个流程的大部分工作。

$ opam install dune-release

首先需要做的是在项目中创建一个 Markdown 格式的 CHANGES.md 文件,其中每个版本都有一个标题。它通常简明总结版本之间的变化,供用户阅读。对于第一次发布,我们可能会写:

## v1.0.0

- Initial public release of our glorious hello world
project (@avsm)
- Added test cases for making sure we do in fact hello world.

把这个文件提交到仓库根目录。继续发布之前,需要确保所有本地修改都已经推送到远程 GitHub 仓库,并且工作树干净。可以用 git 完成这件事:

$ git clean -dxf
$ git diff

这会从本地 checkout 中移除所有未跟踪文件,例如 _build 目录,并检查已跟踪文件没有修改。现在应该可以执行发布了。首先创建一个 git tag 来标记此次 release:

$ dune-release tag

它会解析 CHANGES.md 文件,找出最新版本,并在提示你确认后在仓库中创建本地 git tag。成功后,可以通过下面的命令启动发布流程:

$ dune-release

这会开始一个交互式会话,你需要输入一些 GitHub 认证细节,也就是创建一个 personal access token。完成后,该工具会运行所有本地测试,生成文档,并把文档上传到该项目的 GitHub Pages 分支,最后会提出为中心 opam-repository 打开一个 pull request。回想一下,中心 opam 包集合本质上只是一个普通 git 仓库,因此你的 opam 文件会被添加到那里,并由你的 GitHub 账户创建 PR。

到这里,你可以稍作休息,等待中心 opam repository 的测试系统对你的包运行一组安装测试。其中甚至包括你可能无法访问的异构架构,例如 S390X 大型机或 32 位 ARMv7。如果检测到问题,OCaml 社区中友好的维护者会在 pull request 上评论,并指导你如何解决。你可以直接删除 git tag,然后重新运行发布流程,直到包被合并。包合并后,大约一小时内就可以访问 ocaml.org 网站在线查看它。其他用户也可以从中心仓库安装它。

注:为项目创建锁文件

发布项目之前,也可以创建一个 opam 锁文件并把它包含进归档。锁文件会记录生成它时所有传递 opam 依赖的确切版本。你只需要运行:

$ opam lock

这会生成一个 pkgname.opam.locked 文件,其中包含与原始文件相同的元数据,但所有依赖都会被显式列出。之后,如果用户想重建你精确的 opam 环境,而不是用未来的 opam repository 计算出的包解,他们就可以在安装时传入一个选项:

$ opam install pkgname --locked
$ opam switch create . --locked

锁文件不是必需的,但在把项目发布到 Internet 时,它是一个有用步骤。

22.4 从真实项目中学习更多(Learning More from Real Projects)​

任何真实项目中都会有更多定制,本书无法覆盖每个方面。到目前为止,学习更多内容的最佳方式,是深入一个已经建立起来的项目,编译它,甚至尝试为它做贡献。opam repository 中发布了成千上万个库和可执行项目,你可以在 ocaml.org 在线找到它们。

下面是其中一些例子:

  • patdiff 是 patience diff 算法的 OCaml 实现,也是一个使用 Core 的、不错的自包含 CLI 项目:janestreet/patdiff
  • 本书源码以自包含 monorepo 的形式发布,并把所有依赖捆绑在一起,便于编译:realworldocaml/book
  • Flow 是用 OCaml 编写的 JavaScript 静态类型检查器,使用 Base,并能在 macOS、Windows 和 Linux 上运行。它是大型、跨平台、CLI 驱动工具的一个好例子:facebook/flow
  • Octez 是名为 Tezos 的权益证明区块链的 OCaml 实现,其中包含一组综合库集合,例如栈语言解释器,以及一个使用 Lwt 向外部世界提供网络、存储和加密通信的 shell:tezos/tezos
  • MirageOS 是用 OCaml 编写的库操作系统,可以把代码编译到多种嵌入式和 hypervisor 目标。mirage 上有数百个全部使用 dune、但使用方式各异的库。
  • 你可以在 erratique.ch/software 找到一些独立 OCaml 库,主题涵盖 Unicode、解析、计算机图形以及操作系统交互。