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.

Subscribe to Заметочки

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe