第 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 descriptiondefault 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、解析、计算机图形以及操作系统交互。