Добро пожаловать на двенадцатую пилюлю 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 <nixpkgs> { };
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.nix
, где мы описали переменную 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 <nixpkgs> { };
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 <nixpkgs> { };
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
, и в пакетном выражении. Благодаря "Вызову пакета
" мы можем неявно передать входные параметры из выражения верхнего уровня.