[Перевод] 12. Nix в пилюлях: Репозитории пакетов и паттерн Входящие

b2e2b751ddef0759be110d4c71bdcbf8.jpg

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

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

Репозитории в Nix

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

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

Паттерн Single Repository (Единый репозиторий)

Различные дистрибутивы по разному подходят к организации репозиториев. Debian, распределяет пакеты по нескольким маленьким репозиториям, что затрудняет отслеживание взаимозависимых изменений и мешает контрибуции. В то же время Gentoo держит описания всех пакетов в Едином репозитории.

Nix следует паттерну «Единый репозиторий», размещая описания всех пакетов в nixpkgs. Этот подход воспринимается как естественный и привлекательный при внесении правок в пакеты.

Остаток этой пилюли мы посвятим разбору паттерна Единый репозиторий. Реализация, естественная для Nix — создать выражение верхнего уровня, за которым разместить по одному выражению на каждый пакет. Выражение верхнего уровня импортирует и объединяет все выражения пакетов в набор атрибутов, отображая имена в пакеты.

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

Упаковываем graphviz

Мы уже создали пакет для GNU hello. Теперь создадим пакет программы рисования графиков под названием graphviz, чтобы сделать репозиторий, содержащий несколько пакетов. Пакет graphviz был выбран потому, что он использует стандартную систему сборки autotools и не требует никаких патчей. В нём также есть опциональные зависимости, которые позволяют нам проиллюстрировать технику конфигурирования сборок.

Вначале мы загружаем graphviz из gitlab. Выражение graphviz.nix достаточно простое:

let
  pkgs = import  { };
  mkDerivation = import ./autotools.nix pkgs;
in
mkDerivation {
  name = "graphviz";
  src = ./graphviz-2.49.3.tar.gz;
}

Собрав проект командой nix-build graphviz.nix, мы получим готовые исполняемые файлы в каталоге result/bin. Обратите внимание, что мы повторно используем скрипт autotools.nix, созданный нами при написании hello.nix.

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

$ echo 'graph test { a -- b }'|result/bin/dot -Tpng -o test.png
Format: "png" not recognized. Use one of: canon cmap [...]

Если мы хотим сохранять файлы png с помощью graphviz, мы должны добавить поддержку формата в деривацию. Это можно сделать в autotools.niч, где мы описали переменную buildInputs, которая затем объединяется с baseInputs. Эта переменная для того и нужна, чтобы автор пакета мог добавить входные деривации из пакетных выражений.

В graphviz версии 2.49 есть несколько плагинов для работы с png. Для простоты будем использовать libgd.

Передаём информацию о библиотеках в pkg-config через переменные окружения

Конфигурационный скрипт graphviz использует pkg-config для того, чтобы определить, какие флаги должны быть переданы компилятору. Поскольку не существует глобального каталога, где собраны библиотеки, мы должны сказать pkg-config где искать файлы описания, которые подскажут конфигурационному скрипту, откуда брать заголовки и библиотеки.

В классических POSIX системах, pkg-config просто ищет файлы .pc для всех установленных библиотек в системном каталоге наподобие /usr/lib/pkgconfig. Однако, в изолированных окружениях Nix такой подход просто не будет работать.

В качестве альтернативы мы можем информировать pkg-config о местоположении библиотек через переменную окружения PKG_CONFIG_PATH. Мы можем определить эту переменную, используя тот же трюк, что и для переменной PATH — автоматически заполнив пути из buildInputs. Вот соответствующий фрагмент setup.sh:

for p in $baseInputs $buildInputs; do
    if [ -d $p/bin ]; then
        export PATH="$p/bin${PATH:+:}$PATH"
    fi
    if [ -d $p/lib/pkgconfig ]; then
        export PKG_CONFIG_PATH="$p/lib/pkgconfig${PKG_CONFIG_PATH:+:}$PKG_CONFIG_PATH"
    fi
done

Теперь, если мы добавим деривации к buildInputs, их подкаталоги lib/pkgconfig и bin автоматически добавятся к переменным PKG_CONFIG_PATH и PATH.

Завершаем graphviz с помощью gd

Ниже мы завершаем выражение для graphviz, включив в него поддержку gd. Обратите внимание, что использование выражения with c buildInputs позволяет избежать дублирования pkgs:

let
  pkgs = import  { };
  mkDerivation = import ./autotools.nix pkgs;
in
mkDerivation {
  name = "graphviz";
  src = ./graphviz-2.49.3.tar.gz;
  buildInputs = with pkgs; [
    pkg-config
    (pkgs.lib.getLib gd)
    (pkgs.lib.getDev gd)
  ];
}

Мы добавляем к деривации pkg-config чтобы сделать эту утилиту доступной для конфигурационного скрипта. Поскольку gd — пакет с несколькими выходными путями, надо добавить оба пути — lib и dev.

После сборки graphviz может создавать png файлы.

Выражение репозитория

Теперь, когда у нас есть два пакета, мы хотим объединить их в один репозиторий. Чтобы это сделать, будем подражать nixpkgs: создадим один набор атрибутов, содержащий деривации. Впоследствии этот набор атрибутов можно будет импортировать, и доступ к деривациям может быть получен через набор атрибутов верхнего уровня.

Используя эти технику, мы можем абстрагироваться от имён файлов. Эта техника позволяет вместо ссылки на пакет REPO/some/sub/dir/package.nix обращаться к деривации через importedRepo.package (или pkgs.package в нашем примере).

Для начала в текущем каталоге создадим default.nix:

{
  hello = import ./hello.nix;
  graphviz = import ./graphviz.nix;
}

Этот файл можно использовать из nix repl:

$ nix repl
nix-repl> :l default.nix
Added 2 variables.
nix-repl> hello
«derivation /nix/store/dkib02g54fpdqgpskswgp6m7bd7mgx89-hello.drv»
nix-repl> graphviz
«derivation /nix/store/zqv520v9mk13is0w980c91z7q1vkhhil-graphviz.drv»

Для nix-build мы должны передать параметр -A чтобы получить доступ к атрибуту из набора нужного выражения .nix:

$ nix-build default.nix -A hello
[...]
$ result/bin/hello
Hello, world!

Файл default.nix — особенный. Если каталог содержит default.nix, он используется как неявное выражение Nix для этого каталога. Благодаря этому мы, например, можем запустить nix-build -A hello, без явного указания default.nix.

Теперь nix-env можно использовать для установки пакета в пользовательское окружение:

$ nix-env -f . -iA graphviz
[...]
$ dot -V

Разберёмся, как работает эта команда:

  • Параметр -f ссылается на выражение. В нашем случае это выражение из ./default.nix текущего каталога.

  • Параметр -i запускает «установку» («installation»).

  • Параметр -A имеет тот же смысл, что и в nix-build.

Мы воспроизвели самое базовое поведение nixpkgs: объединили несколько дериваций в один набор атрибутов верхнего уровня.

Паттерн Inputs (Входящие)

У подхода, который мы рассмотрели, есть несколько проблем:

  • Во-первых, hello.nix и graphviz.nix зависят от nigpkgs, который они импортируют напрямую. Лучшим подходом была бы передача nixpkgs в качестве аргумента, как в autotools.nix.

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

  • В-третьих, у нас нет возможности протестировать graphviz с определённой версией libgd.

До сих пор наш подход к решению этих проблем был неадекватным и требовал изменения выражения Nix в зависимости от наших потребностей. С помощью паттерна Входящие мы предлагаем другое решение: пусть пользователь меняет параметр inputs выражения.

Когда мы говорим о «входящих параметрах выражения», мы имеем в виду деривации, нужные для сборки выражения. В нашем случае:

  • mkDerivation из autotools. Напомним, что mkDerivation имеет неявную зависимость от инструментария.

  • libgd и её зависимости.

Каталог ./scr также передаётся через параметр, но мы не станем менять исходный код в скрипте сборки. В nixpkgs при повышении версии предпочитают написать ещё одно выражение (в том числе из-за патчей или отличающихся входящих параметров).

Наша цель — создать независимое от репозитория выражение для пакета. Чтобы этого добиться, мы используем функции для объявления входящих параметров для деривации.
Например, мы отредактируем graphviz.nix так, чтобы деривация стала настраиваемой и независимой от репозитория:

{ mkDerivation, lib, gdSupport ? true, gd, pkg-config }:

mkDerivation {
  name = "graphviz";
  src = ./graphviz-2.49.3.tar.gz;
  buildInputs =
    if gdSupport
      then [
        pkg-config
        (lib.getLib gd)
        (lib.getDev gd)
      ]
      else [];
}

Напомню, что {...}: ... — это синтаксис определения функции, принимающей набор атрибутов в качестве аргумента, так что этот пример просто определяет функцию.

Мы сделали gd и её зависимости опциональными. Если параметр gdSupport равен true (по умолчанию это именно так), мы заполняем buildInputs и graphviz будет собран с поддержкой gd. В противном случае, если набор атрибутов передаётся с gdSupport = false;, пакет будет собран без поддержки gd.

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

let
  pkgs = import  { };
  mkDerivation = import ./autotools.nix pkgs;
in
with pkgs;
{
  hello = import ./hello.nix { inherit mkDerivation; };
  graphviz = import ./graphviz.nix {
    inherit
      mkDerivation
      lib
      gd
      pkg-config
      ;
  };
  graphvizCore = import ./graphviz.nix {
    inherit
      mkDerivation
      lib
      gd
      pkg-config
      ;
    gdSupport = false;
  };
}

Мы разделили импорт nixpkgs и mkDerivation, и также добавили вариант сборки graphviz без поддержки gd. Теперь и hello.nix (оставленный читателю в качестве упражнения), и graphviz.nixб во-первых, не зависят от репозитория, а во вторых, настраиваются с помощью входящих параметров.

Если мы захотим собрать graphviz с нужной версией gd, достаточно будет передать gd = ...;

Если мы захотим изменить инструмент сборки, мы передадим другую реализацию mkDerivation.

Давайте взглянем на этот фрагмент внимательней, и разберём, как он работает:

  • Выражение default.nix возвращает набор атрибутов с ключами hello, graphviz и graphvizCore.

  • С помощью let мы определяем несколько локальных переменных.

  • Мы включаем pkgs в область видимости, определяя набор пакетов. Это избавляет нас от необходимости многократно набирать pkgs.

  • Мы импортируем hello.nix и graphviz.nix, каждый из которых возвращает функцию. Мы вызываем функции с набором входных параметров, чтобы получить деривации.

  • Синтаксис inherit x эквивалентен x = x. Строка inherit gd скомбинированная с with pkgs; эквивалентна gd = pkgs.gd.

Весь репозиторий, посвящённый пилюле 12, можно найти в этом GitHub Gist. (У термина gist нет устоявшегося перевода на русский язык. В целом, gist — это штука, которая позволяет ссылаться не на весь код в репозитории, а на его фрагменты).

Заключение

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

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

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

В следующей пилюле мы поговорим про паттерн проектирования »Вызов пакета». Он избавляет от необходимости дублировать имена входных параметров: и в default.nix, и в пакетном выражении. Благодаря »Вызову пакета» мы можем неявно передать входные параметры из выражения верхнего уровня.

© Habrahabr.ru