Nix и Haskell: первая встреча

Всем привет!

Попробовав Nix на вкус, идём далее. Хватит нам играться с vim, давайте приступим к нашему любимому Haskell!

Готовимся

Для начала нам понадобится cabal-install, без него никак:

ПОЯСНЕНИЕ: В рамках данного цикла статей я не планирую использовать stack.

Готово. Посмотрим, что же у нас получилось, заглянем в окружение:

Естественно, окружение расширилось, теперь мне доступны обе команды, vim и cabal.

Второй инструмент, который нам понадобится, это cabal2nix:

Готово. Назначение этого инструмента я поясню ниже.

Ну хорошо, давайте уже что-нибудь сотворим!

hello

Создадим простейшую программу и назовём её “hello”:

Так, стоп, ghc же у нас есть вроде. Проверяем:

Нет, нету. Установим:

Ок, как вы уже поняли, никакой установки не было, просто появились новые символьные ссылки в моём профиле.

Возвращаемся к проекту. Прохожу диалог с cabal init, всё как обычно, в результате чего получаю простейший проектик:

Никаких зависимостей, кроме как от base, никакой полезной работы, кроме вывода строки “hello”. Всё стандартно и примитивно.

Nix-ификация

А вот теперь начинается самое интересное. Давайте сделаем нашу программку Nix-пакетом. Но вначале необходимо определиться с понятиями.

Когда мы говорим о Nix-пакетах, это отнюдь не то же самое, что, например, deb-пакеты. Как вы уже помните, в основе Nix лежит чисто-функциональных подход к управлению пакетами. Но что ещё важно, Nix - это и особый язык программирования! И когда мы говорим о создании Nix-пакета, мы на самом деле подразумеваем написание маленькой программки на языке Nix. Результатом выполнения этой маленькой программки и будет наш пакет! Это примерно как в Hakyll: чтобы построить статический сайт, нужно написать программу на Haskell, результатом выполнения которой и будет наш сайт.

Язык Nix похож на Haskell, но это не Haskell. Чрезмерно углубляться в детали его синтаксиса мы не станем, тем более что есть уже отличная статья об этом (я уже не говорю про исчерпывающее официальное руководство). Будем вникать в этот язык настолько, насколько это необходимо.

Та самая маленькая программка на языке Nix, на основе которой будет построен наш будущий пакет, должна сохраняться в файле default.nix. Строго говоря, имя файла может быть и другим, но по умолчанию ожидается именно такое имя.

Ок, но как же мы её напишем? Да вот так и напишем:

{ pkgs ? import <nixpkgs> {} }:

let command = name: pathToNix:
        pkgs.runCommand name {} ''
            ${pkgs.cabal2nix}/bin/cabal2nix ${pathToNix} > $out
        '';

    haskellPackages = pkgs.haskellPackages.override {
        overrides = self: _: {
            hello = self.callPackage (command "hello.nix" ./.) {};
        };
    };
in haskellPackages.hello

Пока просто скопируйте это содержимое в файл default.nix. Я специально не стану рассказывать вам о содержимом этого файла, потому что этот вопрос заслуживает одной из будущих статей. Лишь обратите внимание на команду cabal2nix: вот для чего нужна была установка cabal2nix, о которой упомянуто ранее.

Теперь, когда у нас есть программка на языке Nix, нам нужно её собрать и запустить. Но поскольку Nix не является компилируемым языком, нам следует всего лишь передать её на вход команды nix-build - и работа будет сделана. Делается это так:

Обратите внимание, что имя .nix файла не передаётся команде явно. Вот почему нужно было назвать его default.nix: команда nix-build ищет в текущем каталоге файл именно с таким именем.

Вы спросите, а зачем нужен --dry-run? Это - холостой прогон, ничего в действительности не строящий, а лишь показывающий, что произойдёт при постройке. И вот что он нам покажет:

То, что нам и нужно! Когда мы запустим эту команду по-настоящему, наш проектик станет частью /nix/store/, как и все остальные пакеты! Сделаем же это:

Сборка завершится, а последней строкой будет вот что:

Проверим:

Оно. А теперь пробуем запустить нашу программку:

Хм… Установить установили, а запустить не можем. К счастью, в каталоге проекта появилась специальная символьная ссылка result. Именно она и ведёт к нашей программке. Убедимся в этом:

Тот самый хэш. Таким образом, можем запускать следующим образом:

Победа.

Итак, теперь наш пакетик является частью /nix/store/, и поэтому его содержимое теперь неизменно аки египетские пирамиды:

Да, но что же будет, если мы продолжим разработку нашего проекта? Ну, скажем, изменим выводимую на консоль строку. Откроем src/Main.hs и напишем:

Если мы заново соберём этот проект командой nix-build и запустим через result, мы увидим новую строку, как и ожидается. Но тут возникает вопрос: если там, в /nix/store/, проект неизменен, как же произошло обновление? Давайте заглянем:

Как видите, пакетов стало два, и ссылка result ведёт теперь на последнюю, свежую сборку. Вспомните чисто-функциональный подход: значение, будучи однажды созданным, уже не может быть изменено.

Вы спросите, как же быть с накоплениями? Ведь мы, может быть, сделаем сто правок кода, прежде чем перейдём на следующую версию проекта. Это что же, у нас в хранилище появится сто сборок версии 0.1.0.0 с разными префиксами?! Да, именно так. Но вспомните про сборку мусора. Выполним знакомую нам команду:

и тогда всё старьё будет уничтожено, а останется лишь самая последняя сборка, на которую и ведёт ссылка result. Таким образом, если вдруг мы осознали, что наша программа не должна быть частью нашей системы - ну не знаю, передумали её делать - её очень просто удалить. Нужно всего лишь удалить ту самую символьную ссылку result, а затем повторно выполнить команду nix-collect-garbage -d. Всё, программы в хранилище больше нет.

Заключение

Вот наше первое знакомство с Haskell-разработкой в среде Nix. В следующих статьях мы продолжим наше Nix-путешествие и узнаем ещё очень много интересного.