强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

OCaml 教程 / Dune 构建系统

Dune 构建系统

Dune 是 OCaml 社区的标准构建系统,取代了早期的 ocamlbuild 和 Oasis。它以声明式 DSL 描述构建规则,支持增量构建、并行编译、自动依赖分析,以及与 opam 的深度集成。


dune-project 文件

每个使用 Dune 的项目根目录必须有一个 dune-project 文件:

(lang dune 3.16)

(name my_project)

(generate_opam_files true)

(source
 (github user/my_project))

(authors "Your Name <[email protected]>")

(maintainers "Your Name <[email protected]>")

(license MIT)

(documentation https://user.github.io/my_project/)

(package
 (name my_project)
 (synopsis "Short description")
 (description "Longer description")
 (depends
  (ocaml (>= 4.14))
  (dune (>= 3.0))
  (yojson (>= 2.0))
  (lwt (>= 5.6))))

dune-project 的关键字段:

字段说明
(lang dune 3.16)Dune 语言版本,决定可用的 DSL 特性
(name ...)项目名称
(generate_opam_files true)(package ...) 自动生成 .opam 文件
(package ...)包声明(可多个)

⚠️ 注意(lang dune X.Y) 一旦确定不应轻易升级,高版本 Dune 构建的项目不能用低版本 Dune 构建。


构建规则

可执行文件(executable)

在目录下创建 dune 文件:

(executable
 (name main)
 (public_name my-app)
 (libraries lwt lwt.unix cohttp-lwt-unix))
  • (name main) — 源文件为 main.ml
  • (public_name my-app) — 安装后的可执行文件名(可选)
  • (libraries ...) — 依赖库列表

库(library)

(library
 (name my_lib)
 (public_name my_project.lib)
 (libraries yojson fmt)
 (preprocess (pps ppx_deriving ppx_sexp)))
  • (name my_lib) — OCaml 模块名前缀
  • (public_name my_project.lib) — opam 安装路径(使用 . 分层)
  • (preprocess ...) — PPX 预处理器

测试(test)

(test
 (name test_main)
 (libraries my_lib alcotest))

(tests
 (names test_parse test_eval)
 (libraries my_lib alcotest))

(test) 运行单个测试文件,(tests) 运行多个。

# 运行测试
dune runtest

多目录项目结构

my_project/
├── dune-project
├── bin/
│   ├── dune              # executable
│   └── main.ml
├── lib/
│   ├── dune              # library
│   ├── parser.ml
│   ├── parser.mli
│   ├── eval.ml
│   └── eval.mli
└── test/
    ├── dune              # test
    ├── test_parser.ml
    └── test_eval.ml

bin/dune:

(executable
 (name main)
 (public_name my_project)
 (libraries my_project.lib))

lib/dune:

(library
 (name my_project)
 (public_name my_project.lib)
 (libraries yojson fmt))

test/dune:

(test
 (name test_parser)
 (libraries my_project.lib alcotest))

依赖声明

直接依赖

(library
 (name my_lib)
 (libraries lwt cohttp-lwt-unix))

Dune 通过 ocamlfind(META 文件)或自身包发现机制定位库路径。

运行时依赖(仅测试/可执行)

(test
 (name my_test)
 (libraries my_lib alcotest)
 (deps data/example.json data/config.toml))

(deps ...) 声明文件系统依赖,Dune 会在这些文件变化时重新构建。

可选依赖

(library
 (name my_lib)
 (optional)
 (libraries some_optional_lib))

公共库与私有库

公共库

(public_name ...) 的库可被其他项目通过 opam 安装和使用:

(library
 (name core_engine)
 (public_name my_company.engine)
 (libraries ...))

私有库

不带 (public_name ...),仅供项目内部使用:

(library
 (name internal_utils)
 (libraries fmt))

私有库不能被外部项目引用,也不会被安装。

库包装(wrapped)

默认情况下,Dune 将库的模块"包装"在一个与库同名的顶级模块下:

(library
 (name my_lib)
 (wrapped true))  ; 默认值

使用 my_lib.Parser.parse 访问。设为 (wrapped false) 则模块直接暴露(不推荐)。


PPX 预处理

PPX 是 OCaml 的语法扩展机制,Dune 原生支持:

(library
 (name my_lib)
 (preprocess
  (pps ppx_deriving ppx_deriving_yojson ppx_sexp_conv)))

常用 PPX 列表

PPX 包用途
ppx_deriving自动生成 showeqmake
ppx_deriving_yojson自动 JSON 序列化/反序列化
ppx_sexp_convS-expression 转换(Core 库)
ppx_expect期望测试(Jane Street)
ppx_inline_test内联测试(Jane Street)
ppx_janeJane Street PPX 集合

PPX 运行原理

源码 (.ml) ──▶ PPX 预处理器 ──▶ 展开后代码 ──▶ OCaml 编译器

⚠️ 注意:PPX 调试困难。当 PPX 报错时,使用 dune build --force 查看展开后的中间文件(通常在 _build/default/.../.ppx/ 中)。


Dune 构建命令

# 构建整个项目
dune build

# 构建指定目标
dune build bin/main.exe
dune build @test/runtest

# 运行测试
dune runtest

# 运行可执行文件(带 opam 环境)
dune exec bin/main.exe
dune exec my-app  # 使用 public_name

# 传递参数给可执行文件
dune exec my-app -- --input data.json

# 清理构建产物
dune clean

# 格式化源码
dune fmt

# 查看构建依赖图
dune build --display=short

Watch 模式

# 文件变化时自动重新构建
dune build --watch

# 结合测试
dune runtest --watch

Watch 模式使用 fswatch(macOS)或 inotify(Linux)监听文件系统事件。

💡 提示:在开发时用 dune build --watch 开一个终端窗口,另一个窗口编写代码,实现类似 IDE 的实时反馈。


opam 集成

自动生成 opam 文件

;; dune-project
(generate_opam_files true)

(package
 (name my_lib)
 (depends (ocaml (>= 4.14)) (dune (>= 3.0)) (lwt (>= 5.6))))

运行 dune build 时,Dune 会自动从 (package ...) 规则生成 my_lib.opam

构建发布包

# 使用 opam 的方式构建(CI 中标准做法)
dune build -p my_lib

# -p 标志:
# 1. 忽略非 public 的库和可执行文件
# 2. 只构建包名指定的组件
# 3. 不使用 dev 模式

子项目与 Vendor 库

子项目

大型项目可以包含子项目,每个子项目有自己的 dune-project

monorepo/
├── dune-project           # 根项目
├── service-a/
│   ├── dune-project       # 子项目 A
│   └── src/
├── service-b/
│   ├── dune-project       # 子项目 B
│   └── src/
└── shared/
    ├── dune-project       # 共享库
    └── lib/

Vendor 目录

将第三方库源码嵌入项目(vendoring):

my_project/
├── dune-project
├── vendor/
│   ├── some-lib/
│   │   ├── dune
│   │   └── src/
│   └── another-lib/
│       ├── dune
│       └── src/
└── src/

vendor/some-lib/dune 中使用 (vendored_dirs ...) 声明:

;; 项目根 dune 文件
(vendored_dirs vendor)

Dune 会对 vendor 目录下的代码禁用警告(-w -a),避免第三方代码的警告干扰。


Dune 最佳实践

1. 使用 .mli 接口文件

lib/
├── parser.ml      # 实现
├── parser.mli     # 接口(公开 API)
├── eval.ml
└── eval.mli

接口文件控制模块公开的内容,也是文档的良好载体。

2. 分层构建

project/
├── lib/           # 核心逻辑(纯库)
├── bin/           # 可执行入口(薄壳)
└── test/          # 测试

bin/main.ml 应尽量只做参数解析和调用 lib

(* bin/main.ml *)
let () =
  let config = Config.parse_cli () in
  My_lib.run config

3. 使用 (inline_tests) 集成测试

(library
 (name my_lib)
 (libraries fmt)
 (inline_tests))

my_lib.ml 中:

let%test "double" = double 3 = 6

let%test_unit "print" =
  Format.printf "testing@."

需要安装 ppx_inline_test

4. 固定 Dune 版本

dune-project 中显式声明 Dune 版本约束:

(lang dune 3.16)

并在 .opam 文件中:

depends: [
  ("dune" >= "3.16")
]

业务场景

场景:Monorepo 统一构建

company-monorepo/
├── dune-project          # 统一项目文件
├── libs/
│   ├── auth/             # 认证库
│   │   ├── dune
│   │   └── auth.ml
│   ├── db/               # 数据库库
│   │   ├── dune
│   │   └── db.ml
│   └── web/              # Web 框架
│       ├── dune
│       └── web.ml
├── services/
│   ├── api-server/       # API 服务
│   │   ├── dune
│   │   └── main.ml
│   └── scheduler/        # 调度服务
│       ├── dune
│       └── main.ml
└── test/
    └── integration/
        ├── dune
        └── test_api.ml

一个 dune build 构建所有服务,dune runtest 跑所有测试。


扩展阅读