Разработка с помощью nix-shell

Добро пожаловать на десятую пилюлю Nix. В предыдущей девятой пилюле мы познакомились с одной из мощных возможностей Nix: автоматическим обнаружения зависимостей времени выполнения. Заодно мы завершили разработку пакета GNU hello.

В этой пилюле мы познакомимся с утилитой nix-shell и попробуем с её помощью взломать программу hello. Мы узнаем, что nix-shell создаёт для нас изолированную среду с возможностью редактирования исходных файлов проекта, точно также, как nix-build создаёт изолированную среду во время сборки деривации.

В конце концов мы сделаем наш скрипт сборки более эргономичным, ориентируясь на возможности nix-shell

Что такое nix-shell?

Утилита nix-shell помещает нас в командную оболочку с настроенными переменными окружения, необходимыми для сборки деривации. Она не запускает сборку; она готовит плацдарм для пошаговой сборки проекта.

Давайте вспомним, что в Nix у нас нет доступа к библиотекам или программам до тех пор, пока они не установлены с помощью nix-env. Впрочем, установка библиотек через nix-env не считается хорошей практикой. Вместо этого мы предпочитаем изолированное окружение для разработки, доступное благодаря утилите nix-shell.

Мы можем передать в nix-shell любое выражение Nix, возвращающее деривацию, но в переменной PATH, которая в конечном итоге попадёт в bash, не будет нужных нам утилит.

$ nix-shell hello.nix
[nix-shell]$ make
bash: make: command not found
[nix-shell]$ echo $baseInputs
/nix/store/jff4a6zqi0yrladx3kwy4v6844s3swpc-gnutar-1.27.1 [...]

Такая оболочка в лучшем случае бесполезна. Было бы разумно ожидать, что программы, описанные в $buildInputs, попадают в PATH (в том числе и программа GNU make), но в нашем случае это не так.

Однако, у нас есть переменные окружения, которые мы установили в деривации, в частности $baseInputs, $buildInputs, $src и др.

Это значит, что мы можем запустить source с параметром builder.sh и она построит деривацию. На этапе установки у вас может возникнуть ошибка, потому что ваш пользователь не имеет прав записи в /nix/store:

[nix-shell]$ source builder.sh
...

Деривация не установилась, но она была построена. Обратите внимание вот на что:

  • Мы запустили builder.sh и он выполнил все шаги сборки, включая настройку PATH.
  • Рабочий каталог — больше не временный каталог, созданный nix-build, а каталог, в котором мы запустили оболочку.
  • Таким образом, hello-2.10 был распакован в текущий каталог.

Мы можем войти в каталог hello-2.10 и запустить make, поскольку make теперь доступен.

Это подтверждает, что nix-shell помещает нас в оболочку с тем же (или очень похожим) окружением, что и во время сборки.

Сборщик для nix-shell

Предыдущие шаги требуют ручного запуска команд и не оптимизированы для работы с nix-shell. Сейчас мы сделаем наш сборщик более дружественным по отношению к nix-shell.

Вот несколько пунктов, которые нам надо изменить.

Во-первых, когда мы загружаем builder.sh, мы загружаем его в текущий каталог. Что мы действительно хотим, так это поместить builder.sh в хранилище Nix, поскольку утилита nix-build использует именно этот файл. Корректный способ в том, чтобы передать в деривацию правильную переменную окружения. (Обратите внимание, что переменная $builder уже определена, но она указывает на исполняемый файл bash вместо builder.sh. Наш builder.sh передаётся в bash как аргумент.)

Во-вторых, мы не хотим запускать сборку полностью, мы собираемся всего лишь настроить окружение, нужное для ручной сборки проекта. Так что мы можем разбить builder.sh на два файла: setup.sh для настройки окружения и настоящий builder.sh, который отправится в nix-build.

В процессе рефакторинга мы завернём этапы сборки в функции, чтобы придать больше структуры нашему дизайну. Дополнительно, мы перенесём set -e из файла настройки в файл сборки. Команда set -e в nix-shell раздражает, так как она завершает работу оболочки при возникновении ошибки.

Вот наш исправленный autotools.nix. Примечательным является атрибут setup = ./setup.sh в деривации, который добавляет setup.sh в хранилище Nix и соответственно инициализирует переменную окружения $setup в сборщике.

pkgs: attrs:
let
  defaultAttrs = {
    builder = "${pkgs.bash}/bin/bash";
    args = [ ./builder.sh ];
    setup = ./setup.sh;
    baseInputs = with pkgs; [
      gnutar
      gzip
      gnumake
      gcc
      coreutils
      gawk
      gnused
      gnugrep
      binutils.bintools
      patchelf
      findutils
    ];
    buildInputs = [ ];
    system = builtins.currentSystem;
  };
in
derivation (defaultAttrs // attrs)

Благодаря этому мы можем разделить builder.sh на setup.sh и builder.sh. Задача builder.sh заключается в том, чтобы загрузить $setup и вызвать функцию genericBuild. Всё остальное — небольшие изменения в скрипте bash.

Вот исправленная версия builder.sh:

set -e
source $setup
genericBuild

Вот новая добавленная версия setup.sh:

unset PATH
for p in $baseInputs $buildInputs; do
    export PATH=$p/bin${PATH:+:}$PATH
done

function unpackPhase() {
    tar -xzf $src

    for d in *; do
    if [ -d "$d" ]; then
        cd "$d"
        break
    fi
    done
}

function configurePhase() {
    ./configure --prefix=$out
}

function buildPhase() {
    make
}

function installPhase() {
    make install
}

function fixupPhase() {
    find $out -type f -exec patchelf --shrink-rpath '{}' \; -exec strip '{}' \; 2>/dev/null
}

function genericBuild() {
    unpackPhase
    configurePhase
    buildPhase
    installPhase
    fixupPhase
}

Наконец, вот hello.nix:

let
  pkgs = import <nixpkgs> { };
  mkDerivation = import ./autotools.nix pkgs;
in
mkDerivation {
  name = "hello";
  src = ./hello-2.12.1.tar.gz;
}

Возвращаемся в nix-shell:

$ nix-shell hello.nix
[nix-shell]$ source $setup
[nix-shell]$

Теперь, скажем, вы можете запустить unpackPhase, которая распакует $src и зайдёт в каталог. И вы можете запускать такие команды, как ./configure, make и т.д. вручную, или запускать фазы с помощью соответствующих функций.

Всё правильно: процесс настолько прост, насколько вам кажется. nix-shell собирает файл .drv и все его входные зависимости, и затем запускает командную оболочку с настроенными переменными окружения, необходимыми для сборки .drv. В частности, переменные окружения в оболочке совпадают с теми, которые передаются в функцию derivation.

Заключение

С помощью nix-shell мы можем запустить изолированное окружение, подходящее для разработки проекта. Это окружение предоставляет необходимые зависимости для оболочки разработчика, подобно тому, как nix-build предоставляет необходимые зависимости сборщику. Дополнительно, мы можем собирать и отлаживать проект вручную, выполняя его пошагово, как мы делали бы в любой другой среде разработки. Заметьте, что мы никогда не устанавливаем такие инструменты, как gcc или make в систему; эти инструменты и библиотеки изолированы и доступны попроектно.

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

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