Система сборки Dune
Dune — composable система сборки OCaml-проектов, ныне является стандартом, входящим в список OCaml Platform. Помимо самой сборки проектов обладает богатым функционалом по работе с экосистемой языка и другими инструментарием.
Как начать использовать?
Рекомендуем начать с туториала Your First OCaml Program, после чего обращаться к документации для получения полной справки.
Управление зависимостями
Стоит понимать, Dune не занимается управлением пакетами (зависимостями). Для этого используется пакетный менеджер OPAM.
Видео-иллюстрация из серии OCaml Tips
TODO
Смотреть на Youtube.
Mental Model
Зачастую разработка большинства проектов происходит в рамках самодостаточного Dune-проекта (существует также Dune-workspace — множество связанных Dune-проектов), корень которого определяется по файлу dune-project.
Проект в свою очередь состоит из компонентов.
| Компонент | Описание | dune |
|---|---|---|
| Executable | Содержит непосредственно исполняемый код | (executable ..) |
| Library | Код для использования другими компонентами | (library ..) |
| Test | Содержит тесты для компонентов | (test ..) |
Компоненты
У компонента всегда есть имя (строфа name) по которому можно обращаться внутри проекта. Если вы хотите сделать компонент публичным (дать доступ к нему не только в рамках проекта), то вы должны дать ему публичное имя (строфа public_name) и, если требуется, указать в качестве пакета в dune-project или напрямую в .opam манифесте.
Зависимости между компонентами
Если ваши приватные компоненты зависят друг от друга, то явно указывайте к какому пакету они относятся! В противном случае Dune будет выдавать ошибку с просьбой сделать подключаемый компонент публичным при попытки подключить приватный библиотечный компонент к публичному библиотечному.
Публичные имена
Публичные имена могут иметь символы-разделители, такие как - или ., использование которых является распространенной практикой.
Примеры из экосистемы
Пример из Lwt:
(library
(name lwt_unix)
(public_name lwt.unix)Пример из Cohttp:
(library
(name cohttp_eio)
(public_name cohttp-eio)Разница между - и . заключается в том, что . не может быть применен в качестве имени для отдельного пакета, точка обозначает поддиректность к проекту.
Приватный библиотечный компонент для библиотечного пакета
Если вы пишите библиотечный пакет и хотите иметь несколько "приватных" компонентов, от которых зависите, то вам надо прописать к какому пакету относятся эти компоненты.
(package <package-name>)Пример
.
├── dune-project
├── foo
│ ├── dune
│ └── foo.ml
├── hello_world.opam
└── lib
└── dunelib/dune
(library
(public_name hello_world)
(libraries foo))foo/dune
(library
(name foo)
(package hello_world))Исполняемый и библиотечный компонент с одним именем
Если вы пишите библиотеку, то может быть удобным также сделать её в виде CLI утилиты. Например, CLI утилита для библиотеки HTTP-клиента.
Как это сделать?
Пример
lib/dune
(library
(public_name foo))bin/dune
(executable
(name main)
(public_name foo)
(libraries foo))dune-project
...
(package
(name foo)
...)Правила и Действия
Разговор про компоненты это разговор в самой высокой плоскости, более фундаментальными абстракциями для Дюны в конечном счете являются так называемые rules (правила).
A rule reads dependencies and writes targets using an action (and it can be attached to aliases).
Пример простого правила
(rule
(target a.out)
(deps main.ml)
(action
(run ocamlopt main.ml)))Правила могут зависеть от других правил и исполняются они при этом инкрементально, то есть исполняются при изменении своих зависимостей.
Platform-depend select
Если у вас есть логика, зависящая от конкретной платформой, то смотрите возможность Alternative Dependencies, select-механизм.
(select <target-filename> from
(<literals> -> <filename>)
(<literals> -> <filename>)
...)Пример из экосистемы
ocaml-crypt — A tiny binding for the unix crypt function.
(library
(public_name crypt)
(libraries
(select
ffi.ml
from
(if_is_linux_or_freebsd -> ffi.posix.extended.ml)
...
...)
(library
(name if_is_linux_or_freebsd)
(modules)
(package crypt)
(enabled_if
(or
(= %{system} "linux")
(= %{system} "freebsd"))))Автоматическое форматирование
Смотрите статью про форматтер ocamlformat.
Чтения файлов в тестах
Распространённый кейс, когда в тестах вы читаете какой-нибудь файл. Если вы попробуете это сделать, то получите ошибку о том, что файл не найден, ибо этот файл не находится в каталоге _build.
Пример каталога с тестом:
test/
├── data.test.txt
├── dune
└── test_demo.ml(* test_demo.ml *)
let () = open_in "data.test.txt" |> In_channel.input_all |> print_endline$ 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.
(test
(name test_demo)
(deps data.test.txt)) // [!code ++]Подробнее смотрите в Dependency Specification.
Зависимости при установки
Dune умеет в установку скомпилированных артефактов в систему, но помимо бинарника надо иногда иметь и сторонние ресурсы. Например, HTML-странички в случае веб-сайта.
Для этого существует строфа install в dune файле. Пример:
(install
(files hello.txt)
(section share)
(package mypackage))В этом примере файл hello.txt будет установлен по пути <prefix>/share/mypackage.
За подробностями читайте мануал.
Поддиректории
Если вам нужно иметь внутри компонента древовидную структуру файлов, то об этом надо будет явно сообщить посредством строфы include_subdirs.
Открытие модуля для всего проекта
Тоже самое, что ocamlc -open <module>. Может быть полезным, например, при использовании альтернативной стандартной библиотеке, вроде Base.
(env (_ (flags (:standard -open Base))))Встраивание ресурсов
https://dune.readthedocs.io/en/latest/howto/bundle.html
(rule
(with-stdout-to
css.ml
(progn
(echo "let css = {|")
(cat resources/site.css)
(echo "|}"))))$ tree src
src
└── lib
└── my_lib
├── dune
└── resources
└── site.csslet () = Printf.printf "%s" Css.cssСмотрите также про установку зависимых артефактов.
Загрузка printers в Toplevel
Смотрите также UTop printers.
let eval code =
let as_buf = Lexing.from_string code in
let parsed = !Toploop.parse_toplevel_phrase as_buf in
ignore (Toploop.execute_phrase true Format.std_formatter parsed)
let () =
eval {|#require "yourlib";;|};
eval "#install_printer yourlib.pp_something;;"(library
(name lib_top)
(public_name lib.top)
(modes byte)
(wrapped false)
(libraries compiler-libs.common))Перевод некоторых ошибок в предупреждения
Dune по-умолчанию очень строг, но иногда хотелось бы сделать его мягче. Например, разрешить unused-var-strict, unused-value-declaration и т.д..
Это можно сделать при помощи флага -warn-error:
(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'ой файлы для своей работы. Это можно заметить при создания нового файла, который редактор будет помечать красным с просьбой обновить кеш (то есть собрать проект для получения необходимой информации о новом файле).
Поэтому для повышения отзывчивости вы можете воспользоваться командой
$ dune build @check -wRelease-сборка проекта
Можно так, но он вообще работает?
$ dune build --profile release
# или
$ dune build releaseМем

Уменьшение размера исполняемого файла
По-умолчанию при компиляции генерируется много debug-информации, что существенно увеличивает размер исполняемого файла.
Убрать её можно при помощи утилиты strip.
Также стоит понимать, что компилятор OCaml не обладает большим количеством оптимизаций и возможностей. Для повышения производительности можно использовать Flambda (про опции сборки компилятора), но оно тоже не столько агрессивно, как GCC.