Добро пожаловать на двадцатую пилюлю 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
, у каждой из которых есть свой этап загрузки. Дайте мне знать, что вам интересно!