Skip to content

Система сборки Dune

Dune - самое популярное решение для сборки OCaml-проектов, оно глубоко интегрировано в экосистему языка, обладает современным функционалом (на подобие инкрементальной сборки, параллельности) и позволяет собирать как исполняемые программы, так и библиотеки, вызывать тесты.

Как начать использовать?

Рекомендуем начать с туториала Your First OCaml Program, после чего обращаться к документации для получения полной справки.

Управление зависимостями

Стоит понимать, Dune не занимается управлением пакетами (зависимостями). Для этого используется пакетный менеджер OPAM.

Базовые понятия

Разработка происходит в рамках проекта, корень проекта определяется по файлу dune-project. Проект в свою очередь состоит из компонентов.

КомпонентОписаниеdune
ExecutableСодержит непосредственно исполняемый код(executable ..)
LibraryКод для использования другими компонентами(library ..)
TestСодержит тесты для компонентов(test ..)

Компоненты

У компонента всегда есть имя (строфа name) по которому можно обращаться внутри проекта. Если вы хотите сделать компонент публичным, то вы должны дать ему публичное имя (строфа public_name) и указать в качестве пакета в dune-project (если требуется).

Зависимости между компонентами

Если ваши компоненты зависят друг от друга, то явно указывайте к какому пакету они относятся. В противном случае Dune будет выдавать ошибку с просьбой сделать подключаемый компонент публичным при попытки подключить приватный библиотечный компонент к публичному библиотечному.

Публичные имена

Публичные имена могут иметь символы-разделители, такие как - или ., использование которых является распространенной практикой.

Пример из Lwt:

Dune
(library
 (name lwt_unix)
 (public_name lwt.unix)

Пример из Cohttp:

Dune
(library
 (name cohttp_eio)
 (public_name cohttp-eio)

Приватный библиотечный компонент для библиотечного пакета

Если вы пишите библиотечный пакет и хотите иметь несколько "приватных" компонентов, от которых зависите, то вам надо прописать к какому пакету относятся эти компоненты.

(package <package-name>)
Пример
.
├── dune-project
├── foo
│   ├── dune
│   └── foo.ml
├── hello_world.opam
└── lib
    └── dune

lib/dune

Dune
(library
 (public_name hello_world)
 (libraries foo))

foo/dune

Dune
(library
  (name foo)
  (package hello_world))

Исполняемый и библиотечный компонент с одним именем

Если вы пишите библиотеку, то может быть удобным также сделать её в виде CLI утилиты. Например, CLI утилита для библиотеки HTTP-клиента.

Как это сделать?

Пример

lib/dune

Dune
(library
 (public_name foo))

bin/dune

Dune
(executable
 (name main)
 (public_name foo)
 (libraries foo))

dune-project

...
(package
 (name foo)
 ...)

Автоматическое форматирование

Смотрите статью про форматтер ocamlformat.

Чтения файлов в тестах

Распространённый кейс, когда в тестах вы читаете какой-нибудь файл. Если вы попробуете это сделать, то получите ошибку о том, что файл не найден, ибо этот файл не находится в каталоге _build.

Пример каталога с тестом:

test/
├── data.test.txt
├── dune
└── test_demo.ml
ocaml
(* test_demo.ml *)
let () = open_in "data.test.txt" |> In_channel.input_all |> print_endline
sh
$ dune runtest
File "test/dune", line 2, characters 7-16:
2 |  (name test_demo))
           ^^^^^^^^^
Fatal error: exception Sys_error("data.test.txt: No such file or directory")

Для исправления этого в файле dune вы должны указать зависимости в поле deps.

Dune
(test
 (name test_demo)
 (deps data.test.txt))

Подробнее смотрите в Dependency Specification.

Зависимости при установки

Dune умеет в установку скомпилированных артефактов в систему, но помимо бинарника надо иногда иметь и сторонние ресурсы. Например, HTML-странички в случае веб-сайта.

Для этого существует строфа install в dune файле. Пример:

Dune
(install
 (files hello.txt)
 (section share)
 (package mypackage))

В этом примере файл hello.txt будет установлен по пути <prefix>/share/mypackage.

За подробностями читайте мануал.

Поддиректории

Если вам нужно иметь внутри компонента древовидную структуру файлов, то об этом надо будет явно сообщить посредством строфы include_subdirs.

Открытие модуля для всего проекта

Тоже самое, что ocamlc -open <module>. Может быть полезным, например, при использовании альтернативной стандартной библиотеке, вроде Base.

Dune
(env (_ (flags (:standard -open Base))))

Перевод некоторых ошибок в предупреждения

Dune по-умолчанию очень строг, но иногда хотелось бы сделать его мягче. Например, разрешить unused-var-strict, unused-value-declaration и т.д..

Это можно сделать при помощи флага -warn-error:

Dune
(env (dev (flags :standard -warn-error -27-32)))

.opam.template

Если вы используете автогенерацию .opam манифеста, то для добавления дополнительных значений (например, pin-depends) или переопределения существующих вам нужно создать шаблонный файл, который будет включаться в сгенерированный манифест.

Файл должен называться как <пакет>.opam.template (название аналогично <пакет>.opam).

Из оф. документации:

(package) stanzas do not support all opam fields or complete syntax for dependency specifications. If the package you are adapting requires this, keep the corresponding opam fields in a pkg.opam.template file. See Packages.

Смотрите пример использования: переопределение, новые поля.

Интеграция с LSP

Реализация языкового сервера OCaml использует генерируемые при сборки Dune'ой файлы для своей работы. Это можно заметить при создания нового файла, который редактор будет помечать красным с просьбой обновить кеш (то есть собрать проект для получения необходимой информации о новом файле).

Поэтому для повышения отзывчивости вы можете воспользоваться командой

sh
$ dune build @check -w

Release-сборка проекта

Можно так, но он вообще работает?

sh
$ dune build --profile release
# или
$ dune build release
Мем

Уменьшение размера исполняемого файла

По-умолчанию при компиляции генерируется много debug-информации, что существенно увеличивает размер исполняемого файла.

Убрать её можно при помощи утилиты strip.

Также стоит понимать, что компилятор OCaml не обладает большим количеством оптимизаций и возможностей. Для повышения производительности можно использовать Flambda (про опции сборки компилятора), но оно тоже не столько агрессивно, как GCC.