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

Всем привет!

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

Готовимся

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

$ nix-env -iA nixos.pkgs.cabal-install

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

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

$ ls -al /nix/var/nix/profiles/per-user/demo/profile-4-link/bin/
... cabal -> /nix/store/n3dw12h5w0pm3001f5645xqsnqrsv42z-cabal-install-1.22.6.0/bin/cabal
...
... vim -> /nix/store/yblqgyrn4jgwfg89qp9041i0n2z26v5b-vim-7.4.827/bin/vim

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

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

$ nix-env -iA nixos.pkgs.cabal2nix

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

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

hello

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

$ cd

$ cabal init
Config file path source is default config file.
Config file /home/demo/.cabal/config not found.
Writing default configuration to /home/demo/.cabal/config
cabal: The program 'ghc' version >=6.4 is required but it could not be found.

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

$ ghc
ghc: command not found

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

$ nix-env -iA nixos.pkgs.ghc
installing ‘ghc-7.10.2’
building path(s) ‘/nix/store/m209df88gnyqmlczk8g29mwvcbiwbmd0-user-environment’
created 62 symlinks in user environment

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.10.2

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

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

$ tree
.
├── hello.cabal
├── LICENSE
├── Setup.hs
└── src
    └── Main.hs

1 directory, 4 files

Никаких зависимостей, кроме как от 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-build --dry-run

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

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

building path(s) ‘/nix/store/7pxxlhd1ypzikqmznh67igp7vabrlk5w-hello.nix’
these derivations will be built:
  /nix/store/ir5ig6dn02arjwbnkx6m008m3rqa97n2-hello-0.1.0.0.drv

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

$ nix-build

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

/nix/store/jkq9ynd6zsz9rbl1rjb7dbm0rg9dimkb-hello-0.1.0.0

Проверим:

$ ls /nix/store/jkq9ynd6zsz9rbl1rjb7dbm0rg9dimkb-hello-0.1.0.0/bin/
hello

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

$ hello
hello: command not found

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

$ ls -al result
... result -> /nix/store/jkq9ynd6zsz9rbl1rjb7dbm0rg9dimkb-hello-0.1.0.0

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

$ ./result/bin/hello
hello

Победа.

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

$ cd /nix/store/jkq9ynd6zsz9rbl1rjb7dbm0rg9dimkb-hello-0.1.0.0/bin/

$ mv hello hello_
mv: cannot move ‘hello’ to ‘hello_’: Read-only file system

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

main :: IO ()
main = putStrLn "Hi, Nix world!"

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

$ ls /nix/store/ | grep hello
...
jkq9ynd6zsz9rbl1rjb7dbm0rg9dimkb-hello-0.1.0.0
bv51yc31qb70mdb6yzqxsi079ywhrspm-hello-0.1.0.0
...

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

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

$ nix-collect-garbage -d

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

Заключение

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