Система сборки 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:
(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)
...)Автоматическое форматирование
Смотрите статью про форматтер 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.