Система сборки 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
└── dune
lib/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))
Подробнее смотрите в 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))))
Перевод некоторых ошибок в предупреждения
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 -w
Release-сборка проекта
Можно так, но он вообще работает?
$ dune build --profile release
# или
$ dune build release
Мем
Уменьшение размера исполняемого файла
По-умолчанию при компиляции генерируется много debug-информации, что существенно увеличивает размер исполняемого файла.
Убрать её можно при помощи утилиты strip
.
Также стоит понимать, что компилятор OCaml не обладает большим количеством оптимизаций и возможностей. Для повышения производительности можно использовать Flambda (про опции сборки компилятора), но оно тоже не столько агрессивно, как GCC.