Nix and Flakes
Nix
Nix это странный пакетный менеджер и одноименный специализированный функциональный язык, используемый им в качестве DSL. Пакетный менеджер функциональный в математическом смысле, т.е. обладает таким свойствами как иммутабельность (одни и те же входные данные/зависимости дают один и тот же результат) и отсутствие побочных эффектов (изменения входных данных/зависимостей создает новый результат, предыдущий остается таким каким был, в общем та же иммутабельность). Основное назначение - воспроизводимые сборки. Т.е. это в первую очередь про воспроизводимое разворачивание чистого окружения, а не переключение между состояниями, хотя это тоже у него замечательно получается - смена конфигураций атомарная. До массового внедрения виртуализации, обновление серверов до следующего релиза для админов воспринималось как испытание, теперь же голова про это не болит: оно или обновится, или останется в нетронутом состоянии. Разумеется такое удовольствие не бесплатно, места на диске занимает как Windows)
Как мы знаем, проекты можно собирать с указанием директории установки, для autotools это ключик --prefix
, для CMake переменная CMAKE_INSTALL_PREFIX
. Значение по умолчанию как правило /usr
либо /usr/local
. Nix же каждый проект устанавливает в отдельную папочку:
$ nix eval nixpkgs#qtcreator.outPath
"/nix/store/xxnfns4iphr9cb9k2cda28wm8wxgn7v6-qtcreator-11.0.1"
$ nix eval nixpkgs#cmake.outPath
"/nix/store/0dv0ylafnx7cdajyv9ahbpqrniblixq1-cmake-3.26.4"
В начале имени хэш от всех зависимостей и полей пакета. Потом из этих отдельных кусочков симлинками на файловой системе и подстановкой путей в переменные среды собирается требуемое окружение. Строится оно по тем же принципам, что сборка отдельных пакетов. Подробней про внутреннею магию можно почитать в аутентичном источнике, меня же в первую очередь интересовала возможность гибкой настройки окружение для разработки.
В минимальном варианте оно поднимается так:
$ nix-shell -p stdenv cmake ninja
[nix-shell:~]$ gcc --version
gcc (GCC) 12.3.0
[nix-shell:~]$ cmake --version
cmake version 3.26.4
[nix-shell:~]$ ninja --version
1.11.1
Но можно довольно маленьким кусочком кода декларативно настраивать окружение под свои задачи и делать это очень тонко. Для начала как описывается сам пакет, default.nix:
{ stdenv
, cmake
, ninja
, boost
, openssl
}:
stdenv.mkDerivation {
name = "boost_asio_awaitable_ext";
src = ./.;
buildInputs = [ boost openssl ];
nativeBuildInputs = [ cmake ninja ];
}
Это функция, принимающая зависимости (перечислены в начале) и вычисляющая derivation. Входные данные для derivation это структура, в полях которой перечислены имя пакета, откуда брать исходники (в готовых пакетах это как правило ссылка на коммит с Github и его хеш) и зависимости. Непосредственно какими командами выполнять сборку не указано, Nix достаточно умен, чтобы автоматически использовать CMake. Явно запустить сборку пакета можно так:
$ nix-build -E 'with import <nixpkgs> {}; callPackage ./default.nix {}'
тут мы подгружаем (import
) репозиторий nixpkgs
, разворачиваем его в текущее пространство имен (with
), и вызываем функцию callPackage
, передавая ей путь к пакету и пустую структуру доп.параметров. Функция callPackage
вызывает функцию из файла default.nix, автоматически подставляя зависимости (компилятор, Boost и т.д.) из nixpkgs
. Сам репозиторий это тоже выражение на Nix, но большое, составленное из .nix файлов, в нем живут все пакеты/функции. Каждый пакет описывается отдельным .nix файлом и в каждом из них есть поле src
, указывающее откуда взять исходники и какой у них хеш. За счет этого зависимости определяются с точностью до одного коммита в их исходниках.
Явное задание всех зависимостей как параметров как раз и позволяет тонко настраивать окружение. Для разработки обычно нужно отключение оптимизаций и включение отладочных режимов у библиотек. Еще хочется последний компилятор. shell.nix:
{ pkgs ? import <nixpkgs> {} }:
with pkgs; let
developEnv = (overrideCC stdenv gcc13);
boost = enableDebugging (boost182.override {
stdenv = developEnv;
enableDebug = true;
});
in
mkShell.override { stdenv = developEnv; } {
name = "developEnv";
packages = [
gdb
];
hardeningDisable = [ "all" ];
inputsFrom = [
(callPackage ./default.nix {
stdenv = developEnv;
inherit boost;
})
];
}
Это конечно тоже функция, на вход она принимает репозиторий в параметре pkgs
, либо подгружает nixpkgs
как дефолтное (текущую ревизию) значение, если параметр не задан. with pkgs
- разворачивание репозитория в текущий неймспейс, между let
и in
задаются локальные переменные, в них определен используемый компилятор и Boost со всеми отладочными флажками. mkShell
вычисляет derivation окружения, используя в качестве зависимости (inpustFrom = [ callPackage ./default.nix ...
) функцию собирающую пакет из default.nix. Заодно в это окружение добавляется отладчик (packages = [ gdb ]
) и отключаются оптимизации компилятора (hardeningDisable = [ "all" ]
). Поднимается это окружение простой командой nix-shell
в директории содержащей нужный нам файл shell.nix. Более просто можно получить окружение с дефолтными настройками, то самое, в котором будет производится сборка пакета:
$ nix-shell -E 'with import <nixpkgs> {}; callPackage ./default.nix {}'
Для этого используется тоже Nix-выражение, что и для сборки пакета, но без запуска самой сборки, вместо команды nix-build используется nix-shell. Синтаксис конечно своеобразен, вроде несложный, но очень непривычен.
Но все равно сборка получается не полностью воспроизводимой. Она опирается на локальную копию nixpkgs
, на разных машинах они могут быть различны. Конечно проблемы с разъездом версий nixpks
всегда можно решать просто накатывая последнюю версию (nix-channel --update
). Но хочется более удобный в использовании инструмент, которым ревизии зависимостей можно указывать явно или фиксировать/обновлять их одним движением. И это исправляют снежинки.
Flakes
Повторять оригинальную документацию не буду и просто покажу как выглядит небольшая снежинка, flake.nix:
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux.default = pkgs.callPackage ./default.nix {};
devShells.x86_64-linux.default = import ./shell.nix { inherit pkgs; };
};
}
В ней указан один вход - репозиторий nixpkgs
лежащий на гитхабе, ветка unstable. Входов у снежинки может быть несколько, в качестве адреса можно указывать почти любую ссылку, в плане ее интерпретации эта штука довольно всеядная. Выход у снежинки это функция принимающая наши инпуты и возвращающая структуру с определенными полями: packages
- описывает сам пакет, опциональный devShells
- шелл для разработки. Я переиспользовал существующий код (default.nix/shell.nix), но в будущем предполагается, что весь код конфигурации будет жить в снежике, а для обратной совместимости уже придумали враппер. Репозиторий nixpkgs
передается как место где его искать, его непосредственая загрузка выполняется в блоке let
/in
. Nix - это про декларативность, поэтому тип платформы (x86_64-linux) нужно указывать явно. Разумеется не обязательно для каждой платформы все повторять, можно описать это функциями как это сделано тут, Nix достаточно мощный язык.
Прежде всего снежинку нужно добавить в систему контроля версий:
$ git add flake.nix
Flakes довольно плотно интегрированы с контролем версий, это требование обязательное.
После этого можно запустить сборку пакета командой nix build
или получить шелл для разработки скомандовав nix develop
. Т.к. у снежинки зависимости не зафиксированы, то первым делом создается JSON-файл flake.lock
, в него сохраняются ревизии всех зависимостей (мы же в качестве входа указали ветку unstable nixpkgs
, а не конкретный коммит).
$ nix build
warning: Git tree '/home/dvetutnev/boost_asio_awaitable_ext' is dirty
warning: creating lock file '/home/dvetutnev/boost_asio_awaitable_ext/flake.lock'
warning: Git tree '/home/dvetutnev/boost_asio_awaitable_ext' is dirty
Файл ревизий flake.lock также добавляем в контроль версий и фиксируем его вместе с flake.nix. А теперь практическая эксплуатация этой магии:
$ nix flake clone -f coro-copy github:dvetutnev/boost_asio_awaitable_ext/main
$ cd coro-copy
$ nix develop
Этими тремя командами мы получили локальную копию проекта и шелл для разработки со всеми зависимостями с точностью до одного коммита каждой из них (включая компилятор). И это воспроизводимо на любой машине, где работает Flake.
Обновить ревизии зависимостей можно лаконичной командой nix flake update
, если ей еще дописать ключик --commit-lock-file
, то они автоматически зафиксируются. Можно обновлять и отдельный инпут: nix flake update nixpkgs
.
Порог вхождения заметно выше чем для классических пакетных менеджеров. И общесистемных apt
/yum
/pacman
, и специализированных conan
/pip
/npm
. Но максимальная гибкость, декларативность и воспроизводимость того стоят. То, что собралось при помощи Nix один раз, уже никогда не сломается и будет работать на любой другой машине где есть Nix.