Основные зависимости и хуки
Добро пожаловать на двадцатую пилюлю 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
помещаются только явные косвенные зависимости.
Хуки установки
Выше мы уже писали, что зависимости иногда должны влиять на пакеты не просто фактом своего существования1.
В качестве примера можно рассмотреть сам параметр 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; на самом деле именно это и сделано в обёртке над CC2. Но этот паттерн встречается достаточно часто, так что кто-то решил добавить несколько вспомогательных функций, чтобы сократить объём кода.
Вторая половина 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
, у каждой из которых есть свой этап загрузки.
Дайте мне знать, что вам интересно!
Теперь мы можем быть точнее и утверждать, что addToEnv
выполняет минимальную обработку зависимости, то есть к пакету, который является просто зависимостью, будет применяться только функция addToEnv
.
В версии nixpkgs
, актуальной на момент написания пилюли, он назывался Обёрткой над GCC;
Поддержка компиляторов Darwin и Clang не стала достаточным основанием, чтобы его переименовать.