Паттерн проектирования callPackage
Добро пожаловать на тринадцатую пилюлю Nix.
В предыдущей двенадцатой пилюле мы рассказали о первом из базовых паттернов проектирования репозиториев.
Кроме того, мы подготовили graphviz
, чтобы в нашем учебном репозитории было два пакета.
Следующий паттерн проектирования, с которым мы познакомимся, называется callPackage
.
Подход, который здесь описан, широко применяется в nixpkgs и в сейчас является стандартом де-факто при импорте пакетов.
Удобство callPackage
В предыдущей пилюле мы продемонстрировали, как паттерн входящие позволяет отвязать пакеты от репозитория. Это позволило нам явным образом передавать аргументы в деривацию: деривация объявляет параметры, а вызывающая сторона передаёт аргументы.
Но, как это часто бывает в языках программирования, нам приходится писать одно и то же два раза: сначала имена параметров, а затем те же самые имена аргументов.
{ input1, input2, ... }:
...
скорее всего, мы бы хотели присоединить эту деривацию к репозиторию через набор атрибутов, определенный как-то так:
rec {
lib1 = import package1.nix { inherit input1 input2; };
program2 = import package2.nix { inherit inputX inputY lib1; };
}
Есть две вещи, на которые стоит обратить внимание.
Во-первых, эти входящие параметры часто имеют те же имена, что и атрибуты в репозитории.
Во-вторых, из-за ключевого слова rec
входящие параметры деривации могут ссылаться на другие пакеты в репозитории.
Вместо того, чтобы дважды передавать входящие параметры, мы можем автоматически передавать их из репозитория, переопределяя их в случае необходимости.
Для этого нам потребуется такая функция callPackage
, чтобы её можно было вызывать вот так:
{
lib1 = callPackage package1.nix { };
program2 = callPackage package2.nix { someoverride = overriddenDerivation; };
}
Надо, чтобы callPackage
была функцией с двумя аргументами, которая делает следующее:
- Импортирует выражение, заданное в файле, который передаётся в первом аргументе, и возвращает функцию. Полученная функция возвращает деривацию пакета, при этом деривация использует паттерн Входящие.
- Определяет имена аргументов функции, то есть имена входящих параметров деривации.
- Передаёт значения аргументов из набора репозитория — мы назовём их значениями по умолчанию. В то же время она позволяет переопределить эти значения, если мы хотим поменять настройки деривации.
Реализуем callPackage
В этом разделе мы реализуем паттерн callPackage
с нуля.
Для начала разберёмся, как получать имена аргументов функции во время выполнения.
В нашем случае речь идёт о функции, которая создаёт деривацию.
Зная имена, мы сможем передавать аргументы автоматически.
Для этого Nix предоставляет встроенную функцию:
nix-repl> add = { a ? 3, b }: a+b
nix-repl> builtins.functionArgs add
{ a = true; b = false; }
Вместе с именами аргументов, набор атрибутов, возвращаемый functionArgs
содержит признак, есть ли у аргумента значение по умолчанию.
Сейчас для наших целей достаточно только имён аргументов.
Следующий шаг — сделать так, чтобы callPackage
автоматически передавал значения аргументов в нашу деривацию, основываясь на именах аргументов, которые мы узнали благодаря functionArgs
.
Для этого нам нужны две вещи:
- Набор репозитория с деривациями, чьи имена совпадают с именам полученных нами аргументов.
- Способ автоматически объединить набор атрибутов репозитория и значения, полученные из
functionArgs
.
Чтобы достичь первой цели, достаточно в качестве имён аргументов использовать имена пакетов репозитория, например, nixpkgs
.
Вторая цель достигается благодаря ещё одной встроенной функции Nix:
nix-repl> values = { a = 3; b = 5; c = 10; }
nix-repl> builtins.intersectAttrs values (builtins.functionArgs add)
{ a = true; b = false; }
nix-repl> builtins.intersectAttrs (builtins.functionArgs add) values
{ a = 3; b = 5; }
intersectAttrs
возвращает набор атрибутов, имена которых — это пересечение имён атрибутов обоих аргументов, а значения получены из второго аргумента.
Это всё, что нам нужно: мы получили имена аргументов из функции и заполнили их значениями из существующего набора атрибутов.
Вот простая учебная реализация callPackage
:
nix-repl> callPackage = set: f: f (builtins.intersectAttrs (builtins.functionArgs f) set)
nix-repl> callPackage values add
8
nix-repl> with values; add { inherit a b; }
8
Проанализируем этот фрагмент кода:
- Мы определяем функцию
callPackage
. - Первый параметр функции
callPackage
— это набор пар имя-значение. Часть из них может совпадать с набором аргументов вызываемой фукнции. - Второй параметр — вызываемая функция.
- Мы получаем имена аргументов функции и находим их пересечение с набором всех значений.
- Наконец, мы вызываем функцию
f
с полученным пересечением.
Фрагмент выше демонстрирует, что вызов callPackage
эквивалентен прямому вызову add a b
.
У нас получилось почти всё, что мы хотели: мы вызываем функции автоматически, передавая им набор аргументов.
Если аргумент в наборе не найден, мы получаем ошибку (если только у функции не переменное число аргументов, объявленных через ...
, как мы объясняли в пятой таблетке).
Последняя возможность, который мы хотим добиться — позволить пользователям переопределять параметры.
Нам не всегда подходят значения из большого набора.
Поэтому мы добавляем к callPackage
третий параметр, принимающий набор переопределений:
nix-repl> callPackage = set: f: overrides: f ((builtins.intersectAttrs (builtins.functionArgs f) set) // overrides)
nix-repl> callPackage values add { }
8
nix-repl> callPackage values add { b = 12; }
15
Код довольно ясный, не смотря на выросшее количество скобок. Здесь мы всего-навсего объединяем набор аргументов по умолчанию с набором переопределений.
Используем callPackage
, чтобы упростить код репозитория
Имея на руках функцию callPackage
, мы можем упростить выражение репозитория в default.nix
:
let
nixpkgs = import <nixpkgs> { };
allPkgs = nixpkgs // pkgs;
callPackage =
path: overrides:
let
f = import path;
in
f ((builtins.intersectAttrs (builtins.functionArgs f) allPkgs) // overrides);
pkgs = with nixpkgs; {
mkDerivation = import ./autotools.nix nixpkgs;
hello = callPackage ./hello.nix { };
graphviz = callPackage ./graphviz.nix { };
graphvizCore = callPackage ./graphviz.nix { gdSupport = false; };
};
in
pkgs
Разберёмся, как это работает:
- Выражение выше определяет наш собственный репозиторий пакетов, который мы называем
pkgs
. Он содержит пакетhello
и два варианта пакетаgraphviz
. - В выражении
let
мы импортируемnixpkgs
. Обратите внимание, что раньше мы ссылались на это значение с помощью переменнойpkgs
, но теперь это имя зарезервировано за репозиторием, который мы создаём. - Нам нужен способ как-то передать
pkgs
вcallPackage
. Вместо того, чтобы напрямую вернуть набор пакетов изdefault.nix
, мы сначала присваиваем его переменной в выраженииlet
, что позволяет передать его вcallPackage
. - В целях упрощения, мы передаём в
callPackage
не функцию, а имя файла, из которогоcallPackage
эту функцию импортирует. Иначе бы нам пришлось вручную импортировать каждый пакет. - Поскольку наши выражения используют пакеты из
nixpkgs
, вcallPackage
мы описываем переменнуюallPkgs
, в которую помещаем пакеты изnixpkgs
и наши пакеты. - Мы переместили функцию
mkDerivation
вpkgs
, чтобы она передавалась в параметрах автоматически.
Обратите внимание, как легко мы переопределили аргументы для создания graphviz
без gd
.
Кроме того, обратите внимание, как легко слить два репозитория — nixpkgs
и наш pkgs
!
Читатель должен заметить магическую вещь, которая здесь происходит.
Чтобы определить pkgs
, мы используем callPackage
, а при определении callPackage
мы используем pkgs
.
Эта магия работает благодаря ленивым вычислениям: builtins.intersectAttrs
не должен знать все значения из allPkgs
, чтобы найти пересечение, а только ключи, ни один из которых не требует вычисления callPackage
.
Заключение
Паттерн "callPackage
" значительно упростил код репозитория.
Мы смогли импортировать пакеты с именованными аргументами и вызывать их автоматически, при этом пользуясь пакетами из nixpkgs
.
Также мы познакомились с полезными встроенными функциями, которые позволили нам получить информацию о функциях Nix и манипулировать атрибутами. Эти встроенные функции обычно используются не для установки пакета, а, скорее, как инструменты при установке. Они описаны в Руководстве по Nix.
Создание репозитория в Nix — это эволюция разработки удобных функций для комбинирования пакетов. Эта пилюля демонстрирует, как Nix может быть универсальным инструментом для сборки и развёртывания программ и как он подходит для создания репозиториев, которые следуют нашим собственным соглашениями.
В следующей пилюле
В следующей пилюле мы поговорим о паттерне проектирования “переопределение
”.
Пакет graphvizCore
кажется простым.
Он использует скрипт graphviz.nix
и собирает пакет без поддержки gd
.
В следующей пилюле мы посмотрим на этот процесс с другой точки зрения: может быть стоит начать с функции pkgs.graphviz
и отключать gd
при её вызове?