Предисловие

В 2014 и 2015 годах Люка Бруно (Luca Bruno aka Lethalman) опубликовал серию постов про пакетный менеджер Nix, операционную систему NixOS и хранилище Nixpkgs.

Люка назвал свои посты пилюлями (англ. pill — таблетка, пилюля).

Берясь за перевод, я пытался выяснить, нет ли у выражения in pills устойчивого смысла. Оказалось, что скрытый смысл есть у слова Nix. Это одна из торговых марок перметрина — средства против клещей, которое доступно только в виде мази. Иными словами, медицинского Никса ни в пилюлях, ни в таблетках не бывает.

С момента публикации, Nix в пилюлях считается классическим введением в Nix. В 2017 году Грэм Кристиансен (Graham Christensen aka grahamc/gchristensen) инициировал работу по переводу серии статей в формат электронной книги.

Актуальную оригинальную версию книги вы найдёте по адресу https://nixos.org/guides/nix-pills/. Там же доступен вариант в формате EPUB.

В 2024 году Марк Шевченко начал перевод книги на русский язык. Актуальная версия доступна по адресу https://nix-pills-ru.github.io.

ℹ️ В примерах, команды, которые начинаются с символа “решётка” (#), должны быть запущены с правами пользователя root.

Почему вам стоит попробовать Nix

Введение

Добро пожаловать на первую пилюлю из цикла «Nix в пилюлях». Nix — это чистый функциональный пакетный менеджер и система развёртывания для POSIX-совместимых ОС.

Есть немало материалов, посвящённых Nix, NixOS и связанным проектам. И, возможно, вы даже не стали бы их читать, так что цель этой статьи — убедить вас попробовать Nix. Установка NixOS не потребуется, но иногда я буду ссылаться на NixOS, как на реальный пример операционной системы, построенной на базе Nix.

Почему появился этот цикл?

Руководства по Nix, Nixpkgs и NixOS вместе с вики — великолепные ресурсы, объясняющие как устроены Nix/NixOS, как их использовать, и насколько крутые штуки можно делать с их помощью.

Цель этих статей — дополнить существующие документы чуть менее формальными объяснениями.

А теперь давайте познакомимся с Nix. Пилюли бывают горькими, поэтому постараемся закончить всё как можно быстрее.

Когда пакетный менеджер — не чистый функциональный

Большинство, если не все, популярные пакетные менеджеры (dpkg, rpm, …) изменяют глобальное состояние системы. Установив пакет foo-1.0 в каталог /usr/bin/foo, вы не сможете установить туда же новую версию foo-1.1, т.к. имя исполняемого файла будет одно и то же. Впрочем, его изменение может ввести в заблуждение пользователей.

Есть разные способы избежать этой проблемы. Скажем, Debian частично решает её с помощью системы альтернатив.

Поэтому в теории можно установить несколько версий одного пакета, но на практике этот опыт может быть болезненным.

Скажем, вам нужен сервис nginx и — кроме него — сервис nginx-openresty. Вы должны создать новый пакет, и поменять в нём все пути, например, добавив к ним суффикс -openresty.

Или, представим, вам надо запустить разные версии mysql: 5.2 и 5.5. Возникнет та же свистопляска с путями, что и в предыдущем случае. Кроме того, вам надо будет убедиться, что разные версии библиотеки mysqlclient не конфликтуют друг с другом.

Такая ситуация может возникнуть, и в этом случае справиться с ней будет очень непросто. А если вы захотите установить два различных программных стека, скажем, GNOME 3.10 и GNOME 3.13, вам можно только посочувствовать.

Администраторы скажут: вы можете использовать контейнеры. Типичный подход в наши дни — создать контейнер для каждого сервиса, особенно, если речь идёт о разных версиях. Подход в какой-то степени решает проблему, но на другом уровне и с другими побочными эффектами. Скажем, вам потребутся средства оркестрации, настройка общего кэша пакетов и новые машины для мониторинга всего этого счастья — вместо пары простых сервисов.

Разработчики скажут: вы можете использовать virtualenv для python, или jhbuild для gnome, или что-то подобное для других языков. Но как вы смешаете разные стеки? Как вам избежать перекомпиляции исходников, если их надо сделать общими для нескольких проектов? А ещё вам придётся настроить инструменты разработки, чтобы они загружали библиотеки из правильных каталогов. И, наконец, всегда остаётся риск, что часть софта некорректно работает с системными библиотеками.

В общем, проблем много. Nix решает эти проблемы на уровне пакетов, и решает их хорошо. Один инструмент — чтобы управлять всеми пакетами1.

Когда пакетный менеджер — чистый функциональный

Nix не делает никаких предположений о глобальном состоянии системы. У такого подхода есть много преимуществ, но, конечно, есть и недостатки. Сердцем системы Nix является хранилище, обычно расположенное в /nix/store, а также кое-какие инструменты для работы с ним. В Nix вместо понятия пакет существует понятие деривация2 (derivation). Для новичков различия между ними кажутся слишком тонкими, поэтому я буду использовать эти слова, как синонимы.

Деривации/пакеты находятся в хранилище Nix в подкаталогах, чьи имена соответствуют формату /nix/store/hash-name, где хэш (hash) — уникальный идентификатор деривации (с некоторыми оговорками, на которые мы не будет отвлекаться), а имя (name) — её имя.

Например, взглянем, на деривацию bash: /nix/store/s4zia7hhqkin1di0f187b79sa2srhv6k-bash-4.2-p45/. Это каталог в хранилище Nix, где находится утилита bin/bash.

Фактически это значит, что в системе нет никакой глобальной оболочки, а есть только эта конкретная версия в одном из каталогов хранилища. То же касается и других утилит, да и вообще всего. Чтобы утилиты можно было вызывать из командной строки, Nix следит за тем, чтобы в переменной PATH были правильные пути.

В итоге у нас есть хранилище всех пакетов (разные версии пакетов хранятся в разных каталогах), и всё, что там есть — менять нельзя.

В системе нет даже кэша ldconfig, так что вы вправе спросить: как в таком случае bash находит libc?

$ ldd  `which bash`
libc.so.6 => /nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib/libc.so.6 (0x00007f0248cce000)

Оказывается, когда bash был собран, он был собран с конкретной версией glibc из хранилища Nix, и при запуске он загружает именно эту версию glibc.

Пусть вас не смущает номер версии в имени деривации: это имя для нас, людей. Можно создать две деривации с одним и тем же именем, но разными хэшами: значение имеет только хэш.

Для чего все эти сложности? Благодаря им, теперь можно запускать mysql 5.2 с glibc-2.18 и mysql 5.5 с glibc-2.19. Можно использовать модуль c python 2.7, собранным gcc 4.6 и тот же самый модуль — с python 3, собранным gcc 4.8, в одной и той же системе.

Никакого больше геморроя с зависимостями и даже никакого алгоритма разрешения зависимостей. Прямые зависимости дериваций от других дериваций.

Администраторы скажут: если вам нужна старая версия PHP для одного приложения, но вы хотите обновить всю остальную систему, это можно сделать безболезненно.

Разработчики скажут: если вы хотите разрабатывать webkit и с llvm 3.4, и с llvm 3.3, это можно сделать безболезненно.

Изменяемое против неизменного

При обновлении библиотеки большинство пакетных менеджеров просто перезаписывают файл в каталоге. Потом все приложения просто запускаются с новой версией. Ненадёжно, но кого это волнует? В конце концов, все приложения динамически ссылаются на libc6.so.

Поскольку деривации Nix неизменны (иммутабельны), обновление библиотеки наподобие glibc требует перекомпиляции всех приложений, потому что путь к glibc в хранилище Nix зависит от версии.

Как же нам быть с обновлениями безопасности? В Nix есть несколько трюков (всё ещё чистых), чтобы справиться с этой проблемой, но к ним мы вернёмся позже.

Другая проблема заключается в том, что софт, который рассчитывает на глобальные пути, не так то и просто заставить работать в Nix.

Для примера возьмём Firefox. В большинстве системы вы устанавливаете flash, и он просто начинает работать, потому что Firefox ищет плагины по глобальному пути.

В Nix не существует никого глобального пути для плагинов. Firefox должен точно знать, где находится flash. Мы справляемся с этой проблемой, создавая для Firefox особое окружение, позволяющее найти flash в хранилище Nix. Придётся создать новую деривацию Firefox: это займёт несколько секунд, и сделает настройку чуть более сложной.

Зато вам больше не нужны скрипты обновления/удаления ваших данных. В этом просто нет смысла, потому что не бывает дериваций, которые можно было бы обновлять. В Nix вы переключаетесь на другой софт со своим собственным набором зависимостей, но при этом нет никаких обновлений или удалений.

Если изменился формат данных, то миграция на новый формат — отвественность автора программы.

Заключение

Nix позволяет гибко управлять сборкой программ, делая их настолько воспроизводимыми (идентичными), насколько это вообще возможно. Кроме того, из-за природы Nix, разворачивать приложения в облаке настолько просто, что в мире Nix все инструменты контейнеризации и оркестрации безнадёжно устарели по сравнению с NixOps.

Тем не менее, Nix пока не справляется с динамической компоновкой при работе программ, или с заменой низкоуровневых библиотек, из-за того, что всё это требует перекомпиляции.

Звучит пугающе, но на практике NixOS нормально работает и на сервере, и на десктопе. Да, некоторые архитектурные проблемы ждут своего решения, но всему своё время.

Взглянув на Nixpkgs (ссылка на github) — репозиторий всего существующего софта, построенный с нуля, с непривычным подходом, с небольшим количеством основных разработчиков, но с растущим год от года вкладом сообщества, мы должны признать, что он вышел из стадии эксперимента и находится в прекрасной рабочей форме. Он стоит потраченного на него времени.

В следующей пилюле

…мы установим Nix в вашу систему (предположительно GNU/Linux, но подойдёт и OSX), и начнём его изучать.

1

Отсылка на «Властелина колец».

2

Сначала я решил, что перевод должен быть переводом. Слово деривация уже заимствовано в русском языке, но широко не используется и большинству читателей неизвестно. Так что я выбрал слово порождение, которое передаёт смысл, присутствующий в оригинальном английском термине. Однако, волна возмущения в telegram-чате NixOS RU оказалась настолько высокой, что я вынужден был отказаться от своей идеи. Derivation — ключевое для Nix понятие, которое встречается в названии функций, в сообщениях об ошибках, при поиске в Google — короче, почти везде. Оно похоже на термин file, который гораздо практичнее оказалось просто заимствовать. Мне пришлось переписывать первые восемь пилюль, которые я уже успел перевести, но зато теперь везде в тексте использован термин деривация.

Установка в вашей системе

Добро пожаловать на вторую пилюлю Nix. В первой пилюле мы кратко рассказали про Nix.

Теперь установим Nix в нашу систему и разберёмся, что изменилось после установки.

Если вы используте NixOS, Nix у вас уже установлен, так что вы сразу можете переходить к следующей пилюле.

За инструкциями по установке, пожалуйста, обратитесь к разделу Установка Nix Справочного руководства Nix.

Установка

Эти статьи — не руководство по использованию Nix. Здесь мы будем знакомиться с Nix, не погружаясь в формальности, просто чтобы разобраться, как он устроен.

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

ℹ️ При многопользовательской установке (такая как раз применяется в NixOS), хранилищем владеет root, а многочисленные пользователи могут устанавливать или собирать софт при помощи демона Nix. Больше о многопользовательской установке вы можете прочитать здесь: https://nixos.org/manual/nix/stable/installation/installing-binary.html#multi-user-installation.

С чего начинается хранилище в Nix

Вот что печатает команда установки Nix во время установки:

    copying Nix to /nix/store..........................

Речь идёт о каталоге /nix/store, который мы обсуждали в первой статье. Туда копируется всё, что необходимо для запуска системы Nix. Вы можете заметить bash, утилиты ядра, компилятор C, библиотеки, Perl, sqlite и сам Nix с его утилитами и libnix.

Обратите внимание, что в /nix/store лежат не только каталоги, но и файлы. У них такой же формат имени /hash-name.

База данных Nix

Сразу после наполнения хранилища, процесс установки инициализирует базу данных:

    initialising Nix database...

Да, в Nix есть база данных. Она находится в каталоге /nix/var/nix/db. Это база данных sqlite, которая хранит зависимости между деривациями.

Схема очень простая: есть таблица корректных путей, где каждому пути сопоставлен целый ключ. Он автоматически увеличивается при вставке новых дериваций.

Далее, есть таблица зависимостей, которая обеспечивает связь один-ко-многим, так что, зная деривацию, вы можете выяснить, от каких дериваций она зависит.

Можно исследовать эту базу, установив sqlite (nix-env -iA sqlite -f '<nixpkgs>') и выполнив команду sqlite3 /nix/var/nix/db/db.sqlite.

ℹ️ Сразу после установки Nix не забудьте закрыть и заново открыть терминалы, чтобы обновить настройки командной строки.

📢 Изменяйте /nix/store вручную, только если вы на самом деле знаете, что делаете, или хранилище больше не будет синхронизировано в базой данных sqlite.

Первый профиль

Завершив установку, познакомимся с профилем:

creating /home/nix/.nix-profile
installing 'nix-2.1.3'
building path(s) `/nix/store/a7p1w3z2h8pl00ywvw6icr3g5l9vm5r7-user-environment'
created 7 symlinks in user environment

Профиль в Nix — это удобная концепция, чтобы откатывать изменения. Профили используются для объединения компонентов, разбросанных по разным путям, в одно целое. Более того, у профилей есть версии или «поколения». Когда вы изменяете профиль, рядом со старой версией появляется новая.

Поколения можно переключать и откатывать атомарно, что делает управление изменениями особенно удобным.

Итак, внимательно посмотрим на наш профиль:

$ ls -l ~/.nix-profile/
bin -> /nix/store/ig31y9gfpp8pf3szdd7d4sf29zr7igbr-nix-2.1.3/bin
[...]
manifest.nix -> /nix/store/q8b5238akq07lj9gfb3qb5ycq4dxxiwm-env-manifest.nix
[...]
share -> /nix/store/ig31y9gfpp8pf3szdd7d4sf29zr7igbr-nix-2.1.3/share

Деривация nix-2.1.3 в хранилище Nix — это сам Nix вместе с исполняемыми файлами и библиотеками. Процесс «установки» деривации в профиль, в сущности, воспроизводит дерево nix-2.1.3 из хранилища с помощью символических ссылок.

В настоящий момент в профиль установлена только одна программа (сам Nix), поэтому каталог bin ссылается на каталог bin из nix-2.1.3.

Но ~/.nix-profile — это не реальный каталог, а символическая ссылка на последнее поколение нашего профиля, /nix/var/nix/profiles/default. Которая, в свою очередь, тоже является ссылкой на соседний каталог default-1-link. Так что текущим профилем может быть люоое из поколений, но сейчас он указывает на первое.

В конечном итоге, default-1-link — символическая ссылка на деривацию user-environment, которая печаталась на экране в процесс установки.

О файле manifest.nix мы подробнее поговорим в следующей статье.

Выражения Nixpkgs

Ещё немного вывода от программы установки:

downloading Nix expressions from `http://releases.nixos.org/nixpkgs/nixpkgs-14.10pre46060.a1a2851/nixexprs.tar.xz'...
unpacking channels...
created 2 symlinks in user environment
modifying /home/nix/.profile...

Выражения Nix написаны на языке Nix, они описывают пакеты и процесс их сборки. Nixpkgs — это репозиторий, содержащий все выражения: https://github.com/NixOS/nixpkgs.

Установщик скачал описания пакетов, начиная с коммита a1a2851.

Второй профиль, который есть в системе — это профиль каналов. ~/.nix-defexpr/channels ссылается на /nix/var/nix/profiles/per-user/nix/channels, который в свою очередь ссылается на channels-1-link, который ссылается на каталог в хранилище со скачанными выражениями Nix.

Каналы — это набор пакетов и выражений, доступных для скачивания. Подобно стабильным и нестабильным репозиториям в Debian, в Nix есть есть стабильные и нестабильные каналы.

Позже мы вернёмся к выражениям Nix, а пока закончим с профилями.

В конечном итоге установщик изменил ~/.profile так, чтобы вы попадали в окружение Nix автоматически. Что делает скрипт ~/.nix-profile/etc/profile.d/nix.sh на самом деле, так это добавляет ~/.nix-profile/bin в PATH и ~/.nix-defexpr/channels/nixpkgs в NIX_PATH. Переменную NIX_PATH мы обсудим позже.

Попробуйте в разобраться в скрипте nix.sh, он не очень большой.

Вопросы и ответы: можно ли заменить /nix на что-то другое?

Да, можно, но есть веская причина использовать именно каталог /nix вместо любого другого. Все деривации зависят от других дериваций, используя при этом абсолютные пути. В первой статье мы видели, что bash ссылается на glibc по конкретному абсолютному пути внутри /nix/store.

Убедитесь сами, и не волнуйтесь, если увидите множество дериваций bash:

$ ldd /nix/store/*bash*/bin/bash
[...]

Размещая хранилище в /nix, мы можем напрямую использовать бинарные образы с nixos.org (точно также, как и пакеты с зеркал Debian).

Если же разместить хранилище в каталоге, скажем, /foo/store, то:

  • glibc будет установлен в /foo/store
  • После этого bash будет указывать на glibc в /foo/store вместо /nix/store
  • В результате мы не сможем использовать бинарный образ, так как нам нужен другой bash, и мы вынуждены перекомпилировать вообще всё.

Помимо прочего, /nix — это осмысленное место для хранилища.

Заключение

Мы установили Nix в нашу систему. Он полностью изолирован и принадлежит пользователю nix, а мы продолжим изучать особенности новой системы.

В этой статье мы познакомились с новыми концепциями, такими как профили и каналы. В частности, с помощью профилей мы научились управлять поколениями, а с помощью каналов — загружать бинарные образы с nixos.org.

Программа установки помещает всё в каталог /nix, создавая несколько символических ссылок в домашнем каталоге пользователя Nix.

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

В следующей пилюле

…мы погрузимся в окружение Nix и научимся взаимодействовать с хранилищем.

Погружаемся в среду

Добро пожаловать на третью Nix-пилюлю. Во второй пилюле мы установили Nix в свою систему. Сейчас мы, наконец, устроим пару экспериментов. Эта статья будет полезной, даже если вы используете NixOS, а не просто Nix.

Начинаем погружение

Если вы используете NixOS, то можете пропустить следующий шаг.

В прошлый раз мы создали пользователя Nix. Давайте переключимся на него с помощью команды su - nix. Если ваш ~/.profile уже создан, вам должны быть доступны команды наподобие nix-env и nix-store.

Если нет, запустите:

$ source ~/.nix-profile/etc/profile.d/nix.sh

Ранее мы выяснили, что ~/.nix-profile/etc указывает на деривацию nix-2.1.3. В данный момент мы находимся в профиле пользователя Nix.

Устанавливаем что-нибудь

И вот она, практика! Установка в окружение Nix — интересный процесс. Начнём с hello — простой командной утилиты, которая печатает Hello world и часто используется для проверки компиляторов и пакетных менеджеров.

Итак, установка:

$ nix-env -i hello
installing 'hello-2.10'
[...]
building '/nix/store/0vqw0ssmh6y5zj48yg34gc6macr883xk-user-environment.drv'...
created 36 symlinks in user environment

Теперь программу можно запускать. Прежде, чем двигаться дальше, зафиксируем несколько важных моментов:

  • Мы установили программу с правами пользователя Nix, и только для пользователя Nix.
  • Из-за этого появилось новое окружение пользователя (иногда его называют средой пользователя). Это — новое поколение профиля нашего пользователя Nix.
  • Утилита nix-env управляет окружениями, профилями и их поколенями.
  • Мы установили hello, используя только имя, без указания версии. Повторяю: мы указали только имя деривации (без версии) для установки.

Мы можем вывести список поколений без блужданий по каталогу /nix:

$ nix-env --list-generations
   1   2014-07-24 09:23:30
   2   2014-07-25 08:45:01   (current)

Вывести установленные деривации:

$ nix-env -q
nix-2.1.3
hello-2.10

Куда мы установили hello на самом деле? which hello говорит нам ~/.nix-profile/bin/hello, который указывает на хранилище. Мы также можем вывести путь деривации с помощью команды nix-env -q --out-path. Этот путь деривации называется выходом сборки.

Слияние путей

Сейчас вы, возможно, хотите запустить man чтобы получить кое-какую информацию. Даже если у вас уже есть man в основной системе, вы можете установить её в окружение Nix с помощью команды nix-env -i man-db.

Исследуем профиль:

$ ls -l ~/.nix-profile/
dr-xr-xr-x 2 nix nix 4096 Jan  1  1970 bin
lrwxrwxrwx 1 nix nix   55 Jan  1  1970 etc -> /nix/store/ig31y9gfpp8pf3szdd7d4sf29zr7igbr-nix-2.1.3/etc
[...]

Мы видим кое-что интересное. Когда у нас была установлена только одна деривация nix-2.1.3, bin был символической ссылкой на nix-2.1.3. Теперь, когда мы дополнительно установили несколько программ (man, hello), bin стал реальным каталогом, не симлинком.

$ ls -l ~/.nix-profile/bin/
[...]
man -> /nix/store/83cn9ing5sc6644h50dqzzfxcs07r2jn-man-1.6g/bin/man
[...]
nix-env -> /nix/store/ig31y9gfpp8pf3szdd7d4sf29zr7igbr-nix-2.1.3/bin/nix-env
[...]
hello -> /nix/store/58r35bqb4f3lxbnbabq718svq9i2pda3-hello-2.10/bin/hello
[...]

Так, кое-что стало проясняться. nix-env слил пути из установленных дериваций. which man указывает на профиль Nix, вместо того, чтобы указывать на системный man, потому что ~/.nix-profile/bin находится в начале $PATH1.

Откат и переключение поколений

Последняя установленная команда — это man. Сейчас мы находимся в поколении профиля номер 3, если вы ничего не меняли на предыдущих шагах. Мы можем откатиться к предыдущему поколению:

$ nix-env --rollback
switching from generation 3 to 2

Теперь nix-env -q не показывет man. Команда ls -l \which man`` должна вывести содержимое каталога основной системы.

Разобравшись с откатом, вернёмся обратно в поколение 3:

$ nix-env -G 3
switching from generation 2 to 3

Самое время познакомиться со справкой на команду nix-env. Для запуска nix-env нужно указывать операцию с опциями. Часть из них общие, а часть — специфичные для отдельных операций.

Конечно, вы можете и удалять, и обновлять пакеты.

Запросы в хранилище

К текущему моменту мы научились исследовать и изменять окружение. Поскольку все компоненты окружения указывают на хранилище, нам необходимо научиться работать с ним.

Для этого есть команда nix-store. Она предоставляет широкие возможности, но пока мы познакомимся только с несколькими видами запросов.

Чтобы вывести непосредственные зависимости программы hello:

$ nix-store -q --references `which hello`
/nix/store/fg4yq8i8wd08xg3fy58l6q73cjy8hjr2-glibc-2.27
/nix/store/58r35bqb4f3lxbnbabq718svq9i2pda3-hello-2.10

Аргумент nix-store может иметь любой тип, если он находится в хранилище Nix. Утилита следует симлинкам.

Сейчас это может показаться неважным, но давайте просмотрим деривации, зависящие от hello:

$ nix-store -q --referrers `which hello`
/nix/store/58r35bqb4f3lxbnbabq718svq9i2pda3-hello-2.10
/nix/store/fhvy2550cpmjgcjcx5rzz328i0kfv3z3-env-manifest.nix
/nix/store/yzdk0xvr0b8dcwhi2nns6d75k2ha5208-env-manifest.nix
/nix/store/mp987abm20c70pl8p31ljw1r5by4xwfw-user-environment
/nix/store/ppr3qbq7fk2m2pa49i2z3i32cvfhsv7p-user-environment

Призайтесь, вы этого не ждали? Оказывается, наши окружения зависят от hello. Да, это значит, что окружения также находятся в хранилище, и поскольку они содержат симлинки на hello, каждое из них зависит от hello.

Мы видим в списке два окружения, относящиеся к поколениям 2 и 3, так как именно они содержат установленную программу hello.

Файл manifest.nix содержит метаданные, относящиеся к окружению, например, список установленных дериваций. Команда nix-env может выводить, обновлять и удалять их. И снова — текущий manifest.nix находится по пути ~/.nix-profile/manifest.nix.

Замыкания

Замыкание деривации — это дерево всех её зависимостей, которое включает абсолютно всё, что нужно для использования данной деривации.

$ nix-store -qR `which man`
[...]

Копирование всех этих дериваций в хранилище на другой машине позволит запустить на ней утилиту man, ничего больше не настраивая. Это — основа развёртывания с помощю Nix. Думаю, вы уже догадываетесь, насколько это мощный и удобный инструмент для развёртывания приложений в облаках (подсказка: nix-copy-closures и nix-store --export).

Просмотр замыкания в виде дерева зависимостей:

$ nix-store -q --tree `which man`
[...]

С помощью этой команды вы можете точно выяснить, почему у данной деривации существует зависимость времени выполнения, прямая или косвенная.

То же самое относится и к окружениям. В качестве упражнения запустите nix-store -q --tree ~/.nix-profile и убедитесь, что прямыми наследниками окружения пользователя являются установленные деривации и файл manifest.nix.

Разрешение зависимостей

В Nix нет ничего похожего на apt, которая решает задачу выполнимости булевых формул (SAT problem), чтобы подобрать зависимости с учётом верхних и нижних границ версий. Вся эта сложность не нужна, потому что все зависимости статичны: если деривация X зависит от деривации Y, она зависит от неё всегда. Деривация X, которая зависит от Z, должна быть другой деривацией.

Ручное восстановление

$ nix-env -e '*'
uninstalling 'hello-2.10'
uninstalling 'nix-2.1.3'
[...]

Ой, команда удалила все деривации из окружения, включая сам Nix! Это значит, что теперь мы не можем запустить даже nix-env. Что делать?

Раньше мы получали доступ к nix-env из окружения. Окружения удобны для пользователей, но Nix всё ещё находится в хранилище!

Сначала выберем одну из дериваций nix-2.1.3: ls /nix/store/*nix-2.1.3, пусть это будет /nix/store/ig31y9gfpp8pf3szdd7d4sf29zr7igbr-nix-2.1.3.

Первый способ всё починить — откатить изменения:

$ /nix/store/ig31y9gfpp8pf3szdd7d4sf29zr7igbr-nix-2.1.3/bin/nix-env --rollback

Второй способ — заново установить nix-env, тем самым создав новое поколение:

$ /nix/store/ig31y9gfpp8pf3szdd7d4sf29zr7igbr-nix-2.1.3/bin/nix-env -i /nix/store/ig31y9gfpp8pf3szdd7d4sf29zr7igbr-nix-2.1.3/bin/nix-env

Каналы

Откуда берутся пакеты? Мы уже говорили об этом во второй статье. Есть список каналов, откуда мы получаем пакеты, хотя обычно мы используем только один. И есть утилита для управления каналами — nix-channel.

$ nix-channel --list
nixpkgs http://nixos.org/channels/nixpkgs-unstable

Если вы используете NixOS, вы можете не увидеть вывода этой команды (если используете настройки по умолчанию), либо вы можете увидеть канал, чьё имя начинается с “nixos-” вместо “nixpkgs”.

По сути, это содержимое файла ~/.nix-channels.

ℹ️ ~/.nix-channels — это не символическая ссылка на хранилище!

Для обновления канала запустите nix-channel --update. Эта команда скачает новые выражения Nix (описания пакетов), создаст новое поколнение профиля каналов и распакует его в ~/.nix-defexpr/channels.

Она немного похожа на apt-get update. (См. таблицу, где в первом приближении сравниваются средства управления пакетами Ubuntu и NixOS).

Заключение

Мы узнали, как исследовать окружение пользователя и изменять его, устанавливая и удаляя программы. Обновлять программы тоже не сложно, подробности см. в руководстве (вкратце: nix-env -u обновит все пакеты в окружении).

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

Далее мы выяснили, как работать с хранилищем. Мы изучили прямые и обратные зависимости путей в нём.

Мы увидели, как симлинки используются для слияния путей из хранилища Nix — полезный трюк.

Подходящая аналогия с языками программирования: у вас есть куча с объектами, и она похожа на хранилище Nix. У вас есть объекты, которые указывают на другие объекты, они похожи на деривации. Возможно, эта метафора поможет вам понять, как деривации ссылаются друг на друга.

В следующей пилюле

…мы изучим основы языка Nix. Язык Nix используется для описания того, как строить деривации. Он является основой всего, включая NixOS. Поэтому очень важно понимать как синтаксис, так и семантику языка.

1

Это значит, что путь к системному man находится в списке путей позже и программа which его не видит — примечание переводчика.

Основы языка

Добро пожаловать на четвёртую пилюлю Nix. В предыдущей статье мы познакомились с окружениями Nix. Мы устанавливали программы, управляли профилем, переключались между поколениями и писали запросы к хранилищу Nix. Всё это — основы системного администрирования с помощью Nix.

Для написания выражений, которые конструируют деривации, используется Язык Nix. Для построения дериваций из выражений используется утилита nix-build. Даже если вы системный администратор (а не программист), вам нужно освоить Nix, если вы хотите настраивать систему. Используя Nix в вашей работе, в качестве бонуса вы получаете все те возможности, о которых я рассказывал в прошлых статьях.

Синтаксис Nix достаточно непривычный, так что, разбираясь с примерами, вы можете решить, что здесь замешана какая-то магия. На самом деле речь в основном идёт об удобном написании служебных функций.

Этот синтаксис прекрасно подходит для описания пакетов, так что изучение языка окупится при написании пакетных выражений.

📢 В Nix всё является выражением, там нет операторов. Обычное дело в функциональных языках.

📢 Значения в Nix неизменяемые (иммутабельные).

Типы значений

В Nix 2.0 есть команда nix repl. Это простая утилита, которая позволяет экспериментировать с языком Nix. Напомню, что Nix — это не только набор утилит для работы с деривациями, но и чистый ленивый функциональный язык. Синтаксис nix repl немного отличается от синтаксиса Nix, когда речь заходит о присваивании переменных (ведь в функциональных языках не бывает присваиваний). Просто помните об этом, и вы не запутаетесь. Эксперименты с nix repl помогут нам быстрее вкатиться в язык.

Запустите nix repl. Прежде всего, Nix поддерживает основные арифметические операции: +, -, * и /. (Чтобы выйти из nix repl, введите команду :q. Команда :? выводит справку.)

nix-repl> 1+3
4

nix-repl> 7-4
3

nix-repl> 3*2
6

Попытка выполнить деление в Nix может вас удивить.

nix-repl> 6/3
/home/nix/6/3

Что произошло? Вспомним, что Nix не является языком общего назначения, это предметно-ориентированный язык для написания пакетов. Деление чисел — не самая нужная операция при написании пакетных выражений. Для Nix 6/3 — это путь, построенный относительно текущего каталога. Чтобы заставить Nix выполнить деление, добавьте пробел после /, либо вызовите встроенную функцию builtins.div.

nix-repl> 6/ 3
2

nix-repl> builtins.div 6 3
2

Другие операторы — это ||, && и | для булевых значений и операторы сравнения, такие как !=, ==, <, >, <=, >=. В Nix <, >, <= and >= используются нечасто. Есть и другие операторы, с которыми мы познакомимся в этом цикле статей.

В Nix есть простые типы: целые числа, числа с плавающей запятой, строки, пути, булевы значения и null. Кроме того, есть списки, множества и функции. Этих типов хватает, чтобы собрать целую операционную систему.

Nix является сильно типизированным, но не статически типизированным языком. То есть, вы не можете смешивать строки и целые числа без предварительного преобразования типов.

Мы выяснили, что выражения считаются путями, если после символа деления нет пробела. Поэтому, чтобы указать текущий каталог, пишите ./. Кроме того, Nix умеет распознавать url’ы.

Не все url’ы или пути могут быть распознаны обычным образом. Если возникает ошибка распознавания, вы всегда можете вернуться к обычным строкам. Строковые url’ы и пути также обеспечивают дополнительную безопасность.

Идентификаторы

Идентификаторы в Nix такие же, как в других языках, за исключением того, что позволяют писать дефис (-). Удобно, имея дело с пакетами, писать дефис в имени. Пример:

nix-repl> a-b
error: undefined variable `a-b' at (string):1:1
nix-repl> a - b
error: undefined variable `a' at (string):1:1

Как видите, a-b распознаётся как идентификатор, а не как вычитание.

Строки

Строки заключаются в двойные кавычки (") или в пару одинарных кавычек ('').

nix-repl> "foo"
"foo"

nix-repl> ''foo''
"foo"

В других языках, например, в Python, можно заключать строки в одиночные кавычки ('foo'), но не в Nix.

Можно интерполировать выражения Nix внутри строк с помощью синтаксиса ${...}. Если вы писали на других языках, то можете по привычке написать $foo или {$foo}, но этот синтаксис работать не будет.

nix-repl> foo = "strval"
nix-repl> "$foo"
"$foo"
nix-repl> "${foo}"
"strval"
nix-repl> "${2+3}"
error: cannot coerce an integer to a string, at (string):1:2

Помните, что присваивание foo = "strval" — это специальный синтаксис, доступный только в nix repl и недоступный в обычном языке.

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

Заключая строку в пару одинарных кавычек, можно писать двойные кавычки внутри без необходимости их экранировать.

nix-repl> ''test " test''
"test \" test"
nix-repl> ''${foo}''
"strval"

Экранирование ${...} в строках с двойными кавычками делается с помощью обратной косой линии (бекслеша), а в строках с парой одиночных кавычек — с помощью '':

nix-repl> "\${foo}"
"${foo}"
nix-repl> ''test ''${foo} test''
"test ${foo} test"

Списки

Списки — это последовательность выражений, разделённая пробелами (не запятыми):

nix-repl> [ 2 "foo" true (2+3) ]
[ 2 "foo" true 5 ]

Списки, как и всё в Nix, неизменяемы (иммутабельны). Добавление или удаление элементов в списке возможно, но возвращает новый список.

Наборы атрибутов

Набор1 атрибутов — это ассоциативный массив со строковыми ключами и значениями Nix. Ключи могут быть только строками. Если ключи являются правильными идентификаторами, их можно записывать без кавычек.

nix-repl> s = { foo = "bar"; a-b = "baz"; "123" = "num"; }
nix-repl> s
{ "123" = "num"; a-b = "baz"; foo = "bar"; }

Набор атрибутов можно перепутать с набором аргументов при вызове функций, но это разные вещи.

Чтобы обратиться к элементу в наборе атрибутов:

nix-repl> s.a-b
"baz"
nix-repl> s."123"
"num"

Чтобы обратиться к ключу, который не является правильным идентификатором, используйте кавычки.

Внутри набора нельзя ссылаться на другие элементы или на сам набор:

nix-repl> { a = 3; b = a+4; }
error: undefined variable `a' at (string):1:10

Это можно делать с помощью рекурсивных наборов:

nix-repl> rec { a = 3; b = a+4; }
{ a = 3; b = 7; }

Такая возможность полезна при описании пакетов, которые часто имеют рекурсивную природу.

Выражения ‘if’

Это всё ещё выражения, не операторы.

nix-repl> a = 3
nix-repl> b = 4
nix-repl> if a > b then "yes" else "no"
"no"

Нельзя записывать только ветку then без ветки else, потому что у выражения при любом раскладе должен быть результат.

Выражения ‘let’

Выражения ‘let’ используются, чтобы определить локальные переменные для других (внутренних) выражений.

nix-repl> let a = "foo"; in a
"foo"

Синтаксис такой: сначала определяем переменные, затем пишем ключевое слово in, затем выражение, в котором можно ссылаться на определённые переменные. Значением всего выражения let будет значение выражения после in.

nix-repl> let a = 3; b = 4; in a + b
7

Попробуем записать два выражения let, одно внутри другого:

nix-repl> let a = 3; in let b = 4; in a + b
7

Помните, что с помощью let нельзя присвоить переменной другое значение. Однако, можно перекрывать внешние переменные:

nix-repl> let a = 3; a = 8; in a
error: attribute `a' at (string):1:12 already defined at (string):1:5
nix-repl> let a = 3; in let a = 8; in a
8

Нельзя ссылаться на переменные в выражении let снаружи:

nix-repl> let a = (let c = 3; in c); in c
error: undefined variable `c' at (string):1:31

Можно ссылаться на переменные в выражении let, определяя другие перменные, как в рекурсивных наборах.

nix-repl> let a = 4; b = a + 5; in b
9

Общее правило: избегайте ситуаций, когда вам надо сослаться на внешнюю переменную, но переменная с таким же именем есть в текущем выражении let. Это же правило действует и в отношении рекурсивных наборов.

Выражения ‘with’

Это непривычный тип выражений — его нечасто можно встретить в других языках. Можно считать его расширенной версией оператора using из C++, или from module import* из Python. Конструкция with включает атрибуты набора в область видимости.

nix-repl> longName = { a = 3; b = 4; }
nix-repl> longName.a + longName.b
7
nix-repl> with longName; a + b
7

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

nix-repl> let a = 10; in with longName; a + b
14
nix-repl> let a = 10; in with longName; longName.a + b
7

Ленивые вычисления

Nix вычисляет выражения только тогда, когда ему нужен результат. Эта особенность языка активно используется при описании пакетов.

nix-repl> let a = builtins.div 4 0; b = 6; in b
6

Здесь значение a не требуется, поэтому ошибка деления на ноль не возникает — выражение просто не вычисляется. Из-за этой особенности языка, пакеты можно определять по мере необходимости, при этом доступ к ним осуществляется очень быстро.

В следующей пилюле

…поговорим о функциях и импорте. В этой пилюле я старался избегать функций, иначе пост стал бы слишком большим.

1

Оригинальный термин set обычно переводится на русский, как множество. Но в нашем случае термин множество вводит в заблуждение, поскольку set содержит не отдельные элементы, а пары ключ-значение. Такую стркутуру обычно называют называют словарём или ассоциативным массивом. В конечном итоге я остановился на слове набор, которое всё-таки не совсем множество.

Функции и импорт

Добро пожаловать в пятую пилюлю Nix. В предыдущей четвёртой пилюле мы начали изучение языка программирования Nix: рассказали про основные типы и значения языка, и про базовые выражения, такие как if, with и let. Чтобы закрепить материал, запустите сессию REPL и поэкспериментируйте с выражениями Nix.

Функции часто используются, чтобы строить повторно используемые компоненты в больших хранилищах, скажем, в nixpkgs. В руководстве по языку Nix есть великолепное объяснение функций, так что я буду часто на него ссылаться.

Напоминаю, как запустить среду Nix: source ~/.nix-profile/etc/profile.d/nix.sh

Безымянные с единственным параметром

В Nix функции всегда анонимы (то есть являются лямбдами), и у них всегда один параметр. Синтаксис экстремально прост: пишите имя параметра, затем “:” и тело функции.

nix-repl> x: x*2
«lambda»

Здесь мы определили функцию, которая принимает параметр x и возвращает x*2. Проблема в том, что мы не можем её вызвать, потому что у неё нет имени… шутка!

Мы можем дать функции имя, связав её с переменной.

nix-repl> double = x: x*2
nix-repl> double
«lambda»
nix-repl> double 3
6

Как я писал ранее, присваивание существует только в nix repl, в обычном языке Nix его нет. Итак, мы определили функцию x: x*2, которая принимает один параметр x и возвращает x*2. Затем она присваивается переменной double. После чего её можно вызвать: double 3.

Важное примечание: в большинстве языков программирования параметры нужно заключать в скобки: double(3). В Nix скобки не нужны: double 3.

Итого: чтобы вызывать функцию, напишите её имя, затем пробел, затем аргумент. Настолько всё просто.

Когда параметров больше одного

Как записать функцию, которая принимает больше одного параметра? Тем, кто не сталкивался с функциональным программированием, потребуется немного времени, чтобы разобраться. Исследуем тему по шагам.

nix-repl> mul = a: (b: a*b)
nix-repl> mul
«lambda»
nix-repl> mul 3
«lambda»
nix-repl> (mul 3) 4
12

Сначала мы определили функцию, которая принимает параметр a и возвращает другую функцию. Эта другая функция принимает параметр b и возвращает a*b. Вызывая mul 3 мы получаем в результате функцию b: 3*b. Вызывая её с параметром 4, мы получаем искомый результат.

В этом коде можно вообще отказаться от скобок, поскольку в Nix есть приоритеты операторов:

nix-repl> mul = a: b: a*b
nix-repl> mul
«lambda»
nix-repl> mul 3
«lambda»
nix-repl> mul 3 4
12
nix-repl> mul (6+7) (8+9)
221

Всё выглядит так, как будто у функции mul два параметра. Из-за того, что аргументы разделяются пробелом, вам придётся ставить скобки, чтобы передавать более сложные выражения. В других языках вы бы написали mul(6+7, 8+9).

Поскольку функции имеют только один параметр, несложно использовать частичное применение:

nix-repl> foo = mul 3
nix-repl> foo 4
12
nix-repl> foo 5
15

Мы сохранили функцию, которую вернула mul 3 в переменную foo, и затем вызывали.

Набор аргументов

Одна из самых мощных возможностей Nix — сопоставление с образцом параметра, который имеет тип набор атрибутов. Напишем альтернативную версию mul = a: b: a*b сначала используя набор аргументов, а затем — сопоставление с образцом.

nix-repl> mul = s: s.a*s.b
nix-repl> mul { a = 3; b = 4; }
12
nix-repl> mul = { a, b }: a*b
nix-repl> mul { a = 3; b = 4; }
12

В первом случае мы определили функцию, которая принимает один параметр-набор. Затем мы взяли атрибуты a и b из этого набора. Заметьте, как элегантно выглядит запись вызова без скобок. В других языках нам пришлось бы написать mul({ a=3; b=4; }).

Во втором случае мы определили набор аргументов. Это похоже на определение набора атрибутов, только без значений. Мы требуем, чтобы переданный набор содержал ключи a и b. Затем мы можем использовать эти a и b непосредственно в теле функции.

nix-repl> mul = { a, b }: a*b
nix-repl> mul { a = 3; b = 4; c = 6; }
error: anonymous function at (string):1:2 called with unexpected argument `c', at (string):1:1
nix-repl> mul { a = 3; }
error: anonymous function at (string):1:2 called without required argument `b', at (string):1:1

Функция принимает набор ровно с теми атрибутами, которые были указаны при её определении.

Атрибуты по умолчанию и вариативные атрибуты

В наборе аргументов можно указывать значения атрибутов умолчанию:

nix-repl> mul = { a, b ? 2 }: a*b
nix-repl> mul { a = 3; }
6
nix-repl> mul { a = 3; b = 4; }
12

Функция может принимать больше атрибутов, чем ей нужно. Такие атрибуты называются вариативными:

nix-repl> mul = { a, b, ... }: a*b
nix-repl> mul { a = 3; b = 4; c = 2; }

Здесь вы не можете получить доступ к атрибуту c. Но вы сможете обратиться к любым атрибутам, дав имя всему набору с помощью @-образца:

nix-repl> mul = s@{ a, b, ... }: a*b*s.c
nix-repl> mul { a = 3; b = 4; c = 2; }
24

Написав name@ перед образцом, вы даёте имя name всему набору атрибутов.

Преимущества использования наборов аргументов:

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

Недостатки:

  • Частичное применение не работает с набором аргументов. Вы должны определить набор атрибутов целиком, нельзя определить только его часть.

Наборы атрибутов похожи на **kwargs из языка Python.

Импорт

Встроенная в язык функция import позволяет включать в текст программы другие файлы .nix. Такой подход весьма распространён в программировании: мы определяем каждый компонент в отдельном файле .nix, а затем соединяем компоненты, импортируя эти файлы в один модуль.

Начнём с простейшего примера.

a.nix:

3

b.nix:

4

mul.nix:

a: b: a*b
nix-repl> a = import ./a.nix
nix-repl> b = import ./b.nix
nix-repl> mul = import ./mul.nix
nix-repl> mul a b
12

Да, всё действительно настолько просто. Вы импортируете файл, он компилируется в выражение. Важный момент: в импортируемом файле нет доступа к переменным из импортирующего файла.

test.nix:

x
nix-repl> let x = 5; in import ./test.nix
error: undefined variable `x' at /home/lethal/test.nix:1:1

Чтобы передать информацию в импортируемый модуль, нужно использовать функции. Пример посложнее:

test.nix:

{ a, b ? 3, trueMsg ? "yes", falseMsg ? "no" }:
if a > b
  then builtins.trace trueMsg true
  else builtins.trace falseMsg false
nix-repl> import ./test.nix { a = 5; trueMsg = "ok"; }
trace: ok
true

Объяснение:

  • В test.nix мы возвращаем функцию. Она принимет набор, где у атрибутов b, trueMsg и falseMsg есть значения по умолчанию.
  • builtins.trace — встроенная функция, которая принимает два аргумента. Первый — это сообщение для печати, второй — возвращаемое значение. Обычно она используется для отладки.
  • В конце мы импортируем test.nix и вызываем функцию с набором { a = 5; trueMsg = "ok"; }.

Когда сообщение будет напечатано? Тогда, когда вычисления доберутся до соответствующей ветви кода.

В следующей пилюле

…мы, наконец, напишем свою первую деривацию.

Наша первая деривация

Добро пожаловать на шестую пилюлю. В предыдущей пятой пилюле мы познакомились с функциями и импортом. Функции и импорт — очень простые концепции, которые позволяют строить сложные абстракции и композицию модулей, чтобы собрать гибкую систему Nix.

В этом посте мы, наконец, добрались до написания деривации. Деривации, с точки зрения файловой системы — это строительные блоки системы Nix. Для описания дериваций используется язык Nix.

Напоминаю, как входить в окружение Nix: source ~/.nix-profile/etc/profile.d/nix.sh

Функция “derivation”

Для создания дериваций используется встроенная функция `derivation``. Прежде, чем двигаться дальше, пройдите по ссылке и познакомьтесь с тем, что написано в официальном руководстве. С точки зрения языка Nix, деривация — всего лишь набор с несколькими атрибутами, так что вы можете хранить её в переменной и передавать в другие функции, как любое другое значение.

Здесь-то и появляется реальная мощь.

Функция derivation в качестве первого агрумента принимает набор. Он требует по меньшей мере следующих трёх атрибутов:

  • name: название деривации. В хранилище Nix деривации хранятся в формате /hash-name, и часть name берётся из этого атрибута.
  • system: название системы, в которой деривация может быть собрана. Например, x86_64-linux.
  • builder: программа, которая собирает деривацию — бинарный образ или скрипт.

Прежде всего надо выяснить, какое, с точки зрения Nix, название у нашей системы?

nix-repl> builtins.currentSystem
"x86_64-linux"

Что будет, если использовать несуществующее название системы?

nix-repl> d = derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; }
nix-repl> d
«derivation /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv»

Так, так, что это? Скрипт собрал деривацию? Нет, не собрал, он всего лишь создал файл .drv. nix repl не собирает деривации, пока вы явно не попросите об этом.

Отсутпление про файлы .drv

Что такое файл .drv? Это спецификация, как собрать деривацию, без лишнего языкового шума, который появляется при работе с любыми высокоуровневыми языкми, в том числе и с Nix.

Чтобы разобраться, можно провести несколько аналогий с языком C:

  • Файлы .nix похожи на файлы .c.
  • Файлы .drv являются промежуточными файлами наподобие файлов .o. Файл .drv описывает, как собрать деривацию. Здесь самый минимум информации.
  • В конечном итоге результатом сборки является выходной путь.

Как вы видите из примера, и выходной путь, и путь к файлу .drv ведут в хранилище.

Так что внутри файла .drv? Его можно прочитать, но лучше распечатать его содержимое в понятном виде:

ℹ️ Если в вашей версия Nix нет команды nix derivation show, используйте вместо неё nix show-derivation.

$ nix derivation show /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv
{
  "/nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
      }
    },
    "inputSrcs": [],
    "inputDrvs": {},
    "platform": "mysystem",
    "builder": "mybuilder",
    "args": [],
    "env": {
      "builder": "mybuilder",
      "name": "myname",
      "out": "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname",
      "system": "mysystem"
    }
  }
}

Так, мы видим здесь выходной путь, но на диске его ещё нет. Даже до начала сборки мы точно знаем, куда будут записаны файлы. Для чего это сделано?

Сборка больших дериваций, таких, как Firefox, занимает много времени. Если бы Nix пришлось собирать их всякий раз, даже если мы не хотим их запускать, а просто что-то проверить, нам бы пришлось дожидаться завершения сборки. Поэтому Nix заранее вычисляет путь, но ничего туда не записывает.

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

В файле .drv заполнены не все поля, так что я вкратце опишу их назначение:

  1. Выходные пути (их может быть несколько). По умолчанию Nix создаёт один выходной путь, называемый “out”.
  2. Список входных дериваций. Он пуст, поскольку мы не ссылались на другие деривации. В противном случае, здесь был бы список других файлов .drv.
  3. Название системы (system) и путь к сборщику (да, сейчас это ненастоящая программа).
  4. Наконец, список переменных окружения, передаваемых в программу сборки.

Это минимум информации, нужной для сборки деривации.

Важное замечание: переменные окружения, передаваемые в сборщик — не только те, которые вы видите в файле .drv, но и некоторые другие, относящиеся к конфигурации Nix (количество ядер, временный каталог и др.). Сборщик не наследут никаких переменных из вашей оболочки, иначе сборки бы страдали от не-детерминизма.

Но вернёмся к нашей учебной деривации.

Попробуем её собрать, не смотря на то, что она почти ненастоящая.

nix-repl> d = derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; }
nix-repl> :b d
[...]
these derivations will be built:
  /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv
building path(s) `/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname'
error: a `mysystem' is required to build `/nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv', but I am a `x86_64-linux'

Команда :b доступна только в nix repl, она используется для сборки деривации. Вы можете получить помощь по командам, введя :?. На экране мы видим путь к файлу .drv, в котором написано, как собирать деривацию. Далее там написано, какой путь будет нашим выходным путём. Наконец, мы видим ошибку, которую и должны были увидеть: деривация не может быть собрана в нашей системе.

Мы выполняем построение с помощью nix repl, но это не единственный способ. Вы можете реализовать файл .drv с помощью команды:

\$ nix-store -r /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv

На экране будет напечатано то же самое.

Давайте исправим атрибут `system``:

nix-repl> d = derivation { name = "myname"; builder = "mybuilder"; system = builtins.currentSystem; }
nix-repl> :b d
[...]
build error: invalid file name `mybuilder'

В качестве программы сборки указана mybuilder, которой в действительности не существует. К чему приведёт запуск такого файла .drv?

Что есть в наборе деривации?

Для начала давайте взглянем на результат функции derivation. Это простой набор:

nix-repl> d = derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; }
nix-repl> builtins.isAttrs d
true
nix-repl> builtins.attrNames d
[ "all" "builder" "drvAttrs" "drvPath" "name" "out" "outPath" "outputName" "system" "type" ]

Вы можете догадаться, что делает builtins.isAttrs: она возвращает true, если аргумент является набором. Вторая функция, builtins.attrNames, возвращает список ключей из заданного набора. Можно сказать, что это своего рода рефлексия.

Начнём с drvAttrs:

nix-repl> d.drvAttrs
{ builder = "mybuilder"; name = "myname"; system = "mysystem"; }

По сути, это входные данные, которые мы передали функции derivation. Конкретно d.name, d.system и d.builder — те атрибуты, которые мы указали при вызове.

nix-repl> (d == d.out)
true

А здесь мы видим, что значение атрибута out — это и есть деривация, что кажется странным. Причина в том, что у деривации может быть только один выходной путь. По этой же причине d.all — это синглтон. Позже мы узнаем, как сделать несколько выходных путей у одной деривации.

d.drvPath — это пусть к файлу .drv: /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv.

Кое-что интересное об атрибует type. Его значение "derivation". Nix добавляет небольшую магию при работе с наборами типа derivation, но действительно небольшую. Чтобы разобраться, создайте набор с типом “derivation”:

nix-repl> { type = "derivation"; }
«derivation ???»

Конечно, в нём нет никакой информации, так что Nix не знает, что печатать. Как понимаете, type = "derivation" — всего лишь соглашение для Nix и для нас, чтобы мы понимали, что набор — это деривация.

При создании пакетов, нас интересует конечный результат — файлы, записанные на диск. Все остальные метаданные нужны Nix чтобы знать, как определить путь к файлу .drv и выходной путь out.

Атрибут outPath — это выходной путь в хранилище Nix: /nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname.

Ссылки на другие деривации

Другие пакетные менеджеры позволяют пакетам ссылаться друг на друга. А как сослаться на другие деривации в Nix, какие указывать пути? Для этого используется атрибут outPath, который содержит путь к файлам нужной деривации. Для удобства Nix умеет конвертировать набор деривации в строку.

nix-repl> d.outPath
"/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
nix-repl> builtins.toString d
"/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"

Nix выполняет “конверсию набора в строку”, если в наборе есть атрибут outPath (это что-то вроде метода toString в других языках):

nix-repl> builtins.toString { outPath = "foo"; }
"foo"
nix-repl> builtins.toString { a = "b"; }
error: cannot coerce a set to a string, at (string):1:1

Скажем, мы хотим использовать бинарные программы из coreutils:

nix-repl> :l <nixpkgs>
Added 3950 variables.
nix-repl> coreutils
«derivation /nix/store/1zcs1y4n27lqs0gw4v038i303pb89rw6-coreutils-8.21.drv»
nix-repl> builtins.toString coreutils
"/nix/store/8w4cbiy7wqvaqsnsnb3zvabq1cp2zhyz-coreutils-8.21"

Безотносительно nixpkgs, представьте, будто мы добавили в область видимости переменные и одна из них это coreutils. Это деривация пакета coreutils, который может быть знаком вам по другим дистрибутивам Linux — он содержит основные программы для систем GNU/Linux. В вашем хранилище может быть несколько дериваций coreutils.

$ ls /nix/store/*coreutils*/bin
[...]

Напоминаю, что внутри строк можно интерполировать выражения Nix с помощью ${...}:

nix-repl> "${d}"
"/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
nix-repl> "${coreutils}"
"/nix/store/8w4cbiy7wqvaqsnsnb3zvabq1cp2zhyz-coreutils-8.21"

Это очень удобно, потому что мы можем ссылаться, скажем, на бинарник bin/true вот так:

nix-repl> "${coreutils}/bin/true"
"/nix/store/8w4cbiy7wqvaqsnsnb3zvabq1cp2zhyz-coreutils-8.21/bin/true"

Почти работающая деривация

В предыдущем примере мы использовали ненастоящую программу сборки, mybuilder, которой, очевидно, не существует. Для экспериментов мы можем вызвать программу bin/true, которая всегда завершается с кодом 0 (успех).

nix-repl> :l <nixpkgs>
nix-repl> d = derivation { name = "myname"; builder = "${coreutils}/bin/true"; system = builtins.currentSystem; }
nix-repl> :b d
[...]
builder for `/nix/store/qyfrcd53wmc0v22ymhhd5r6sz5xmdc8a-myname.drv' failed to produce output path `/nix/store/ly2k1vswbfmswr33hw0kf0ccilrpisnk-myname'

Как мы видим, выполняется программа сборки bin/true, которая ничего не создаёт ни файла, ни каталога по выходному пути, она просто завершается с кодом 0.

Очевидное замечание: каждый раз, когда мы меняем деривацию, вычисляется новый хэш.

Давайте проверим новый файл .drv, после того как мы сослались на другую деривацию:

$ nix derivation show /nix/store/qyfrcd53wmc0v22ymhhd5r6sz5xmdc8a-myname.drv
{
  "/nix/store/qyfrcd53wmc0v22ymhhd5r6sz5xmdc8a-myname.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/ly2k1vswbfmswr33hw0kf0ccilrpisnk-myname"
      }
    },
    "inputSrcs": [],
    "inputDrvs": {
      "/nix/store/hixdnzz2wp75x1jy65cysq06yl74vx7q-coreutils-8.29.drv": [
        "out"
      ]
    },
    "platform": "x86_64-linux",
    "builder": "/nix/store/qrxs7sabhqcr3j9ai0j0cp58zfnny0jz-coreutils-8.29/bin/true",
    "args": [],
    "env": {
      "builder": "/nix/store/qrxs7sabhqcr3j9ai0j0cp58zfnny0jz-coreutils-8.29/bin/true",
      "name": "myname",
      "out": "/nix/store/ly2k1vswbfmswr33hw0kf0ccilrpisnk-myname",
      "system": "x86_64-linux"
    }
  }
}

Ага! Nix добавил зависимость в наш myname.drv, это coreutils.drv. Перед тем, как собрать нашу деривацию, Nix должен построить coreutils.drv. Но, поскольку coreutils уже лежит в нашем хранилище, не нужно ничего строить, так что мы просто обращаемся по пути /nix/store/qrxs7sabhqcr3j9ai0j0cp58zfnny0jz-coreutils-8.29.

Когда деривация собирается на самом деле

Nix не строит дериваций в процессе вычисления выражений Nix. В частности, именно поэтому мы должны вызывать “:b drv” в nix repl или использовать nix-store -r.

В Nix существует важное разделение:

  • Время Инстанцирования/Вычисления: выражения Nix анализируются, интерпретируются и превращаются в набор деривации. При вычислении, вы можете ссылаться на другие деривации, потому что Nix создаст файлы .drv, для которых нам будут известны их выходные пути. Для этого мы используем команду nix-instantiate. – Время Реализации/Построения: деривация собирается из файла .drv, предварительно собрав деривации из файлов .drv, от которых она зависит. Для этого мы используем команду nix-store -r.

Процесс похож на компиляцию и компоновку в проектах C/C++. Сначала вы компилируете все исходные файлы в объектные файлы. А затем компонуете объектные файлы в один исполняемый файл.

В Nix, сначала выражение (обычно из файла .nix) компилируется в .drv, а затем каждый .drv собирается и записывается в хранилище по выходным путям.

Заключение

Сложно ли создать пакет в Nix? Нет, не сложно.

Мы рассмотрели основы дериваций Nix, чтобы разобраться в том, как они работают и как они хранятся. Реальные пакеты в Nix обычно выглядят проще, но у нас была задача научиться, поэтому мы прошлись по всем закоулкам. Новая информация ждёт нас в следующих пилюлях.

С помощью функции derivation мы описываем построение пакета, и принимаем готовый файл .drv в качестве результата. Nix конвертирует набор в строку, когда в наборе есть атрибут outPath. Благодаря этой удобной возможности, легко выстраивать записимости между деривациями.

Когда Nix собирает деривацию, он сначала из выражения создаёт файл .drv, а затем использует этот файл для сборки выходных файлов. Он делает это рекурсивно для всех зависимостей и “выполняет” файлы .drv друг за другом. Не так уж много магии, если подумать.

В следующей пилюле

…мы в конце концов напишем нашу первую работающую деривацию. Да, этот пост тоже про “наше первую деривацию”, но я никогда не утверждал, что она будет работать.

Работающая деривация

Введение

Добро пожаловать на седьмую пилюлю Nix. В предыдущей шестой пилюле мы начали разбираться с деривациями в языке Nix, выяснили, как определить пустую деривацию и (попытаться) её собрать.

В этом посте мы продолжим двигаться тем же курсом, создав деривацию, которая действительно что-то собирает. Затем мы попытаемся создать пакет с реальной программой: скомпилируем простой файл .c и создадим деривацию, используя классический инструментарий.

Напоминаю, как запускать окружение Nix: source ~/.nix-profile/etc/profile.d/nix.sh

Использование скрипта для сборки

Каков простейший способ выполнить цепочку команд, чтобы что-нибудь собрать? Скрипт. Напишем свой скрипт builder.sh, и запустим его при сборке деривации: bash builder.sh.

Мы не можем использовать шебанг1 в builder.sh, поскольку во время написания мы не знаем путь к bash в хранилище Nix. Не смотря на то, что bash конечно же есть в хранилище, ведь там есть всё!

Мы даже не можем сослаться на /usr/bin/env, потому что тогда мы потеряем такое крутое свойство Nix, как отсутствие состояния. Это не говоря о том, что PATH очищается во время сборки, так что bash всё равно не будет найден.

В итоге, чтобы собрать деривацию с помощью bash, мы должны вызывать его, передав аргумент builder.sh. Оказывается, функция derivation принимает опциональный атрибут args, который используется для передачи аргументов исполняемому файлу сборки.

Для начала, запишем наш builder.sh в текущий каталог:

declare -xp
echo foo > $out

Команда declare -xp выводит список эскпортированных переменных (declare — встроенная функция bash). В предыдущей пилюле мы выяснили, что Nix вычисляет выходной путь деривации на основании её атрибутов. Получившийся файл .drv содержит список переменных окружения, передаваемых в скрипт сборки. И одна из этих переменных — $out.

Что мы должны сделать, так это создать что-то по пути $out — файл или каталог. В нашем примере мы создаём файл.

Дополнительно, мы выводим переменные среды в процессе построения. Мы не можем использовать env для этого, потому что env является частью coreutils, и этой зависимости у нас пока нет. Сейчас у нас есть только bash.

Как и в случае с coreutils из предыдущей пилюли, мы без усилий получаем доступ к bash, благодаря нашей волшебной “груде всего”, которая называется nixpkgs.

nix-repl> :l <nixpkgs>
Added 3950 variables.
nix-repl> "${bash}"
"/nix/store/ihmkc7z2wqk3bbipfnlh0yjrlfkkgnv6-bash-4.2-p45"

Так что с помощью небольшого трюка, мы можем сослаться на bin/bash и создать нашу деривацию:

nix-repl> d = derivation { name = "foo"; builder = "${bash}/bin/bash"; args = [ ./builder.sh ]; system = builtins.currentSystem; }
nix-repl> :b d
[1 built, 0.0 MiB DL]

this derivation produced the following outputs:
  out -> /nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo

У нас получилось! Содержимым файла /nix/store/w024zci0x1hh1wj6gjq0jagkc1sgrf5r-foo является как раз строка “foo”. Мы собрали нашу первую деривацию.

Обратите внимание, что мы использовали ./builder.sh, а не "./builder.sh". Благодаря этому Nix понимает, что речь идёт о пути, и может выполнить кое-какую магию, которую мы обсудим позже. Попробуйте строковую версию и вы увидите, что Nix не сможет найти builder.sh. Это потому, что он пытается найти скрипт по относительному пути от временного каталога, где идёт сборка.

Окружение скрипта сборки

Мы можем использовать nix-store --read-log, чтобы посмотреть, какие логи записал наш скрипт:

$ nix-store --read-log /nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo
declare -x HOME="/homeless-shelter"
declare -x NIX_BUILD_CORES="4"
declare -x NIX_BUILD_TOP="/tmp/nix-build-foo.drv-0"
declare -x NIX_LOG_FD="2"
declare -x NIX_STORE="/nix/store"
declare -x OLDPWD
declare -x PATH="/path-not-set"
declare -x PWD="/tmp/nix-build-foo.drv-0"
declare -x SHLVL="1"
declare -x TEMP="/tmp/nix-build-foo.drv-0"
declare -x TEMPDIR="/tmp/nix-build-foo.drv-0"
declare -x TMP="/tmp/nix-build-foo.drv-0"
declare -x TMPDIR="/tmp/nix-build-foo.drv-0"
declare -x builder="/nix/store/q1g0rl8zfmz7r371fp5p42p4acmv297d-bash-4.4-p19/bin/bash"
declare -x name="foo"
declare -x out="/nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo"
declare -x system="x86_64-linux"

Давайте исследуем эти переменные среды, напечатаннные в процессе сборки.

  • $HOME — это не ваш домашний каталог, и /homeless-shelter вообще не существует. Мы заставляем пакеты не зависеть от $HOME в процессе построения.
  • $PATH также, как и $HOME не содержит реальных путей.
  • $NIX_BUILD_CORES и $NIX_STORE — это конфигурационные опции Nix.
  • $PWD и $TMP указывают на временный каталог, который Nix создал для сборки.
  • $builder, $name, $out и $system — переменные, получившие значения из файла .drv.

Переменная $out содержит путь к деривации, куда нам надо что-нибудь сохранить. Выглядит так, будто Nix зарезервировал для нас слот в хранилище, который мы должны заполнить.

В терминах autotools, $out — то же самое, что и путь --prefix. Да, не переменная DESTDIR из make, а именно --prefix. Вот суть создания пакетов без состояния. Вы не устанавливаете пакет по глобальному пути относительно /, вы устанавливаете его по локальному изолированному пути в слот вашего хранилища Nix.

Содержимое файлов .drv

В примере выше мы добавили в деривацию атрибут args. Как это изменило файл .drv по сравнению с примером из предыдущей пилюли?

$ nix derivation show /nix/store/i76pr1cz0za3i9r6xq518bqqvd2raspw-foo.drv
{
  "/nix/store/i76pr1cz0za3i9r6xq518bqqvd2raspw-foo.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo"
      }
    },
    "inputSrcs": [
      "/nix/store/lb0n38r2b20r8rl1k45a7s4pj6ny22f7-builder.sh"
    ],
    "inputDrvs": {
      "/nix/store/hcgwbx42mcxr7ksnv0i1fg7kw6jvxshb-bash-4.4-p19.drv": [
        "out"
      ]
    },
    "platform": "x86_64-linux",
    "builder": "/nix/store/q1g0rl8zfmz7r371fp5p42p4acmv297d-bash-4.4-p19/bin/bash",
    "args": [
      "/nix/store/lb0n38r2b20r8rl1k45a7s4pj6ny22f7-builder.sh"
    ],
    "env": {
      "builder": "/nix/store/q1g0rl8zfmz7r371fp5p42p4acmv297d-bash-4.4-p19/bin/bash",
      "name": "foo",
      "out": "/nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo",
      "system": "x86_64-linux"
    }
  }
}

Похоже на старый добрый файл .drv, за исключением списка аргументов, который передаётся в bash. Это спосок на самом деле содержит один скрипт builder.sh, который каким-то образом оказался в хранилище Nix. Дело в том, что Nix автоматически копирует в хранилище файлы и каталоги, нужные для сборки. Это гарантирует, что они не изменятся в процессе сборки, и что при развёртывании не может быть никаких состояний или зависимостей от текущей машины.

Поскольку builder.sh — обычный файл, с ним не могут быть ассцииорованы никакие файлы .drv. Путь в хранилище вычисляется на основании имени файла и хэша его содержимого. Путь в хранилище подробно обсуждаются в одной из следующих пилюль.

Создаём пакет из простой программы на C

Напишем простую программу на C, поместив её в файл simple.c:

void main() {
  puts("Simple!");
}

А вот скрипт сборки simple_builder.sh:

export PATH="$coreutils/bin:$gcc/bin"
mkdir $out
gcc -o $out/simple $src

Не бескойтесь о том, откуда возьмутся эти переменные; давайте пока опишем деривацию и соберём её:

nix-repl> :l <nixpkgs>
nix-repl> simple = derivation { name = "simple"; builder = "${bash}/bin/bash"; args = [ ./simple_builder.sh ]; gcc = gcc; coreutils = coreutils; src = ./simple.c; system = builtins.currentSystem; }
nix-repl> :b simple
this derivation produced the following outputs:

  out -> /nix/store/ni66p4jfqksbmsl616llx3fbs1d232d4-simple

Теперь можно запустить /nix/store/ni66p4jfqksbmsl616llx3fbs1d232d4-simple/simple в вашем bash.

Объяснение

Мы добавили два новых атрибута к вызову derivation: gcc и coreutils. В gcc = gcc;, слева находится имя в наборе деривации, а справа — ссылка на деривацию gcc из nixpkgs. То же касается и coreutils.

Мы также добавили атрибут src. Здесь ничего магического — атрибуту присвоен путь ./simple.c. Так же, как и simple-builder.sh, simple.c будет добавлен в хранилище.

Трюк: каждый атрибут в наборе, переданный в derivation будет сконвертирован в строку и передан в скрипт сборки как переменная окружения. Именно так скрипт получает доступ к coreutils и gcc: при конвертации в строку, деривации превращаются в их выходные пути, а добавление к ним /bin ведёт нас к их исполняемым файлам.

То же касается и переменной src. $src — это путь к simple.c в хранилище Nix. В качестве упражнение выведите .drv в читаемом виде. Вы увидите среди входных дериваций simple_builder.sh и simple.c, наряду с файлами .drv, относящимися к bash, gcc и coreutils. Кроме того, вы увидите новые добавленные переменные окружения, описанные выше.

В simple_builder.sh мы установили PATH для исполняемых файлов gcc и coreutils, так что скрипт сборки может найти нужные утилиты вроде mkdir и gcc.

Затем мы создали $out как каталог и разместили бинарные файлы внутри него. Обратите внимание, что gcc найден через переменную окружения PATH, но на него точно также можно было бы сослаться явно, используя $gcc/bin/gcc.

Избавляемся от nix repl

Попробуем повторить те же шаги, избавившись от nix repl, и написав выражение Nix в файле simple.nix:

let
  pkgs = import <nixpkgs> { };
in
pkgs.stdenv.mkDerivation {
  name = "simple";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./simple_builder.sh ];
  gcc = pkgs.gcc;
  coreutils = pkgs.coreutils;
  src = ./simple.c;
  system = builtins.currentSystem;
}

Собрать деривацию можно с помощью команды nix-build simple.nix. Она создаст в текущем каталоге символическую ссылку result, указывающую на выходной путь деривации.

nix-buils выполняет две задачи:

  • nix-instantiate: разбирает и выполняет simple.nix и возвращает файл .drv, относящийся к разобранному набору деривации
  • nix-store -r: исполняет файл .drv, что в действительности строит деривацию.

В конце концов он создаёт символическую ссылку.

Во второй строке simple.nix у нас есть вызов функкции import. Вспомните, что import принимает один аргумент — файл .nix для загрузки. Содержимое файла выполняется, как будто это функция.

Мы вызываем эту функцию с пустым набором. Подобный вызов мы видили в пятой пилюле. Ещё раз обратите внимание: конструкция import <nixpkgs> { } вызывает две функции, не одну. Чтобы стало яснее, прочитайте выражение как (import <nixpkgs>) { }.

Значение, возвращаемое функцией nixpkgs это набор; более точно, это набор дериваций. Вызов import<nixpkgs> { } в выражении let создаёт локальную переменную pkgs и вводит её в область видимости. Тот же эффект воникает и при выполнении инструкции :l <nixpkgs>, который мы использовали в nix repl. Набор pkgs, позволяет нам легко обращаться к таким деривациям, как bash, gcc и coreutils. На эти деривации надо ссылаться, как на атрибуты набора pkgs, т.е. писать pkgs.bach вместо bash.

Ниже представлена исправленная версия simple.nix, использующая ключевое слово inherit:

let
  pkgs = import <nixpkgs> { };
in
pkgs.stdenv.mkDerivation {
  name = "simple";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./simple_builder.sh ];
  inherit (pkgs) gcc coreutils;
  src = ./simple.c;
  system = builtins.currentSystem;
}

Для чего используется ключевое слово inherit? inherit foo; является эквивалентом foo = foo;. Точно также inherit gcc coretutils; — эквивалент для gcc = gcc; coreutils = coreutils;. Наконец, inherit (pkgs) gcc coreutils; — эквивалент для gcc = pkgs.gcc; coreutils = pkgs.coreutils;.

Этот синтаксис имеет смысл только внутри наборов. Здесь нет никакой магии, это просто удобный способ избежать повторения одного и того же имени и для атрибута, и для значения в области видимости.

В следующей пилюле

Мы напишем универсальный скрипт сборки. Наверное вы заметили, что мы в этом посте написали два разных скрипта builder.sh. Было бы лучше, если бы у нас был универсальный скрипт, особенно, учитывая, что каждый скрипт сохраняется в хранилище Nix, а это затратно.

Это и правда так трудно — делать пакеты в Nix? Нет, мы просто изучаем основы.

1

Шебанг ровно так и записан в русской википедии. Shebang или hash-bang, как в оригинале у Люка — символы “#!”, которые идут в начале любого скрипта и указывают пусть к интепретатору.

Универсальные скрипты сборки

Добро пожаловать на восьмую пилюлю Nix. В предыдущей седьмой пилюле мы успешно собрали деривацию. Мы написали скрипт сборки, который скомпилировал программу на языке C и установил бинарный образ в хранилище Nix.

В этом посте мы обобщим скрипт сборки, напишем выражение Nix для GNU hello world и создадим обёртку над встроенной функцией derivation.

Упаковываем GNU hello world

В предыдущей пилюле мы упаковали простой файл .c, который был скомпилирован с помощью обычного вызова gcc. Это не самый удачный пример проекта. Многие используют autotools и, поскольку мы хотим обобщить наш скрипт, лучше ориентироваться на самую популярную систему сборки.

GNU hello world не смотря на своё название, это всё ещё простой проект, собираемый при помощи autotools. Загрузите последний архив отсюда: https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz.

Создадим скрипт сборки для GNU hello world, назовём его hello_builder.sh:

export PATH="$gnutar/bin:$gcc/bin:$gnumake/bin:$coreutils/bin:$gawk/bin:$gzip/bin:$gnugrep/bin:$gnused/bin:$bintools/bin"
tar -xzf $src
cd hello-2.12.1
./configure --prefix=$out
make
make install

Деривация hello.nix:

let
  pkgs = import <nixpkgs> { };
in
derivation {
  name = "hello";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./hello_builder.sh ];
  inherit (pkgs)
    gnutar
    gzip
    gnumake
    gcc
    coreutils
    gawk
    gnused
    gnugrep
    ;
  bintools = pkgs.binutils.bintools;
  src = ./hello-2.12.1.tar.gz;
  system = builtins.currentSystem;
}

Nix в Darwin

Сборка в Darwin (т.е. macOS) в качестве компилятора C традиционно использует clang вместо gcc. Чтобы адаптировать наш пример под Darwin, напишем такую модифицированную версию hello.nix:

let
  pkgs = import <nixpkgs> { };
in
derivation {
  name = "hello";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./hello_builder.sh ];
  inherit (pkgs)
    gnutar
    gzip
    gnumake
    coreutils
    gawk
    gnused
    gnugrep
    ;
  gcc = pkgs.clang;
  bintools = pkgs.clang.bintools.bintools_bin;
  src = ./hello-2.12.1.tar.gz;
  system = builtins.currentSystem;
}

Позже мы покажем как обрабатывать эти различия автоматически. Сейчас просто имейте в виду, что в других скриптах также могут потребоваться изменения, подобные описанным выше.

Соберём программу, запустив nix build hello.nix. Теперь можно выполнить result/bin/hello. Всё довольно просто, но надо ли писать builder.sh для каждого пакета? Надо ли всегда передавать зависимости в функцию derivation?

Пожалуйста, обратите внимание на параметр --prefix=$out, который мы обсуждали в предыдущей пилюле.

Универсальный скрипт

Обобщим builder.sh на все проекты autotools:

set -e
unset PATH
for p in $buildInputs; do
    export PATH=$p/bin${PATH:+:}$PATH
done

tar -xf $src

for d in *; do
    if [ -d "$d" ]; then
        cd "$d"
        break
    fi
done

./configure --prefix=$out
make
make install

Что мы делаем?

  1. С помощью set -e просим оболочку прерывать выполнение скрипта в случае любой ошибки.
  2. Вначале очищаем PATH`` (unset PATH`), потому что в этом месте переменная содержит несуществующие пути.
  3. В конец каждого пути из $buildInputs дописываем bin и всё вместе добавляем к PATH. Подробности обсуждим чуть позже.
  4. Распоковываем исходники.
  5. Ищем каталог, куда были распакованы исходники и переходим в него, выполнив команду cd.
  6. Наконец, конфигурируем, компилируем и устанавливаем проект.

Как видите, в скрипте сборки больше нет никаких ссылок на “hello”. Скрипт по прежнему опирается на несколько соглашений, но безусловно, это версия более универсальна.

Теперь перепишем hello.nix:

let
  pkgs = import <nixpkgs> { };
in
derivation {
  name = "hello";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./builder.sh ];
  buildInputs = with pkgs; [
    gnutar
    gzip
    gnumake
    gcc
    coreutils
    gawk
    gnused
    gnugrep
    binutils.bintools
  ];
  src = ./hello-2.12.1.tar.gz;
  system = builtins.currentSystem;
}```

Тут всё ясно, за исключением, может быть, `buildInputs`.
Но и в `buildInputs` нет никакой чёрной магии.

Nix умеет конвертировать списки в строку.
Сначала он конвертирует в строки каждый отдельный элемент, а затем склеивает их, разделяя пробелом:

```text
nix-repl> builtins.toString 123
"123"

nix-repl> builtins.toString [ 123 456 ]
"123 456"

Вспомним, что и деривации можно конвертировать в строку, поэтому:

nix-repl> :l <nixpkgs>
Added 3950 variables.

nix-repl> builtins.toString gnugrep
"/nix/store/g5gdylclfh6d224kqh9sja290pk186xd-gnugrep-2.14"

nix-repl> builtins.toString [ gnugrep gnused ]
"/nix/store/g5gdylclfh6d224kqh9sja290pk186xd-gnugrep-2.14 /nix/store/krgdc4sknzpw8iyk9p20lhqfd52kjmg0-gnused-4.2.2"

Вот так всё просто! Переменная buildInputs в конечном итогде будет содержать нужные нам пути, разделённые пробелом. Нет ничего лучше для использования в цикле for интерпретатора bash.

Удобная версия функции derivation

Нам удалось написать скрипт, который можно использовать для разных проектов autotools. Но в выражении hello.nix мы определяем все программы, которые могут потребоваться, включая те, которые не нужны для сборки конкретного проекта.

Мы можем написать функцию, которая также, как и derivation, принимает набор атрибутов, и сливает его с другим набором атрибутов, общим для всех проектов.

autotools.nix:

pkgs: attrs:
let
  defaultAttrs = {
    builder = "${pkgs.bash}/bin/bash";
    args = [ ./builder.sh ];
    baseInputs = with pkgs; [
      gnutar
      gzip
      gnumake
      gcc
      coreutils
      gawk
      gnused
      gnugrep
      binutils.bintools
    ];
    buildInputs = [ ];
    system = builtins.currentSystem;
  };
in
derivation (defaultAttrs // attrs)

Чтобы разобраться, как работает этот код, вспоминм кое-что о фукнциях Nix. Всё выржаение Nix из файла autotools.nix превращается в функцию. Эта функция принимает параметр pkgs и возвращает функцию, которая принимает параметр attrs.

Внутри функции не происходит ничего сложного, но при первом знакомстве нам, возможно, придётся потратить время, чтобы понять, как она работает.

  1. Сначала добавляем в обласить видимость магический набор атрибутов pkgs.
  2. С помощью выражения let определяем вспомогательную переменную defaultAttrs, куда складываем несколько атрибутов, нужных для деривации.
  3. В конце создаём вызываем derivation, передавая в качестве параметра странное выражение (defaultAttrs // attrs).

Оператор // — принимает на вход два набора. Результатом является их объединение. В случае конфликта имён атрибутов, используется значение из правого набора.

Так что мы используем defaultAttrs как основу, и добавляем (переопределяем) туда атрибуты из attrs.

Пара примеров прояснит работу оператора:

nix-repl> { a = "b"; } // { c = "d"; }
{ a = "b"; c = "d"; }

nix-repl> { a = "b"; } // { a = "c"; }
{ a = "c"; }

Упражнение: Завершите новый скрипт builder.sh добавив $baseInputs в цикл for вместе с $buildInputs.

Результат оператора // мы передаём в функцию derivation. Атрибут buildInputs пустой, поэтому он будет иметь точно то значение, которое указано в наборе attrs.

Перепишем hello.nix:

let
  pkgs = import <nixpkgs> { };
  mkDerivation = import ./autotools.nix pkgs;
in
mkDerivation {
  name = "hello";
  src = ./hello-2.12.1.tar.gz;
}

Финал! Мы получили простейшее описание пакета! Несколько комментариев, которые помогут вам лучше разораться в языке Nix.

  • Мы помещаем в переменную pkgs импорт, который в предыдущих выражениях помещали в оператор “with”. Это обычная практика, не стоит её опасться.
  • Переменная mkDerivation — прекрасный пример частичного применения. На неё можно смотреть, как как на ‘(import ./autotools.nix) pkgs’.
  • Вначале мы импортируем выражение, затем применяем его к параметру pkgs1.
  • Это даёт нам функцию, которая принимает набор атрибутов attrs.
  • Мы создаём деривацию, указывая только атрибуты name и src. Если проекту нужны другие знависимости в PATH, их можно добавить в buildInputs, но в примере с hello.nix нам это было не нужно.

Обратие внимание, что мы не используем никаких других библиотек. Нам могут потребоваться флаги компилятора C, чтобы искать включаемые файлы других библиотек. Также, нам могут потребоваться флаги компоновщика, чтобы искать статические библиотечные.

Заключение

Nix даёт нам базовые инструменты для создания дериваций, подготовки окружения для сборки и сохранения результата в хранилище Nix.

В этой пилюле мы написали универсальный скрипт сборки проектов autotools и функцию mkDerivation. Последняя объединяет основные компоненты, используемые в проектах autotools с настройками по умолчанию, и избавляет нас от дублирования кода в разных проектах.

Мы познакомились с тем, как расширять систему Nix: мы пишем и объединяем новые деривации.

Аналогия: в C вы создаёте объекты, которые находятся в куче, и затем на их основе создаёте новые объекты. Для ссылки на другие объекты используются указатели.

В Nix вы создаёте деривации, которые находятся в хранилище Nix, и затем на их основе создаёте новые деривации. Для ссылки на другие деривации используются выходные пути.

В следующей пилюле

…мы поговорим про зависимости от среды выполнения. Является ли пакет GNU hello world автономным? Каковы зависимости его среды выполнения? Пока что мы определили зависимости для сборки, посредством использования других дериваций в деривации “hello”.

1

В функциональных языках вызов функции с параметром часто называют применением функции к параметру.

Автоматические зависимости времени выполнения

Добро пожаловать на девятую пилюлю Nix. В предыдущей восьмой пилюле мы разработали универсальный скрипт сборки для проектов autotools. Мы загрузили зависимости и исходники, и получили в качестве результата деривацию Nix.

Сегодня мы обратимся к программе GNU hello, чтобы исследовать зависимости времени сборки и времени выполнения. Затем мы усовершенствуем наш скрипт, чтобы исключить ненужные зависимости.

Зависимости сборки

Давайте начнём анализ зависимостей сборки для пакета GNU hello:

$ nix-instantiate hello.nix
/nix/store/z77vn965a59irqnrrjvbspiyl2rph0jp-hello.drv
$ nix-store -q --references /nix/store/z77vn965a59irqnrrjvbspiyl2rph0jp-hello.drv
/nix/store/0q6pfasdma4as22kyaknk4kwx4h58480-hello-2.10.tar.gz
/nix/store/1zcs1y4n27lqs0gw4v038i303pb89rw6-coreutils-8.21.drv
/nix/store/2h4b30hlfw4fhqx10wwi71mpim4wr877-gnused-4.2.2.drv
/nix/store/39bgdjissw9gyi4y5j9wanf4dbjpbl07-gnutar-1.27.1.drv
/nix/store/7qa70nay0if4x291rsjr7h9lfl6pl7b1-builder.sh
/nix/store/g6a0shr58qvx2vi6815acgp9lnfh9yy8-gnugrep-2.14.drv
/nix/store/jdggv3q1sb15140qdx0apvyrps41m4lr-bash-4.2-p45.drv
/nix/store/pglhiyp1zdbmax4cglkpz98nspfgbnwr-gnumake-3.82.drv
/nix/store/q9l257jn9lndbi3r9ksnvf4dr8cwxzk7-gawk-4.1.0.drv
/nix/store/rgyrqxz1ilv90r01zxl0sq5nq0cq7v3v-binutils-2.23.1.drv
/nix/store/qzxhby795niy6wlagfpbja27dgsz43xk-gcc-wrapper-4.8.3.drv
/nix/store/sk590g7fv53m3zp0ycnxsc41snc2kdhp-gzip-1.6.drv

Учитывая, что наша универсальная функция mkDerivation всегда извлекает такие зависимости (сравните с пакетом build-essential из Debian), они попадут в хранилище Nix ещё до того, как потребуются какому-нибудь пакету для сборки.

Почему мы смотрим на файлы .drv? Потому что hello.drv представляет собой действие, которое собирает программу hello по выходному пути. Как таковой, он содержит входные деривации, нужные для сборки hello.

Немного о файлах NAR

Формат NAR означает “Nix ARchive”. Его разработали, потому что существующие форматы архивов, такие как tar, не удовлетворяют некоторым важным требованиям. Для Nix нужны детерминированные средства сборки, но обычные архиваторы не детерминированы: они выравнивают данные, они не сортируют файлы они добавляют метки времени и так далее. В результате директории, содержащие побитово-идентичные файлы превращаются в побитово-неидентичные архивы, что приводит к различным хэшам.

В отличие от tar, NAR был разработан как простой детерминированный формат архива. Ниже мы увидим, что он широко используется в Nix.

За подробным обоснованием и деталями реализации NAR обращайтесь к докторской диссертации Долстры.

Чтобы создавать архивы NAR из путей хранилища, мы можем использовать утилиты nix-store --dump и nix-store --restore.

Зависимости времени выполнения

Отметим, что Nix автоматически распознал зависимости сборки, поскольку на них ссылается функция derivation, но мы никогда не указывали зависимости времени выполнения.

Зависимости времени выполнения Nix обнаруживает автоматически. Техника, которую он использует, на первый взгляд может показаться хрупкой, но она работает настолько хорошо, что на ней построена операционная система NixOS. Базовый механизм использует хэши путей хранилища. Он выполняется за три шага.

  1. Создаёт архив NAR из деривации. Обратите внимание, что этот шаг сериализует вывод деривации — он хорошо работает и тогда, когда деривация — это один файл, и тогда, когда это целый каталог.
  2. Для каждого файла .drv, от которого зависит сборка и относящегося к нему выходного пути, ищет по этому пути архив NAR.
  3. Если архив найден, то путь является зависимостью времени выполнения.

Приведённый фрагмент показывает зависимости hello.

$ nix-instantiate hello.nix
/nix/store/z77vn965a59irqnrrjvbspiyl2rph0jp-hello.drv
$ nix-store -r /nix/store/z77vn965a59irqnrrjvbspiyl2rph0jp-hello.drv
/nix/store/a42k52zwv6idmf50r9lps1nzwq9khvpf-hello
$ nix-store -q --references /nix/store/a42k52zwv6idmf50r9lps1nzwq9khvpf-hello
/nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19
/nix/store/8jm0wksask7cpf85miyakihyfch1y21q-gcc-4.8.3
/nix/store/a42k52zwv6idmf50r9lps1nzwq9khvpf-hello

Мы видим, что glibc и gcc являются зависимостями времени выполнения. Интуитивно, здесь не должно быть gcc! Но вывод на экран строк из двоичного файла hello показывает, что gcc действительно там встречается:

$ strings result/bin/hello|grep gcc
/nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib:/nix/store/8jm0wksask7cpf85miyakihyfch1y21q-gcc-4.8.3/lib64

Вот почему Nix добавил gcc. Но откуда этот путь вообще появился? Дело в том, что он есть в ld rpath: списке каталогов с библиотеками времени выполнения. В других дистрибутивах этим, обычно, не злоупотребляют. Но в Nix нам приходится ссылаться на определённые версии библиотек, поэтому rpath играет важную роль.

Процесс сборки добавляет путь к библиотекам gcc, полагая, что он потребуется во время выполнения. Впрочем, это не обязательно так. Для решения этой проблемы, Nix предоставляет инструмент под названием patchelf, который сводит rpath к путям, которые действительно нужны при выполнении программы.

Даже после сокращения rpath, исполняемый файл hello по прежнему зависит от gcc из-за отладочной информации. В следующем разделе мы исследуем, как с помощью strip полностью избавиться от этой зависимости.

Ещё одна фаза сборки

Добавим ещё одну фазу в скрипт сборки autotools. В настоящий момент сборщик имеет шесть фаз:

  1. Фаза “настройки окружения”
  2. Фаза “распаковки”: мы распаковываем исходники в текущий каталог (помните, что Nix запускает сборку во временном каталоге)
  3. Фаза “смены каталога”: временный каталог становится корнем дерева исходников
  4. Фаза “конфигурирования”: ./configure
  5. Фаза “сборки”: make
  6. Фаза “установки”: make install

Добавим новую фазу сразу после “установки”, это будет фаза “исправления”. Допишем в конец builder.sh:

find $out -type f -exec patchelf --shrink-rpath '{}' \; -exec strip '{}' \; 2>/dev/null

То есть для каждого файла мы выполняем patchelf --shrink-rpath и strip. Обратите внимание, что, поскольку мы использовали две новые команды find и patchelf, нам надо добавить их в деривацию.

Упражнение: Добавьте findutils и patchelf к baseInputs скрипта autotools.nix.

Теперь снова соберём hello.nix

$ nix-build hello.nix
[...]
$ nix-store -q --references result
/nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19
/nix/store/md4a3zv0ipqzsybhjb8ndjhhga1dj88x-hello

и увидим, что в списке зависимостей времени выполнения осталась только библиотека glibc. Именно этого мы и добивались.

Мы сделали автономный (самодостаточный) пакет. Это значит, что мы можем скопировать его на другую машину, где он соберёт точно такую же программу hello. Обратите внимание, что для её запуска требуются некоторые компоненты из /nix/store, так что нужно будет запустить nix. Исполняемый файл hello запускается именно с той версией библиотеки glibc и интерпретатора1, которые указаны в нём, а не с системными версиями.

$ ldd result/bin/hello
 linux-vdso.so.1 (0x00007fff11294000)
 libc.so.6 => /nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib/libc.so.6 (0x00007f7ab7362000)
 /nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib/ld-linux-x86-64.so.2 (0x00007f7ab770f000)

Конечно, исполняемый файл будет прекрасно запускаться, пока все нужные пакеты лежат в каталоге /nix/store.

Заключение

Мы познакомились с несколькими инструментами Nix и с их возможностями. В частности, мы узнали, как Nix распознаёт зависимости времени выполнения. И речь идёт не только о разделяемых библиотеках, но и об исполняемых файлах, скриптах, библиотеках Python и так далее.

Такой подход к сборке позволяет делать пакеты самодостаточными, гарантируя, что копирования пакета на другую машину достаточно для запуска программы. Благодаря этому, мы можем запускать программы без установки, используя nix-shell и использовать Nix для надёжного развёртывания в облаке.

В следующей пилюле

Следующая пилюля расскажет про nix-shell. Ранее мы строили деривации с нуля с помощью nix-build: распаковывали исходные коды, конфигурировали, собирали и устанавливали. Развёртывание больших пакетов может занимать много времени. Мы могли бы вносить небольшие изменения и использовать инкрементальную компиляцию, в то же время опираясь на преимущества самодостаточного окружения, как в nix-build. Для этого нам и потребуется nix-shell.

1 Речь про загрузчик ELF, который иногда называют интерпретатором.

Разработка с помощью nix-shell

Добро пожаловать на десятую пилюлю Nix. В предыдущей девятой пилюле мы познакомились с одной из мощных возможностей Nix: автоматическим обнаружения зависимостей времени выполнения. Заодно мы завершили разработку пакета GNU hello.

В этой пилюле мы познакомимся с утилитой nix-shell и попробуем с её помощью взломать программу hello. Мы узнаем, что nix-shell создаёт для нас изолированную среду с возможностью редактирования исходных файлов проекта, точно также, как nix-build создаёт изолированную среду во время сборки деривации.

В конце концов мы сделаем наш скрипт сборки более эргономичным, ориентируясь на возможности nix-shell

Что такое nix-shell?

Утилита nix-shell помещает нас в командную оболочку с настроенными переменными окружения, необходимыми для сборки деривации. Она не запускает сборку; она готовит плацдарм для пошаговой сборки проекта.

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

Мы можем передать в nix-shell любое выражение Nix, возвращающее деривацию, но в переменной PATH, которая в конечном итоге попадёт в bash, не будет нужных нам утилит.

$ nix-shell hello.nix
[nix-shell]$ make
bash: make: command not found
[nix-shell]$ echo $baseInputs
/nix/store/jff4a6zqi0yrladx3kwy4v6844s3swpc-gnutar-1.27.1 [...]

Такая оболочка в лучшем случае бесполезна. Было бы разумно ожидать, что программы, описанные в $buildInputs, попадают в PATH (в том числе и программа GNU make), но в нашем случае это не так.

Однако, у нас есть переменные окружения, которые мы установили в деривации, в частности $baseInputs, $buildInputs, $src и др.

Это значит, что мы можем запустить source с параметром builder.sh и она построит деривацию. На этапе установки у вас может возникнуть ошибка, потому что ваш пользователь не имеет прав записи в /nix/store:

[nix-shell]$ source builder.sh
...

Деривация не установилась, но она была построена. Обратите внимание вот на что:

  • Мы запустили builder.sh и он выполнил все шаги сборки, включая настройку PATH.
  • Рабочий каталог — больше не временный каталог, созданный nix-build, а каталог, в котором мы запустили оболочку.
  • Таким образом, hello-2.10 был распакован в текущий каталог.

Мы можем войти в каталог hello-2.10 и запустить make, поскольку make теперь доступен.

Это подтверждает, что nix-shell помещает нас в оболочку с тем же (или очень похожим) окружением, что и во время сборки.

Сборщик для nix-shell

Предыдущие шаги требуют ручного запуска команд и не оптимизированы для работы с nix-shell. Сейчас мы сделаем наш сборщик более дружественным по отношению к nix-shell.

Вот несколько пунктов, которые нам надо изменить.

Во-первых, когда мы загружаем builder.sh, мы загружаем его в текущий каталог. Что мы действительно хотим, так это поместить builder.sh в хранилище Nix, поскольку утилита nix-build использует именно этот файл. Корректный способ в том, чтобы передать в деривацию правильную переменную окружения. (Обратите внимание, что переменная $builder уже определена, но она указывает на исполняемый файл bash вместо builder.sh. Наш builder.sh передаётся в bash как аргумент.)

Во-вторых, мы не хотим запускать сборку полностью, мы собираемся всего лишь настроить окружение, нужное для ручной сборки проекта. Так что мы можем разбить builder.sh на два файла: setup.sh для настройки окружения и настоящий builder.sh, который отправится в nix-build.

В процессе рефакторинга мы завернём этапы сборки в функции, чтобы придать больше структуры нашему дизайну. Дополнительно, мы перенесём set -e из файла настройки в файл сборки. Команда set -e в nix-shell раздражает, так как она завершает работу оболочки при возникновении ошибки.

Вот наш исправленный autotools.nix. Примечательным является атрибут setup = ./setup.sh в деривации, который добавляет setup.sh в хранилище Nix и соответственно инициализирует переменную окружения $setup в сборщике.

pkgs: attrs:
let
  defaultAttrs = {
    builder = "${pkgs.bash}/bin/bash";
    args = [ ./builder.sh ];
    setup = ./setup.sh;
    baseInputs = with pkgs; [
      gnutar
      gzip
      gnumake
      gcc
      coreutils
      gawk
      gnused
      gnugrep
      binutils.bintools
      patchelf
      findutils
    ];
    buildInputs = [ ];
    system = builtins.currentSystem;
  };
in
derivation (defaultAttrs // attrs)

Благодаря этому мы можем разделить builder.sh на setup.sh и builder.sh. Задача builder.sh заключается в том, чтобы загрузить $setup и вызвать функцию genericBuild. Всё остальное — небольшие изменения в скрипте bash.

Вот исправленная версия builder.sh:

set -e
source $setup
genericBuild

Вот новая добавленная версия setup.sh:

unset PATH
for p in $baseInputs $buildInputs; do
    export PATH=$p/bin${PATH:+:}$PATH
done

function unpackPhase() {
    tar -xzf $src

    for d in *; do
    if [ -d "$d" ]; then
        cd "$d"
        break
    fi
    done
}

function configurePhase() {
    ./configure --prefix=$out
}

function buildPhase() {
    make
}

function installPhase() {
    make install
}

function fixupPhase() {
    find $out -type f -exec patchelf --shrink-rpath '{}' \; -exec strip '{}' \; 2>/dev/null
}

function genericBuild() {
    unpackPhase
    configurePhase
    buildPhase
    installPhase
    fixupPhase
}

Наконец, вот hello.nix:

let
  pkgs = import <nixpkgs> { };
  mkDerivation = import ./autotools.nix pkgs;
in
mkDerivation {
  name = "hello";
  src = ./hello-2.12.1.tar.gz;
}

Возвращаемся в nix-shell:

$ nix-shell hello.nix
[nix-shell]$ source $setup
[nix-shell]$

Теперь, скажем, вы можете запустить unpackPhase, которая распакует $src и зайдёт в каталог. И вы можете запускать такие команды, как ./configure, make и т.д. вручную, или запускать фазы с помощью соответствующих функций.

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

Заключение

С помощью nix-shell мы можем запустить изолированное окружение, подходящее для разработки проекта. Это окружение предоставляет необходимые зависимости для оболочки разработчика, подобно тому, как nix-build предоставляет необходимые зависимости сборщику. Дополнительно, мы можем собирать и отлаживать проект вручную, выполняя его пошагово, как мы делали бы в любой другой среде разработки. Заметьте, что мы никогда не устанавливаем такие инструменты, как gcc или make в систему; эти инструменты и библиотеки изолированы и доступны попроектно.

В следующей пилюле

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

Сборщик мусора

Добро пожаловать на одиннадцатую пилюлю Nix. В предыдущей десятой пилюле, мы провели параллель между изолированной средой сборки, предоставляемой nix-build и изолированной оболочкой разработки, предоставляемой nix-shell.

С этого момента мы больше не будем концентрироваться на на создании пакетов и вместо этого исследующим критический важный компонент Nix: сборщик мусора. Используя инструменты Nix, мы часто строим деривации, включая файлы .drv и выходные пути. Эти артефакты попадают в хранилище Nix и занимают место на нашем носителе так что мы можем захотеть освободить немного места, удалив деривации, которые нам больше не нужны. На решении этого вопроса мы сфокусируемся в одиннадцатой пилюле. По умолчанию, Nix придерживается консервативной стратегии, решая, какие из дериваций являются «нужными». В этой пилюле мы познакомимся с техникой выполнения более «опасных» операций обновления и удаления.

Как работает сборщик мусора?

Языки программирования со сборщиком мусора (garbage collector) используют концепцию множества корневых элементов (корней сборщика мусора или GC-корней) для отслеживания «живых» объектов. GC-корень — это объект, который всегда считается «живым» (пока не будет явным образом удалён из списка GC-корней). Процесс сборки мусора начинается с GC-корней, и рекурсивно помечает объекты, до которых может добраться, как «живые» Все остальные объекты могут быть собраны и удалены.

Вместо объектов, сборщик мусора Nix оперирует путями в хранилище, причём GC-корни также являются путями в хранилище. Этот подход гораздо более принципиален, чем у традиционных менеджеров пакетов, таких как dpkg или rpm, которые могут оставлять неиспользуемые пакеты или висящие файлы.

Реализация очень проста и прозрачна для пользователя. Первичные GC-корни хранятся в /nix/var/nix/gcroots. Если существует символическая ссылка на путь в хранилище, то связанный путь в хранилище является корнем GC.

Nix позволяет этому каталогу иметь подкаталоги: он просто рекурсивно перебирает подкаталоги в поиске символических ссылок на пути в хранилище. При обнаружении символической ссылки, её цель добавляется в список живых путей.

Подводя итог, можно сказать, что Nix ведёт список корней СМ. Эти корни могут быть использованы для вычисления списка всех живых путей в хранилище. Любой другой путь считается мёртвым. Удаление такие путей — простая операция. Сначала Nix переносит мёртвые пути в каталог /nix/store/trash, что является атомарной операцией. После этого корзина чистится.

Экспериментируем со сборщиком мусора

Прежде чем начать, запустим сборщик мусора Nix, чтобы получить чистую систему для наших экспериментов:

$ nix-collect-garbage
finding garbage collector roots...
[...]
deleting unused links...
note: currently hard linking saves -0.00 MiB
1169 store paths deleted, 228.43 MiB freed

Если мы запустим сборщик мусора снова, он, как и ожидается, не найдёт объектов для удаления. После завершения сборки мусора, хранилище Nix содержит только пути с ссылками из корней GC.

Теперь установим новую программу, bsd-games, исследуем её путь в хранилище, и убедимся, что он является GC-корнем. Команда nix-store -q --roots нужна, чтобы запросить корни GC, которые ссылаются на заданную деривацию. В нашем случае, окружение текущего пользователя ссылается на bsd-games:

$ nix-collect-garbage
finding garbage collector roots...
[...]
deleting unused links...
note: currently hard linking saves -0.00 MiB
1169 store paths deleted, 228.43 MiB freed

Теперь удалим его, запустим сборщик мусора и убедимся,что bsd-games всё ещё находится в хранилище Nix.

$ rm /nix/var/nix/profiles/default-9-link
$ nix-env --list-generations
[...]
   8   2014-07-28 10:23:24
  10   2014-08-20 12:47:16   (current)
$ nix-collect-garbage
[...]
$ ls /nix/store/b3lxx3d3ggxcggvjw5n0m1ya1gcrmbyn-bsd-games-2.17
ls: cannot access /nix/store/b3lxx3d3ggxcggvjw5n0m1ya1gcrmbyn-bsd-games-2.17: No such file or directory

Старое поколение всё ещё в хранилище Nix, потому что оно является корнем GC. Как вы увидим ниже, все профили и их поколения — это корни GC.

Удаление корня СМ — простая операция. В нашем случае мы удаляем поколение, которое ссылается на bsd-games, запускаем сборщик мусора и убеждаемся, что bsd-games больше недоступен в хранилище Nix:

ℹ️ Замечание: nix-env --list-generations не опирается на какие-либо метаданные. Он получает поколения, основываясь исключительно на именах файлов в каталоге профилей.

Обратите внимание, что мы удалили ссылку из каталога /nix/var/nix/profiles, не из /nix/var/nix/gcroots. Помимо прочего, Nix трактует /nix/var/nix/profiles как корень GC. Это полезно, поскольку это значит, что любой профиль и его поколения являются корнями GC. Другие пути также считаются корнями GC, например, /run/booted-system в NixOS. Команда nix-store --gc --print-roots печатает все пути, которые считаются корнями GC при запуске сборщика мусора.

Косвенные корни

Вспомните, что построение пакета GNU hello с помощью nix-build создаёт в текущем каталоге символическую ссылку result. Несмотря на выполненную выше сборку мусора, программа hello всё ещё работает. Следовательно, она не была собрана. Так как не существует других дериваций, которые зависят от пакета GNU hello, значит, он и является корнем GC.

Фактически, nix-build автоматически делает символическую ссылку result корнем GC. Обратите внимание, что речь идёт не о собранной деривации, а только о символической ссылке. Эти корни GC добавляются в каталог /nix/var/nix/gcroots/auto.

$ ls -l /nix/var/nix/gcroots/auto/
total 8
drwxr-xr-x 2 nix nix 4096 Aug 20 10:24 ./
drwxr-xr-x 3 nix nix 4096 Jul 24 10:38 ../
lrwxrwxrwx 1 nix nix   16 Jul 31 10:51 xlgz5x2ppa0m72z5qfc78b8wlciwvgiz -> /home/nix/result/

Нам пока не важно, какое имя носит символическая ссылка, ставшая корнем GC. Значение имеет то, что она существует и указывает на /home/nix/result. Это называется косвенным корнем GC. Корень GC считается косвенным, если он определён где-то вне каталога /nix/var/nix/gcroots. В нашем случае это значит, что цель символической ссылки result не будет удаляться сборщиком мусора.

Есть два способа удалить деривацию, считающуюся «живой» из-за косвенного корня GC:

  • Удалить косвенный корень GC из /nix/var/nix/gcroots/auto.
  • Удалить символическую ссылку result.

В первом случае деривация будет удалена из хранилища Nix в течение сборки мусора и result станет висячей символической ссылкой. Во втором случае деривация удаляется вместе с косвенным корнем в /nix/var/nix/gcroots/auto.

Запуск nix-collect-garbage после удаления корня GC или косвенного корня GC удалит деривацию из хранилища.

Чистим всё

Главный источник дублирования программ в хранилище Nix возникает из-за корней GC, связанных с nix-build и поколениями профиля. Запуск nix-build приводит к созданию корня GC для сборки, который ссылается на определённые версии определённых библиотек, например, glibc. После обновления, мы должны удалить предыдущую сборку, если мы хотим, чтобы сборщик мусора удалил соответствующие деривации, а так же, если мы хотим очистить старые зависимости.

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

Другие системы обычно «забывают» всё о своём предыдущем состоянии сразу после обновления. В Nix мы тоже можем выполнить такой тип обновления (заставив Nix удалить старые деривации, включая старые поколения), но это надо делать вручную. Потребуются четыре шага:

  • Во-первых, мы скачиваем новую версию канала nixpkgs, который содержит описание всех программ. Это делается с помощью nix-channel --update.
  • Затем мы обновляем установленные пакеты, запустив nix-env -u. Так у нас появится новое поколение с обновлёнными программами.
  • Затем мы удаляем все косвенные корни, созданные nix-build: будьте осторожны, потому что это приводит к появлением висячих системных ссылок. Более умной стратегией будет одновременное удаление целей этих символических ссылок.
  • Наконец, с помощью опции -d при запуске nix-collect-garbage удаляем старые поколения всех профилей с последующей сборкой мусора. После этого к предыдущим поколениям вернуться нельзя. Прежде, чем запускать эту команду, важно убедиться, что новое поколение корректно работает.

Вот эти четыре команды:

$ nix-channel --update
$ nix-env -u --always
$ rm /nix/var/nix/gcroots/auto/*
$ nix-collect-garbage -d

Заключение

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

В следующей пилюле

В следующей пилюле мы запакуем другой проект и объясним паттерн проектирования «Входы». До сих пор мы работали только с одной деривацией; теперь нам предстоит организовать маленький репозиторий для программ. Паттерн «Входы» широко используется в nixpkgs; он позволяет нам разделять деривации из одного репозитория и обеспечивает тонкую настройку.