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 | 自动生成 show、eq、make 等 |
ppx_deriving_yojson | 自动 JSON 序列化/反序列化 |
ppx_sexp_conv | S-expression 转换(Core 库) |
ppx_expect | 期望测试(Jane Street) |
ppx_inline_test | 内联测试(Jane Street) |
ppx_jane | Jane 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 跑所有测试。