[Перевод] 7. Nix в пилюлях: Работающая деривация

38bdcb73b56b7f2c8f75dde647605f7f.jpg

Введение

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

В этом посте мы продолжим двигаться тем же курсом, создав деривацию, которая действительно что-то собирает. Затем мы попытаемся создать пакет с реальной программой: скомпилируем простой файл .c и создадим деривацию, используя классический инструментарий.

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

Использование скрипта для сборки

Каков простейший способ выполнить цепочку команд, чтобы что-нибудь собрать?
Скрипт. Напишем свой скрипт builder.sh, и запустим его при сборке деривации: bash builder.sh.

Мы не можем использовать шебанг в builder.sh, поскольку во время написания мы не знаем путь к bash в хранилище Nix. Не смотря на то, что bash конечно же есть в хранилище, ведь там есть всё!

(Shebang или hash-bang, как в оригинале у Люка — символы »#!», которые идут в начале любого скрипта и указывают пусть к интерпретатору — прим. переводчика).

Мы даже не можем сослаться на /usr/bin/env, потому что тогда мы потеряем такое крутое свойство Nix, как отсутствие состояния. Это не говоря о том, что PATH очищается во время сборки, так что bash всё равно не будет найден.

В итоге, чтобы собрать деривацию с помощью bash, мы должны вызывать его, передав аргумент builder.sh. Оказывается, функция derivation принимает опциональный атрибут args, который используется для передачи аргументов исполняемому файлу сборки.

Для начала, запишем наш builder.sh в текущий каталог:

declare -xp
echo foo > $out

Команда declare -xp выводит список эскпортированных переменных (declare — встроенная функция bash). В предыдущей пилюле мы выяснили, что Nix вычисляет выходной путь деривации на основании её атрибутов. Получившийся файл .drv содержит список переменных окружения, передаваемых в скрипт сборки. И одна из этих переменных — $out.

Что мы должны сделать, так это создать что-то по пути $out — файл или каталог.
В нашем примере мы создаём файл.

Дополнительно, мы выводим переменные среды в процессе построения. Мы не можем использовать env для этого, потому что env является частью coreutils, и этой зависимости у нас пока нет. Сейчас у нас есть только bash.

Как и в случае с coreutils из предыдущей пилюли, мы без усилий получаем доступ к bash, благодаря нашей волшебной «груде всего», которая называется nixpkgs.

nix-repl> :l 
Added 3950 variables.
nix-repl> "${bash}"
"/nix/store/ihmkc7z2wqk3bbipfnlh0yjrlfkkgnv6-bash-4.2-p45"

Так что с помощью небольшого трюка, мы можем сослаться на bin/bash и создать нашу деривацию:

nix-repl> d = derivation { name = "foo"; builder = "${bash}/bin/bash"; args = [ ./builder.sh ]; system = builtins.currentSystem; }
nix-repl> :b d
[1 built, 0.0 MiB DL]

this derivation produced the following outputs:
  out -> /nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo

У нас получилось! Содержимым файла /nix/store/w024zci0x1hh1wj6gjq0jagkc1sgrf5r-foo является как раз строка «foo». Мы собрали нашу первую деривацию.

Обратите внимание, что мы использовали ./builder.sh, а не "./builder.sh". Благодаря этому Nix понимает, что речь идёт о пути, и может выполнить кое-какую магию, которую мы обсудим позже. Попробуйте строковую версию и вы увидите, что Nix не сможет найти builder.sh.
Это потому, что он пытается найти скрипт по относительному пути от временного каталога, где идёт сборка.

Окружение скрипта сборки

Мы можем использовать nix-store --read-log, чтобы посмотреть, какие логи записал наш скрипт:

$ nix-store --read-log /nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo
declare -x HOME="/homeless-shelter"
declare -x NIX_BUILD_CORES="4"
declare -x NIX_BUILD_TOP="/tmp/nix-build-foo.drv-0"
declare -x NIX_LOG_FD="2"
declare -x NIX_STORE="/nix/store"
declare -x OLDPWD
declare -x PATH="/path-not-set"
declare -x PWD="/tmp/nix-build-foo.drv-0"
declare -x SHLVL="1"
declare -x TEMP="/tmp/nix-build-foo.drv-0"
declare -x TEMPDIR="/tmp/nix-build-foo.drv-0"
declare -x TMP="/tmp/nix-build-foo.drv-0"
declare -x TMPDIR="/tmp/nix-build-foo.drv-0"
declare -x builder="/nix/store/q1g0rl8zfmz7r371fp5p42p4acmv297d-bash-4.4-p19/bin/bash"
declare -x name="foo"
declare -x out="/nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo"
declare -x system="x86_64-linux"

Давайте исследуем эти переменные среды, напечатаннные в процессе сборки.

  • $HOME — это не ваш домашний каталог, и /homeless-shelter вообще не существует. Мы заставляем пакеты не зависеть от $HOME в процессе построения.

  • $PATH также, как и $HOME не содержит реальных путей.

  • $NIX_BUILD_CORES и $NIX_STORE — это конфигурационные опции Nix.

  • $PWD и $TMP указывают на временный каталог, который Nix создал для сборки.

  • $builder, $name, $out и $system — переменные, получившие значения из файла .drv.

Переменная $out содержит путь к деривации, куда нам надо что-нибудь сохранить. Выглядит так, будто Nix зарезервировал для нас слот в хранилище, который мы должны заполнить.

В терминах autotools, $out — то же самое, что и путь --prefix. Да, не переменная DESTDIR из make, а именно --prefix. Вот суть создания пакетов без состояния. Вы не устанавливаете пакет по глобальному пути относительно /, вы устанавливаете его по локальному изолированному пути в слот вашего хранилища Nix.

Содержимое файлов .drv

В примере выше мы добавили в деривацию атрибут args. Как это изменило файл .drv по сравнению с примером из предыдущей пилюли?

$ nix derivation show /nix/store/i76pr1cz0za3i9r6xq518bqqvd2raspw-foo.drv
{
  "/nix/store/i76pr1cz0za3i9r6xq518bqqvd2raspw-foo.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo"
      }
    },
    "inputSrcs": [
      "/nix/store/lb0n38r2b20r8rl1k45a7s4pj6ny22f7-builder.sh"
    ],
    "inputDrvs": {
      "/nix/store/hcgwbx42mcxr7ksnv0i1fg7kw6jvxshb-bash-4.4-p19.drv": [
        "out"
      ]
    },
    "platform": "x86_64-linux",
    "builder": "/nix/store/q1g0rl8zfmz7r371fp5p42p4acmv297d-bash-4.4-p19/bin/bash",
    "args": [
      "/nix/store/lb0n38r2b20r8rl1k45a7s4pj6ny22f7-builder.sh"
    ],
    "env": {
      "builder": "/nix/store/q1g0rl8zfmz7r371fp5p42p4acmv297d-bash-4.4-p19/bin/bash",
      "name": "foo",
      "out": "/nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo",
      "system": "x86_64-linux"
    }
  }
}

Похоже на старый добрый файл .drv, за исключением списка аргументов, который передаётся в bash. Этот способ на самом деле содержит один скрипт builder.sh, который каким-то образом оказался в хранилище Nix. Дело в том, что Nix автоматически копирует в хранилище файлы и каталоги, нужные для сборки. Это гарантирует, что они не изменятся в процессе сборки, и что при развёртывании не может быть никаких состояний или зависимостей от текущей машины.

Поскольку builder.sh — обычный файл, с ним не могут быть ассцииорованы никакие файлы .drv. Путь в хранилище вычисляется на основании имени файла и хэша его содержимого. Путь в хранилище подробно обсуждаются в одной из следующих пилюль.

Создаём пакет из простой программы на C

Напишем простую программу на C, поместив её в файл simple.c:

void main() {
  puts("Simple!");
}

А вот скрипт сборки simple_builder.sh:

export PATH="$coreutils/bin:$gcc/bin"
mkdir $out
gcc -o $out/simple $src

Не бескойтесь о том, откуда возьмутся эти переменные; давайте пока опишем деривацию и соберём её:

nix-repl> :l 
nix-repl> simple = derivation { name = "simple"; builder = "${bash}/bin/bash"; args = [ ./simple_builder.sh ]; gcc = gcc; coreutils = coreutils; src = ./simple.c; system = builtins.currentSystem; }
nix-repl> :b simple
this derivation produced the following outputs:

  out -> /nix/store/ni66p4jfqksbmsl616llx3fbs1d232d4-simple

Теперь можно запустить /nix/store/ni66p4jfqksbmsl616llx3fbs1d232d4-simple/simple в вашем bash.

Объяснение

Мы добавили два новых атрибута к вызову derivation: gcc и coreutils. В gcc = gcc;, слева находится имя в наборе деривации, а справа — ссылка на деривацию gcc из nixpkgs. То же касается и coreutils.

Мы также добавили атрибут src. Здесь ничего магического — атрибуту присвоен путь ./simple.c. Так же, как и simple-builder.sh, simple.c будет добавлен в хранилище.

Трюк: каждый атрибут в наборе, переданный в derivation будет сконвертирован в строку и передан в скрипт сборки как переменная окружения. Именно так скрипт получает доступ к coreutils и gcc: при конвертации в строку, деривации превращаются в их выходные пути, а добавление к ним /bin ведёт нас к их исполняемым файлам.

То же касается и переменной src. $src — это путь к simple.c в хранилище Nix. В качестве упражнение выведите .drv в читаемом виде. Вы увидите среди входных дериваций simple_builder.sh и simple.c, наряду с файлами .drv, относящимися к bash, gcc и coreutils. Кроме того, вы увидите новые добавленные переменные окружения, описанные выше.

В simple_builder.sh мы установили PATH для исполняемых файлов gcc и coreutils, так что скрипт сборки может найти нужные утилиты вроде mkdir и gcc.

Затем мы создали $out как каталог и разместили бинарные файлы внутри него. Обратите внимание, что gcc найден через переменную окружения PATH, но на него точно также можно было бы сослаться явно, используя $gcc/bin/gcc.

Избавляемся от nix repl

Попробуем повторить те же шаги, избавившись от nix repl, и написав выражение Nix в файле simple.nix:

let
  pkgs = import  { };
in
pkgs.stdenv.mkDerivation {
  name = "simple";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./simple_builder.sh ];
  gcc = pkgs.gcc;
  coreutils = pkgs.coreutils;
  src = ./simple.c;
  system = builtins.currentSystem;
}

Собрать деривацию можно с помощью команды nix-build simple.nix. Она создаст в текущем каталоге символическую ссылку result, указывающую на выходной путь деривации.

nix-buils выполняет две задачи:

  • nix-instantiate: разбирает и выполняет simple.nix и возвращает файл .drv, относящийся к разобранному набору деривации

  • nix-store -r: исполняет файл .drv, что в действительности строит деривацию.

В конце концов он создаёт символическую ссылку.

Во второй строке simple.nix у нас есть вызов функции import. Вспомните, что import принимает один аргумент — файл .nix для загрузки. Содержимое файла выполняется, как будто это функция.

Мы вызываем эту функцию с пустым набором. Подобный вызов мы видели в пятой пилюле. Ещё раз обратите внимание: конструкция import { } вызывает две функции, не одну. Чтобы стало яснее, прочитайте выражение как (import ) { }.

Значение, возвращаемое функцией nixpkgs это набор; более точно, это набор дериваций.
Вызов import { } в выражении let создаёт локальную переменную pkgs и вводит её в область видимости. Тот же эффект воникает и при выполнении инструкции :l , который мы использовали в nix repl. Набор pkgs, позволяет нам легко обращаться к таким деривациям, как bash, gcc и coreutils. На эти деривации надо ссылаться, как на атрибуты набора pkgs, т.е. писать pkgs.bach вместо bash.

Ниже представлена исправленная версия simple.nix, использующая ключевое слово inherit:

let
  pkgs = import  { };
in
pkgs.stdenv.mkDerivation {
  name = "simple";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./simple_builder.sh ];
  inherit (pkgs) gcc coreutils;
  src = ./simple.c;
  system = builtins.currentSystem;
}

Для чего используется ключевое слово inherit?
inherit foo; является эквивалентом foo = foo;. Точно также inherit gcc coretutils; — эквивалент для gcc = gcc; coreutils = coreutils;. Наконец, inherit (pkgs) gcc coreutils; — эквивалент для gcc = pkgs.gcc; coreutils = pkgs.coreutils;.

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

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

Мы напишем универсальный скрипт сборки. Наверное вы заметили, что мы в этом посте написали два разных скрипта builder.sh. Было бы лучше, если бы у нас был универсальный скрипт, особенно, учитывая, что каждый скрипт сохраняется в хранилище Nix, а это затратно.

Это и правда так трудно — делать пакеты в Nix? Нет, мы просто изучаем основы.

© Habrahabr.ru