Универсальные скрипты сборки

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

В этом посте мы обобщим скрипт сборки, напишем выражение Nix для GNU hello world и создадим обёртку над встроенной функцией derivation.

Упаковываем GNU hello world

В предыдущей пилюле мы упаковали простой файл .c, который был скомпилирован с помощью обычного вызова gcc. Это не самый удачный пример проекта. Многие используют autotools и, поскольку мы хотим обобщить наш скрипт, лучше ориентироваться на самую популярную систему сборки.

GNU hello world не смотря на своё название, это всё ещё простой проект, собираемый при помощи autotools. Загрузите последний архив отсюда: https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz.

Создадим скрипт сборки для GNU hello world, назовём его hello_builder.sh:

export PATH="$gnutar/bin:$gcc/bin:$gnumake/bin:$coreutils/bin:$gawk/bin:$gzip/bin:$gnugrep/bin:$gnused/bin:$bintools/bin"
tar -xzf $src
cd hello-2.12.1
./configure --prefix=$out
make
make install

Деривация hello.nix:

let
  pkgs = import <nixpkgs> { };
in
derivation {
  name = "hello";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./hello_builder.sh ];
  inherit (pkgs)
    gnutar
    gzip
    gnumake
    gcc
    coreutils
    gawk
    gnused
    gnugrep
    ;
  bintools = pkgs.binutils.bintools;
  src = ./hello-2.12.1.tar.gz;
  system = builtins.currentSystem;
}

Nix в Darwin

Сборка в Darwin (т.е. macOS) в качестве компилятора C традиционно использует clang вместо gcc. Чтобы адаптировать наш пример под Darwin, напишем такую модифицированную версию hello.nix:

let
  pkgs = import <nixpkgs> { };
in
derivation {
  name = "hello";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./hello_builder.sh ];
  inherit (pkgs)
    gnutar
    gzip
    gnumake
    coreutils
    gawk
    gnused
    gnugrep
    ;
  gcc = pkgs.clang;
  bintools = pkgs.clang.bintools.bintools_bin;
  src = ./hello-2.12.1.tar.gz;
  system = builtins.currentSystem;
}

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

Соберём программу, запустив nix build hello.nix. Теперь можно выполнить result/bin/hello. Всё довольно просто, но надо ли писать builder.sh для каждого пакета? Надо ли всегда передавать зависимости в функцию derivation?

Пожалуйста, обратите внимание на параметр --prefix=$out, который мы обсуждали в предыдущей пилюле.

Универсальный скрипт

Обобщим builder.sh на все проекты autotools:

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

tar -xf $src

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

./configure --prefix=$out
make
make install

Что мы делаем?

  1. С помощью set -e просим оболочку прерывать выполнение скрипта в случае любой ошибки.
  2. Вначале очищаем PATH`` (unset PATH`), потому что в этом месте переменная содержит несуществующие пути.
  3. В конец каждого пути из $buildInputs дописываем bin и всё вместе добавляем к PATH. Подробности обсуждим чуть позже.
  4. Распоковываем исходники.
  5. Ищем каталог, куда были распакованы исходники и переходим в него, выполнив команду cd.
  6. Наконец, конфигурируем, компилируем и устанавливаем проект.

Как видите, в скрипте сборки больше нет никаких ссылок на “hello”. Скрипт по прежнему опирается на несколько соглашений, но безусловно, это версия более универсальна.

Теперь перепишем hello.nix:

let
  pkgs = import <nixpkgs> { };
in
derivation {
  name = "hello";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./builder.sh ];
  buildInputs = with pkgs; [
    gnutar
    gzip
    gnumake
    gcc
    coreutils
    gawk
    gnused
    gnugrep
    binutils.bintools
  ];
  src = ./hello-2.12.1.tar.gz;
  system = builtins.currentSystem;
}```

Тут всё ясно, за исключением, может быть, `buildInputs`.
Но и в `buildInputs` нет никакой чёрной магии.

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

```text
nix-repl> builtins.toString 123
"123"

nix-repl> builtins.toString [ 123 456 ]
"123 456"

Вспомним, что и деривации можно конвертировать в строку, поэтому:

nix-repl> :l <nixpkgs>
Added 3950 variables.

nix-repl> builtins.toString gnugrep
"/nix/store/g5gdylclfh6d224kqh9sja290pk186xd-gnugrep-2.14"

nix-repl> builtins.toString [ gnugrep gnused ]
"/nix/store/g5gdylclfh6d224kqh9sja290pk186xd-gnugrep-2.14 /nix/store/krgdc4sknzpw8iyk9p20lhqfd52kjmg0-gnused-4.2.2"

Вот так всё просто! Переменная buildInputs в конечном итогде будет содержать нужные нам пути, разделённые пробелом. Нет ничего лучше для использования в цикле for интерпретатора bash.

Удобная версия функции derivation

Нам удалось написать скрипт, который можно использовать для разных проектов autotools. Но в выражении hello.nix мы определяем все программы, которые могут потребоваться, включая те, которые не нужны для сборки конкретного проекта.

Мы можем написать функцию, которая также, как и derivation, принимает набор атрибутов, и сливает его с другим набором атрибутов, общим для всех проектов.

autotools.nix:

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

Чтобы разобраться, как работает этот код, вспоминм кое-что о фукнциях Nix. Всё выржаение Nix из файла autotools.nix превращается в функцию. Эта функция принимает параметр pkgs и возвращает функцию, которая принимает параметр attrs.

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

  1. Сначала добавляем в обласить видимость магический набор атрибутов pkgs.
  2. С помощью выражения let определяем вспомогательную переменную defaultAttrs, куда складываем несколько атрибутов, нужных для деривации.
  3. В конце создаём вызываем derivation, передавая в качестве параметра странное выражение (defaultAttrs // attrs).

Оператор // — принимает на вход два набора. Результатом является их объединение. В случае конфликта имён атрибутов, используется значение из правого набора.

Так что мы используем defaultAttrs как основу, и добавляем (переопределяем) туда атрибуты из attrs.

Пара примеров прояснит работу оператора:

nix-repl> { a = "b"; } // { c = "d"; }
{ a = "b"; c = "d"; }

nix-repl> { a = "b"; } // { a = "c"; }
{ a = "c"; }

Упражнение: Завершите новый скрипт builder.sh добавив $baseInputs в цикл for вместе с $buildInputs.

Результат оператора // мы передаём в функцию derivation. Атрибут buildInputs пустой, поэтому он будет иметь точно то значение, которое указано в наборе attrs.

Перепишем hello.nix:

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

Финал! Мы получили простейшее описание пакета! Несколько комментариев, которые помогут вам лучше разораться в языке Nix.

  • Мы помещаем в переменную pkgs импорт, который в предыдущих выражениях помещали в оператор “with”. Это обычная практика, не стоит её опасться.
  • Переменная mkDerivation — прекрасный пример частичного применения. На неё можно смотреть, как как на ‘(import ./autotools.nix) pkgs’.
  • Вначале мы импортируем выражение, затем применяем его к параметру pkgs1.
  • Это даёт нам функцию, которая принимает набор атрибутов attrs.
  • Мы создаём деривацию, указывая только атрибуты name и src. Если проекту нужны другие знависимости в PATH, их можно добавить в buildInputs, но в примере с hello.nix нам это было не нужно.

Обратие внимание, что мы не используем никаких других библиотек. Нам могут потребоваться флаги компилятора C, чтобы искать включаемые файлы других библиотек. Также, нам могут потребоваться флаги компоновщика, чтобы искать статические библиотечные.

Заключение

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

В этой пилюле мы написали универсальный скрипт сборки проектов autotools и функцию mkDerivation. Последняя объединяет основные компоненты, используемые в проектах autotools с настройками по умолчанию, и избавляет нас от дублирования кода в разных проектах.

Мы познакомились с тем, как расширять систему Nix: мы пишем и объединяем новые деривации.

Аналогия: в C вы создаёте объекты, которые находятся в куче, и затем на их основе создаёте новые объекты. Для ссылки на другие объекты используются указатели.

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

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

…мы поговорим про зависимости от среды выполнения. Является ли пакет GNU hello world автономным? Каковы зависимости его среды выполнения? Пока что мы определили зависимости для сборки, посредством использования других дериваций в деривации “hello”.

1

В функциональных языках вызов функции с параметром часто называют применением функции к параметру.