Наша первая деривация

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

В этом посте мы, наконец, добрались до написания деривации. Деривации, с точки зрения файловой системы — это строительные блоки системы Nix. Для описания дериваций используется язык Nix.

Напоминаю, как входить в окружение Nix: source ~/.nix-profile/etc/profile.d/nix.sh

Функция “derivation”

Для создания дериваций используется встроенная функция `derivation``. Прежде, чем двигаться дальше, пройдите по ссылке и познакомьтесь с тем, что написано в официальном руководстве. С точки зрения языка Nix, деривация — всего лишь набор с несколькими атрибутами, так что вы можете хранить её в переменной и передавать в другие функции, как любое другое значение.

Здесь-то и появляется реальная мощь.

Функция derivation в качестве первого агрумента принимает набор. Он требует по меньшей мере следующих трёх атрибутов:

  • name: название деривации. В хранилище Nix деривации хранятся в формате /hash-name, и часть name берётся из этого атрибута.
  • system: название системы, в которой деривация может быть собрана. Например, x86_64-linux.
  • builder: программа, которая собирает деривацию — бинарный образ или скрипт.

Прежде всего надо выяснить, какое, с точки зрения Nix, название у нашей системы?

nix-repl> builtins.currentSystem
"x86_64-linux"

Что будет, если использовать несуществующее название системы?

nix-repl> d = derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; }
nix-repl> d
«derivation /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv»

Так, так, что это? Скрипт собрал деривацию? Нет, не собрал, он всего лишь создал файл .drv. nix repl не собирает деривации, пока вы явно не попросите об этом.

Отсутпление про файлы .drv

Что такое файл .drv? Это спецификация, как собрать деривацию, без лишнего языкового шума, который появляется при работе с любыми высокоуровневыми языкми, в том числе и с Nix.

Чтобы разобраться, можно провести несколько аналогий с языком C:

  • Файлы .nix похожи на файлы .c.
  • Файлы .drv являются промежуточными файлами наподобие файлов .o. Файл .drv описывает, как собрать деривацию. Здесь самый минимум информации.
  • В конечном итоге результатом сборки является выходной путь.

Как вы видите из примера, и выходной путь, и путь к файлу .drv ведут в хранилище.

Так что внутри файла .drv? Его можно прочитать, но лучше распечатать его содержимое в понятном виде:

ℹ️ Если в вашей версия Nix нет команды nix derivation show, используйте вместо неё nix show-derivation.

$ nix derivation show /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv
{
  "/nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
      }
    },
    "inputSrcs": [],
    "inputDrvs": {},
    "platform": "mysystem",
    "builder": "mybuilder",
    "args": [],
    "env": {
      "builder": "mybuilder",
      "name": "myname",
      "out": "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname",
      "system": "mysystem"
    }
  }
}

Так, мы видим здесь выходной путь, но на диске его ещё нет. Даже до начала сборки мы точно знаем, куда будут записаны файлы. Для чего это сделано?

Сборка больших дериваций, таких, как Firefox, занимает много времени. Если бы Nix пришлось собирать их всякий раз, даже если мы не хотим их запускать, а просто что-то проверить, нам бы пришлось дожидаться завершения сборки. Поэтому Nix заранее вычисляет путь, но ничего туда не записывает.

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

В файле .drv заполнены не все поля, так что я вкратце опишу их назначение:

  1. Выходные пути (их может быть несколько). По умолчанию Nix создаёт один выходной путь, называемый “out”.
  2. Список входных дериваций. Он пуст, поскольку мы не ссылались на другие деривации. В противном случае, здесь был бы список других файлов .drv.
  3. Название системы (system) и путь к сборщику (да, сейчас это ненастоящая программа).
  4. Наконец, список переменных окружения, передаваемых в программу сборки.

Это минимум информации, нужной для сборки деривации.

Важное замечание: переменные окружения, передаваемые в сборщик — не только те, которые вы видите в файле .drv, но и некоторые другие, относящиеся к конфигурации Nix (количество ядер, временный каталог и др.). Сборщик не наследут никаких переменных из вашей оболочки, иначе сборки бы страдали от не-детерминизма.

Но вернёмся к нашей учебной деривации.

Попробуем её собрать, не смотря на то, что она почти ненастоящая.

nix-repl> d = derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; }
nix-repl> :b d
[...]
these derivations will be built:
  /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv
building path(s) `/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname'
error: a `mysystem' is required to build `/nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv', but I am a `x86_64-linux'

Команда :b доступна только в nix repl, она используется для сборки деривации. Вы можете получить помощь по командам, введя :?. На экране мы видим путь к файлу .drv, в котором написано, как собирать деривацию. Далее там написано, какой путь будет нашим выходным путём. Наконец, мы видим ошибку, которую и должны были увидеть: деривация не может быть собрана в нашей системе.

Мы выполняем построение с помощью nix repl, но это не единственный способ. Вы можете реализовать файл .drv с помощью команды:

\$ nix-store -r /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv

На экране будет напечатано то же самое.

Давайте исправим атрибут `system``:

nix-repl> d = derivation { name = "myname"; builder = "mybuilder"; system = builtins.currentSystem; }
nix-repl> :b d
[...]
build error: invalid file name `mybuilder'

В качестве программы сборки указана mybuilder, которой в действительности не существует. К чему приведёт запуск такого файла .drv?

Что есть в наборе деривации?

Для начала давайте взглянем на результат функции derivation. Это простой набор:

nix-repl> d = derivation { name = "myname"; builder = "mybuilder"; system = "mysystem"; }
nix-repl> builtins.isAttrs d
true
nix-repl> builtins.attrNames d
[ "all" "builder" "drvAttrs" "drvPath" "name" "out" "outPath" "outputName" "system" "type" ]

Вы можете догадаться, что делает builtins.isAttrs: она возвращает true, если аргумент является набором. Вторая функция, builtins.attrNames, возвращает список ключей из заданного набора. Можно сказать, что это своего рода рефлексия.

Начнём с drvAttrs:

nix-repl> d.drvAttrs
{ builder = "mybuilder"; name = "myname"; system = "mysystem"; }

По сути, это входные данные, которые мы передали функции derivation. Конкретно d.name, d.system и d.builder — те атрибуты, которые мы указали при вызове.

nix-repl> (d == d.out)
true

А здесь мы видим, что значение атрибута out — это и есть деривация, что кажется странным. Причина в том, что у деривации может быть только один выходной путь. По этой же причине d.all — это синглтон. Позже мы узнаем, как сделать несколько выходных путей у одной деривации.

d.drvPath — это пусть к файлу .drv: /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv.

Кое-что интересное об атрибует type. Его значение "derivation". Nix добавляет небольшую магию при работе с наборами типа derivation, но действительно небольшую. Чтобы разобраться, создайте набор с типом “derivation”:

nix-repl> { type = "derivation"; }
«derivation ???»

Конечно, в нём нет никакой информации, так что Nix не знает, что печатать. Как понимаете, type = "derivation" — всего лишь соглашение для Nix и для нас, чтобы мы понимали, что набор — это деривация.

При создании пакетов, нас интересует конечный результат — файлы, записанные на диск. Все остальные метаданные нужны Nix чтобы знать, как определить путь к файлу .drv и выходной путь out.

Атрибут outPath — это выходной путь в хранилище Nix: /nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname.

Ссылки на другие деривации

Другие пакетные менеджеры позволяют пакетам ссылаться друг на друга. А как сослаться на другие деривации в Nix, какие указывать пути? Для этого используется атрибут outPath, который содержит путь к файлам нужной деривации. Для удобства Nix умеет конвертировать набор деривации в строку.

nix-repl> d.outPath
"/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
nix-repl> builtins.toString d
"/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"

Nix выполняет “конверсию набора в строку”, если в наборе есть атрибут outPath (это что-то вроде метода toString в других языках):

nix-repl> builtins.toString { outPath = "foo"; }
"foo"
nix-repl> builtins.toString { a = "b"; }
error: cannot coerce a set to a string, at (string):1:1

Скажем, мы хотим использовать бинарные программы из coreutils:

nix-repl> :l <nixpkgs>
Added 3950 variables.
nix-repl> coreutils
«derivation /nix/store/1zcs1y4n27lqs0gw4v038i303pb89rw6-coreutils-8.21.drv»
nix-repl> builtins.toString coreutils
"/nix/store/8w4cbiy7wqvaqsnsnb3zvabq1cp2zhyz-coreutils-8.21"

Безотносительно nixpkgs, представьте, будто мы добавили в область видимости переменные и одна из них это coreutils. Это деривация пакета coreutils, который может быть знаком вам по другим дистрибутивам Linux — он содержит основные программы для систем GNU/Linux. В вашем хранилище может быть несколько дериваций coreutils.

$ ls /nix/store/*coreutils*/bin
[...]

Напоминаю, что внутри строк можно интерполировать выражения Nix с помощью ${...}:

nix-repl> "${d}"
"/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
nix-repl> "${coreutils}"
"/nix/store/8w4cbiy7wqvaqsnsnb3zvabq1cp2zhyz-coreutils-8.21"

Это очень удобно, потому что мы можем ссылаться, скажем, на бинарник bin/true вот так:

nix-repl> "${coreutils}/bin/true"
"/nix/store/8w4cbiy7wqvaqsnsnb3zvabq1cp2zhyz-coreutils-8.21/bin/true"

Почти работающая деривация

В предыдущем примере мы использовали ненастоящую программу сборки, mybuilder, которой, очевидно, не существует. Для экспериментов мы можем вызвать программу bin/true, которая всегда завершается с кодом 0 (успех).

nix-repl> :l <nixpkgs>
nix-repl> d = derivation { name = "myname"; builder = "${coreutils}/bin/true"; system = builtins.currentSystem; }
nix-repl> :b d
[...]
builder for `/nix/store/qyfrcd53wmc0v22ymhhd5r6sz5xmdc8a-myname.drv' failed to produce output path `/nix/store/ly2k1vswbfmswr33hw0kf0ccilrpisnk-myname'

Как мы видим, выполняется программа сборки bin/true, которая ничего не создаёт ни файла, ни каталога по выходному пути, она просто завершается с кодом 0.

Очевидное замечание: каждый раз, когда мы меняем деривацию, вычисляется новый хэш.

Давайте проверим новый файл .drv, после того как мы сослались на другую деривацию:

$ nix derivation show /nix/store/qyfrcd53wmc0v22ymhhd5r6sz5xmdc8a-myname.drv
{
  "/nix/store/qyfrcd53wmc0v22ymhhd5r6sz5xmdc8a-myname.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/ly2k1vswbfmswr33hw0kf0ccilrpisnk-myname"
      }
    },
    "inputSrcs": [],
    "inputDrvs": {
      "/nix/store/hixdnzz2wp75x1jy65cysq06yl74vx7q-coreutils-8.29.drv": [
        "out"
      ]
    },
    "platform": "x86_64-linux",
    "builder": "/nix/store/qrxs7sabhqcr3j9ai0j0cp58zfnny0jz-coreutils-8.29/bin/true",
    "args": [],
    "env": {
      "builder": "/nix/store/qrxs7sabhqcr3j9ai0j0cp58zfnny0jz-coreutils-8.29/bin/true",
      "name": "myname",
      "out": "/nix/store/ly2k1vswbfmswr33hw0kf0ccilrpisnk-myname",
      "system": "x86_64-linux"
    }
  }
}

Ага! Nix добавил зависимость в наш myname.drv, это coreutils.drv. Перед тем, как собрать нашу деривацию, Nix должен построить coreutils.drv. Но, поскольку coreutils уже лежит в нашем хранилище, не нужно ничего строить, так что мы просто обращаемся по пути /nix/store/qrxs7sabhqcr3j9ai0j0cp58zfnny0jz-coreutils-8.29.

Когда деривация собирается на самом деле

Nix не строит дериваций в процессе вычисления выражений Nix. В частности, именно поэтому мы должны вызывать “:b drv” в nix repl или использовать nix-store -r.

В Nix существует важное разделение:

  • Время Инстанцирования/Вычисления: выражения Nix анализируются, интерпретируются и превращаются в набор деривации. При вычислении, вы можете ссылаться на другие деривации, потому что Nix создаст файлы .drv, для которых нам будут известны их выходные пути. Для этого мы используем команду nix-instantiate. – Время Реализации/Построения: деривация собирается из файла .drv, предварительно собрав деривации из файлов .drv, от которых она зависит. Для этого мы используем команду nix-store -r.

Процесс похож на компиляцию и компоновку в проектах C/C++. Сначала вы компилируете все исходные файлы в объектные файлы. А затем компонуете объектные файлы в один исполняемый файл.

В Nix, сначала выражение (обычно из файла .nix) компилируется в .drv, а затем каждый .drv собирается и записывается в хранилище по выходным путям.

Заключение

Сложно ли создать пакет в Nix? Нет, не сложно.

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

С помощью функции derivation мы описываем построение пакета, и принимаем готовый файл .drv в качестве результата. Nix конвертирует набор в строку, когда в наборе есть атрибут outPath. Благодаря этой удобной возможности, легко выстраивать записимости между деривациями.

Когда Nix собирает деривацию, он сначала из выражения создаёт файл .drv, а затем использует этот файл для сборки выходных файлов. Он делает это рекурсивно для всех зависимостей и “выполняет” файлы .drv друг за другом. Не так уж много магии, если подумать.

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

…мы в конце концов напишем нашу первую работающую деривацию. Да, этот пост тоже про “наше первую деривацию”, но я никогда не утверждал, что она будет работать.