Добро пожаловать на девятую пилюлю 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 и интерпретатора, которые указаны в нём, а не с системными версиями.

(Здесь речь про загрузчик ELF, который иногда называют интерпретатором — прим. переводчика).

$ 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.

Комментарии (0)