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

Сегодня мы поговорим о специальном атрибуте — config.packageOverrides. Переопределение пакетов в наборе с неподвижной точкой можно рассматривать как ещё один паттерн проектирования в nixpkgs.

Переопределение пакета

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

Мы поместили функцию override в набор атрибутов, возвращаемый оригинальной функцией.

Возьмём, например, пакет graphviz. У него есть входящий параметр xorg. Если он равен null, graphviz будет собран без поддержки X Window System.

$ nix repl
nix-repl> :l <nixpkgs>
Added 4360 variables.
nix-repl> :b graphviz.override { withXorg = false; }

Этот вызов соберёт graphviz без поддержки X, всё настолько просто.

Теперь пусть, скажем, пакет P зависит от graphviz, как сделать, чтобы P зависел от новой версии graphviz без поддержки X?

В императивном мире...

...вы могли бы сделать что-то такое:

pkgs = import <nixpkgs> {};
pkgs.graphviz = pkgs.graphviz.override { withXorg = false; };
build(pkgs.P)

Учитывая, что pkgs.P зависит от pkgs.graphviz, легко собрать P с изменённым graphviz. В чистом функциональном языке это не так просто, поскольку ваши переменные иммутабельны и вы не можете присваивать им новые значения.

Неподвижная точка

(Комментарий переводчика: автор цикла с места в карьер использует термин неподвижная точка, который неизвестен разработчикам, не сталкивавшимся с функциональным программированием. Это интересная, но не самая простая концепция — трудно объяснить её «на пальцах». Не вдаваясь в подробности, скажу, что неподвижная точка — остроумное решение парадоксальной задачи. В функциональных языках часто используют лямбда-функции, они же анонимные функции, то есть не имеющие имени. Функцию, у которой есть имя, можно сделать рекурсивной — она может вызвать сама себя по имени.

А как сделать рекурсивной анонимную функцию? Ответ — с помощью второй функции специального вида, которая и носит название неподвижной точки. Так что, встретив в тексте «неподвижную точку», вы можете мысленно подставлять вместо вместо неё слова «штука для рекурсии». Этого хватит, чтобы прочитать статью, но я бы посоветовал изучить вопрос глубже. Просто для удовольствия.)

Неподвижная точка с ленивым вычислением — не самая удобная, но необходимая часть языка Nix. С помощью неё можно решить нашу задачу с пакетами P и graphviz.

Вот определение неподвижной точки в nixpkgs:

{
  # Взять функцию и запустить её с результатом, который она вернула.
  fix =
    f:
    let
      result = f result;
    in
    result;
}

Это функция, которая принимает функцию f и вызывает её с параметром result, а результат вызова снова передаёт функции f, и так далее.
Иными словами, это f(f(f(...

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

nix-repl> fix = f: let result = f result; in result
nix-repl> pkgs = self: { a = 3; b = 4; c = self.a+self.b; }
nix-repl> fix pkgs
{ a = 3; b = 4; c = 7; }

У нас получилось сослаться на a и b в том же самом наборе без ключевого слова rec.

  • Сначала вызывается pkgs с пока ещё не вычисленными параметрами (pkgs(pkgs(...)

  • Чтобы установить значение c, вычисляются self.a и self.b.

  • Функция pkgs вызывается снова, чтобы получить значения a и b.

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

Не стану вдаваться здесь в дальнейшие объяснения. Хорошее введение в неподвижную точку можно найти здесь (англ.).

Переопределение набора с помощью неподвижной точки

Учитывая, что self.a и self.b ссылаются на переданный набор, а не на константы в определении функции pkgs, мы можем переопределить a и b, и получить новое значение для c:

nix-repl> overrides = { a = 1; b = 2; }
nix-repl> let newpkgs = pkgs (newpkgs // overrides); in newpkgs
{ a = 3; b = 4; c = 3; }
nix-repl> let newpkgs = pkgs (newpkgs // overrides); in newpkgs // overrides
{ a = 1; b = 2; c = 3; }

В первом случае мы вычислили pkgs с переопределённым набором, а во втором, помимо этого, мы включили переопределённые атрибуты в результат.

Переопределение пакетов nixpkgs

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

Для этого nixpkgs предлагает config.packageOverrides. Так nixpkgs возвращает неподвижную точку набора пакетов, с помощью которой можно переопределять параметры.

Создайте файл config.nix следующего вида:

{
    packageOverrides = pkgs: {
    graphviz = pkgs.graphviz.override {
      # запретить поддержку xorg
      withXorg = false;
    };
  };
}

Теперь мы можем собрать, скажем, asciidoc-full который использует переопределённый graphviz:

nix-repl> pkgs = import <nixpkgs> { config = import ./config.nix; }
nix-repl> :b pkgs.asciidoc-full

Обратите внимание, как мы передаём config с атрибутом packageOverrides, когда импортируем nixpkgs. Здесь pkgs.asciidoc-full — это деривация с входящим параметром graphviz (при этом pkgs.asciidoc — это облегчённая версия, которая в принципе не использует graphviz).

Поскольку в кэше исполняемых файлов нет версии asciidoc с graphviz без поддержки X Window System, Nix перекомпилирует нужные вам компоненты.

Файл ~/.config/nixpkgs/config.nix

Мы уже обсуждали этот файл в предыдущей пилюле. Обычно там находится что-то похожее на config.nix, который мы только что написали.

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

Заключение

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

Императивные пакетные менеджеры устанавливают новые версии библиотек поверх старых. В Nix не всё так просто и прямолинейно. Но более аккуратно.

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

Новая версия asciidoc будет зависеть от новой версии graphviz, а старая версия asciidoc без помех продолжит работать со старой версию graphviz.

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

...мы на время оставим изучение nixpkgs и поговорим о путях хранения. Как Nix генерирует путь в хранилище, где будет размещена сборка? И как добавить в хранилище файлы, для которых предусмотрена проверка целостности?

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