Добро пожаловать на двадцатую пилюлю Nix. В предыдущей девятнадцатой пилюле мы познакомились с деривацией stdenv, где встретили скрипт setup.sh, вспомогательный скрипт default-builder.sh и функцию сборки stdenv.mkDerivation. Разбирались в том, как stdenv сводит всё это вместе, как он используется и немного — на фазах genericBuild.

Сегодня исследуем взаимодействие процесса сборки пакетов с stdevn.mkDerivation. Естественно, что пакеты зависят друг от друга. Мы можем описать зависимости с помощью атрибутов buildInputs и propagatedBuildInputs. Иногда входные пакеты должны влиять на зависимые пакеты образом, который невозможно предсказать заранее. Чтобы с этим справиться, у нас есть хуки установки и хуки окружения. Вместе эти 4 концепции обеспечивает практически любое взаимодействие при сборке пакета.

ℹ️ С течение времени, в основном, для поддержки кросс-компиляции, сложность инфраструктуры зависимостей и хуков выросла. Изучив основные концепции, вы сможете перейти к более сложным темам. Начать изучение можно вот с коммита 6675f0a5 в nixpkgs. Это последняя версия stdenv без поддержки кросс-компиляции.

Атрибут buildInputs

В простейшем случае, когда одному пакету нужен другой пакет, мы используем атрибут buildInputs. Это именно тот паттерн, который мы применяли в нашем скрипте сборки в Пилюле 8. Для демонстрации давайте соберём пакет GNU Hello, а затем другой пакет, в котором будет скрипт, запускающий программу hello.

let

  nixpkgs = import <nixpkgs> { };

  inherit (nixpkgs) stdenv fetchurl which;

  actualHello = stdenv.mkDerivation {
    name = "hello-2.3";

    src = fetchurl {
      url = "mirror://gnu/hello/hello-2.3.tar.bz2";
      sha256 = "0c7vijq8y68bpr7g6dh1gny0bff8qq81vnp4ch8pjzvg56wb3js1";
    };
  };

  wrappedHello = stdenv.mkDerivation {
    name = "hello-wrapper";

    buildInputs = [
      actualHello
      which
    ];

    unpackPhase = "true";

    installPhase = ''
      mkdir -p "$out/bin"
      echo "#! ${stdenv.shell}" >> "$out/bin/hello"
      echo "exec $(which hello)" >> "$out/bin/hello"
      chmod 0755 "$out/bin/hello"
    '';
  };
in
wrappedHello

Обратите внимание, что деривация wrappedHello находит программу hello через переменную PATH. Это работает, поскольку stdenv содержит такие строки:

pkgs=""
for i in $buildInputs; do
    findInputs $i
done

где findInputs определена, как:

findInputs() {
    local pkg=$1

    ## Не повторяем для уже обработанных пакетов
    case $pkgs in
        *\ $pkg\ *)
            return 0
            ;;
    esac

    pkgs="$pkgs $pkg "

    ## Здесь на самом деле есть кое-что ещё, что мы можем пока игнорировать
}

после этого выполняется:

for i in $pkgs; do
    addToEnv $i
done

где addToEnv определена как:

addToEnv() {
    local pkg=$1

    if test -d $1/bin; then
        addToSearchPath _PATH $1/bin
    fi

    ## Здесь на самом деле есть кое-что ещё, что мы можем пока игнорировать
}

Вызов addToSearchPath добавляет $1/bin к _PATH, если такой путь существует (код здесь). Как только все пакеты из buildInputs обработаны, содержимое _PATH добавляется в PATH:

PATH="${_PATH-}${_PATH:+${PATH:+:}}$PATH"

Если путь к hello прописан в PATH, фаза installPhase должна завершиться успешно.

Атрибут propagatedBuildInputs

Атрибут buildInputs покрывает прямые зависимости, но как быть с косвенными зависимостями, когда одному пакету нужен другой пакет, которому нужен третий? Nix и сам прекрасно с этим справляется, умея обрабатывать различные замыкания зависимостей, возникших при сборке предыдущих пакетов. Впрочем, buildInputs всё ещё удобнее, поскольку собирает каталоги pkg/bin в переменную окружения pkgs с последующим включением их в PATH. Для зависимых пакетов в stdenv для тех же целей используют атрибут propagatedBuildInputs:

let

  nixpkgs = import <nixpkgs> { };

  inherit (nixpkgs) stdenv fetchurl which;

  actualHello = stdenv.mkDerivation {
    name = "hello-2.3";

    src = fetchurl {
      url = "mirror://gnu/hello/hello-2.3.tar.bz2";
      sha256 = "0c7vijq8y68bpr7g6dh1gny0bff8qq81vnp4ch8pjzvg56wb3js1";
    };
  };

  intermediary = stdenv.mkDerivation {
    name = "middle-man";

    propagatedBuildInputs = [ actualHello ];

    unpackPhase = "true";

    installPhase = ''
      mkdir -p "$out"
    '';
  };

  wrappedHello = stdenv.mkDerivation {
    name = "hello-wrapper";

    buildInputs = [
      intermediary
      which
    ];

    unpackPhase = "true";

    installPhase = ''
      mkdir -p "$out/bin"
      echo "#! ${stdenv.shell}" >> "$out/bin/hello"
      echo "exec $(which hello)" >> "$out/bin/hello"
      chmod 0755 "$out/bin/hello"
    '';
  };
in
wrappedHello

Обратите внимание, что в пакете intermediary зависимость описана в propagatedBuildInputs, в то время, как wrappedHello получает зависимость через buildInputs.

Как это работает? Вы можете решить, что подобную штуку проворачивает Nix, но на самом деле она происходит не при выполнении кода, а во время сборки программы в bash. Давайте взглянем на фрагмент fixupPhase из stdenv:

fixupPhase() {

    ## Опущено

    if test -n "$propagatedBuildInputs"; then
        mkdir -p "$out/nix-support"
        echo "$propagatedBuildInputs" > "$out/nix-support/propagated-build-inputs"
    fi

    ## Опущено

}

Этот код сохраняет сборки, перечисленные в propagatedBuildInputs в одноимённом файле в каталоге $out/nix-support. Вернёмся к findInputs и исследуем строки, которые мы ранее пропустили:

findInputs() {
    local pkg=$1

    ## Здесь на самом деле есть кое-что ещё, что мы можем пока игнорировать

    if test -f $pkg/nix-support/propagated-build-inputs; then
        for i in $(cat $pkg/nix-support/propagated-build-inputs); do
            findInputs $i
        done
    fi
}

Функция findInputs на самом деле рекурсивна — она исследует propagatedBuildInputs каждой зависимости, затем propagatedBuildInputs этих зависимостей и т. д.

На самом деле мы упростили вызов findInputs в прошлых примерах: в действительности propagatedBuildInputs тоже зациклен:

pkgs=""
for i in $buildInputs $propagatedBuildInputs; do
    findInputs $i
done

Этот код демонстрирует важный момент. Для *текущего" пакета неважно, является ли зависимость косвенной или нет. Все они будут обработаны одним и тем же способом: вызваны через findInputs и переданы в addToEnv. (Пакеты, найденные функций findInputs, собранные в pkgs и переданные в addToEnv, в обоих случаях будут одни и те же.) Однако, в $out/nix-support/propagated-build-inputs помещаются только явные косвенные зависимости.

Хуки установки

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

В качестве примера можно рассмотреть сам параметр propagatedBuildInputs: пакеты, которые его используют, «проталкивают» зависимости в buildInputs зависимых пакетов. Однако, хотелось бы, чтобы зависимости могли оказывать на зависящие пакеты произвольное влияние. Произвольное здесь — ключевое слово. Можно научить setup.sh конкретным вещам, чему-то вроде pkg/nix-support/propagated-build-inputs, но не произвольному взаимодействию.

Хуки установки — основные строительные блоки, которые для этого применяются. В nixpkgs «хуки» — это, по сути, функции обратного вызова в bash, и хуки установки не является исключением. Взглянем на последнюю часть findInputs, которую мы пока игнорировали:

findInputs() {
    local pkg=$1

    ## Здесь на самом деле есть кое-что ещё, что мы можем пока игнорировать

    if test -f $pkg/nix-support/setup-hook; then
        source $pkg/nix-support/setup-hook
    fi

    ## Здесь на самом деле есть кое-что ещё, что мы можем пока игнорировать

}

Если в пакете есть скрипт с именем pkg/nix-support/setup-hook, он будет запущен с помощью команды source любым пакетом, основанным на stdenv и включающим исходный пакет, как зависимость.

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

Хуки окружения

Чтобы сделать написание скриптов сборки ещё удобнее, можно использовать хуки окружения.
Вспомните, как в Пилюле 12 мы собирали пути в NIX_CFLAGS_COMPILE для флага -I, и в NIX_LDFLAGS для флага -L так же, как до этого собирали их в PATH. Однако, это слишком специализированное решение для универсального скрипта сборки. Имеет смысл обрабатывать PATH особым образом, поскольку PATH используется оболочкой, а универсальный скрипт неразрывно связан с оболочкой. Но флаги -I и -L относятся только к компилятору C. Пакет stdenv не обязан как-то по особенному относиться к компилятору С (хотя, по факту, относится), ведь существуют другие компиляторы, у которых могут быть совершенно другие флаги.

В качестве первого шага мы можем переместить эту логику в хук установки для компилятора C; на самом деле именно это и сделано в обёртке над CC (в версии nixpkgs, актуальной на момент написания пилюли, он назывался Обёрткой над GCC;
поддержка компиляторов Darwin и Clang не стала достаточным основанием, чтобы его переименовать). Но этот паттерн встречается достаточно часто, так что кто-то решил добавить несколько вспомогательных функций, чтобы сократить объём кода.

Вторая половина addToEnv выглядит так:

addToEnv() {
    local pkg=$1

    ## Здесь на самом деле есть кое-что ещё, что мы можем пока игнорировать

    # Запускаем специфичные для пакета хуки, установленные в скриптах setup-hook
    for i in "${envHooks[@]}"; do
        $i $pkg
    done
}

Функции, перечисленные в envHooks, применяются к каждому пакету, переданному в addToEnv. Можно написать такой хук установки:

anEnvHook() {
    local pkg=$1

    echo "I'm depending on \"$pkg\""
}

envHooks+=(anEnvHook)

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

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

...я не уверен! Опираясь на знание о том, как работает stdenv, мы могли бы поговорить о других типах зависимостей и хуках, которые нужны при кросс-компиляции. Мы могли бы поговорить о том, как происходит загрузка nixpkgs. Или, мы могли бы поговорить о том, как localSystem и crossSystem превращаются в buildPlatform, hostPlatform и targetPlatform, у каждой из которых есть свой этап загрузки. Дайте мне знать, что вам интересно!

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