Введение в The Nix Package Manager

Пытка первая: язык Nix.

Язык Nix сам по себе довольно простой, его можно характеризовать как чисто функциональный json на стероидах. Но что такое чистые функции и стероиды у json-а?

  1. (Чистая) Функциональность – Нет принципов ООП или ПП. Функции работают по принципу чистого λ-исчисления: 1 аргумент – 1 выражение, от него зависящее. Пример:

    x: x # Функция id
    x: x * x
    x: y: x * y

    Как дать им имя? Через let ... in

    let square = x: x * x;
    in square 12

    Тестируем в nix repl:

    nix-repl>   let square = x: x * x;
            >   in square 12
    144
  2. Attrsets – JSON на стероидах. Attrset – тип данных. По факту словарь, но ключи можно писать без кавычек.

    let myset = {
          key1 = 1337;
          "key2" = 159;
        };
        myfunc = x: y: x + y;
        # Или аргументом
        myfunc2 = set: set.key1 + set.key2;
        # Pattern matching (важно):
        myfunc3 = {key1, key2}: key1 + key2;
        myfunc4 = {key1, ...}@set: key1 + set.key2;
    in [
      (myfunc myset.key1 myset.key2) # Скобки нужны, т.к. иначе это будет список из функций итд
      (myfunc2 myset)
      (myfunc3 myset)
      (myfunc4 myset)
    ] # Списки без запятых

    Результат:

    [
      1496
      1496
      1496
      1496
    ]
  3. Остальные типы вам хорошо знакомы: str, int, float, ... Замечание про строчки: синтаксис "str", для мультилайна есть

    ''
    a
    b
    c
    ''

    Строго говоря, типы у этих синтаксисов строчек отличаются, но обычно приводятся.

Пытка вторая: Что такое пакет?

Nix пропагандирует декларативную сборку пакетов (или дериваций, это такое промежуточное представление, которое передаётся демону nix-daemon). Как это работает? Давайте представим, что у нас есть некий package set. Например

rec { # Рекурсивный сет (антипаттерн, но мне можно)
  stdenv = { # вкладывать можно
    mkDerivation = args: ...; # Функция для создания дериваций (+- примитив)
  };
  fetchgit = url: ...; # Упрощенно представленная функция для скачки репо (в реальности нужен хеш)
  gcc = <уже готовый gcc>;
  ncurses = <уже готовая ncurses>;
}

Нам нужно описать свой пакет. Представим его в виде функции вида packageset -> package

{ stdenv, gcc, ncurses, fetchgit }: # PATTERN MATCHING!!!
   stdenv.mkDerivation {
    pname = "hello";
    version = "99.99";
    src = fetchgit "ssh://hellocreator@git.gud/hello";

    nativeBuildInputs = [
      gcc
    ]; # Проги, нужные на момент компиляции. Дефолт -- gnumake (включает в себя хук для переписывания buildPhase).

    buildInputs = [
      ncurses
    ]; # Тоже проги для комптайма (+ рантайма, но не прям в PATH), но либы для динамической линковки -- сюда.

    configurePhase = ''
      echo "CONFIGURED"
    ''; # Дефолтная configurePhase -- "./configure --prefix=$out". ВЫЗЫВАЕТСЯ ТОЛЬКО ПРИ НАЛИЧИИ ФАЙЛА configure!

    buildPhase = ''
      gcc src/main.c
    ''; # Дефолтная buildPhase -- make (ИЗ-ЗА ХУКА В nativeBuildInputs)

    installPhase = "mkdir -p $out/bin && cp a.out $out/bin/hello";
  };

Обратите внимание, pattern matching здесь обязателен, инструменты оптимизации никса не позволят вам получить нужные вам вещи, если вы заберёте аргумент set и попытаетесь взять set.gcc. Они смотрят именно на сигнатуру паттерн матчинга.

mkDerivation – что?

stdenv.mkDerivation – функция из дефолтного пекедж сета nixpkgs, использующая только примитивы. Она предоставляет обширный инструментарий, пробежимся по СОВСЕМ базовому:

  1. pname и version – вместе формируют name, можно использовать его. Нужно для того, чтобы nix мог идентифицировать пакет.
  2. src – путь к сурсам. функции типа fetchgit автоматом качают его в стор и возвращают путь. О сторе позже.
  3. Фазы. Вот это, пожалуй, самое интересное

  4. nativeBuildInputs и buildInputs

Пытка третья. Стор

Пытка четвёртая. И что мне делать с этой функцией?

Вот мы написали функцию. А как собрать пакет?

  1. Legacy метод. Быстро, не очень репродуктивно (каналы). Создаём default.nix:

    let ourHelloPackage = { stdenv, ... }: ...; # Код выше
    in { pkgs ? import <nixpkgs> {} }: # Функция с дефолтным аргументом. <nixpkgs> -- системный канал nixpkgs.
    pkgs.callPackage ourHelloPackage {} # {} -- доп аргументы, нам не нужны.

    Запускаем nix-build и видим ./result – симлинку на путь в сторе.

  2. Флейки. Репродуцируются засчёт flake.lock. Создаём flake.nix:

    {
      inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05"; # Путь к ветке на гх имеет такой сахар
      outputs = { nixpkgs, ... }: # Тоже функция. "..." нужно, т.к. мы игнорируем параметр self.
      let ourHelloPackage = { stdenv, ... }: ...; # Код выше
          system = "x86_64-linux";
          pkgs = import nixpkgs { inherit system; }; # Указываем систему. inherit system ~~ system = system.
      in {
        packages.${system}.default = pkgs.callPackage ourHelloPackage {}; # Интерполяция. Система так же обязательна.
      };
    }

    Запускаем nix build и видим ./result – симлинку на путь в сторе. Подробнее про флейки (это ОЧЕНЬ мощная и большая штука): https://wiki.nixos.org/wiki/Flakes. (Да и вообще, вики невероятно полезное.)

Пытка пятая. Дальше сами (и ещё примечания)

Это был очень краткий и скомканный экскурс в мир функциональных пакетов. Я считаю, что упомянул важные моменты, а дальше вы свободны гуглить и использовать Nix(OS), как вам вздумается.