NixOS: От декларативного хаоса к надежной рабочей станции

Глубокое погружение в экосистему NixOS, охватывающее путь от понимания функциональной парадигмы управления системой до создания воспроизводимой среды разработки. Курс фокусируется на практическом применении языка Nix для настройки железа, софта и пользовательского окружения.

Философия Nix: Почему декларативный подход меняет всё

Философия Nix: Почему декларативный подход меняет всё

Вы решили обновить библиотеку для одного пет-проекта. Пакетный менеджер скачивает новую версию, попутно обновляя пару системных зависимостей. Проект запускается отлично. Но на следующий день вы открываете свой основной рабочий инструмент, а он падает с ошибкой: «version GLIBC_2.34 not found». Вы пытаетесь откатить обновление, но система сообщает, что это сломает графическое окружение. Добро пожаловать в «ад зависимостей» — фундаментальную проблему традиционных операционных систем, которую NixOS решает на архитектурном уровне.

Чтобы понять, как построить по-настоящему надежную рабочую станцию, нам нужно разобраться, почему ломаются обычные системы и как философия Nix меняет правила игры.

Глобальное изменяемое состояние: корень всех зол

Традиционные дистрибутивы Linux (Ubuntu, Arch, Fedora) работают по стандарту иерархии файловой системы (FHS). Это значит, что в системе есть глобальные директории: /bin для программ, /lib для библиотек, /etc для конфигураций.

Когда вы выполняете команду apt install nodejs, пакетный менеджер распаковывает файлы прямо в эти глобальные директории. Если двум разным программам нужны разные версии одной и той же библиотеки (например, OpenSSL 1.1 и OpenSSL 3.0), возникает конфликт. В глобальной папке /lib может лежать только один файл с конкретным именем.

Такая модель называется императивной. Вы управляете системой, отдавая ей последовательность команд: установи это, удали то, измени эту строчку в конфиге.

Проблема императивного подхода в том, что текущее состояние вашей ОС — это сумма всех команд, которые вы выполнили с момента установки. Если вы купите новый ноутбук, вы не сможете в точности воссоздать свою среду, пока не вспомните и не повторите каждую команду в правильном порядке.

Декларативный подход: от рецепта к меню

NixOS предлагает совершенно иную парадигму — декларативную.

Вместо того чтобы писать скрипты или вводить команды, шаг за шагом меняющие систему, вы описываете желаемое конечное состояние в одном конфигурационном файле. Вы говорите системе: «Мне нужен включенный SSH, браузер Firefox, Docker и таймзона Берлина». Как именно система к этому придет — задача пакетного менеджера Nix.

Особенность Императивный подход (Ubuntu, Arch) Декларативный подход (NixOS)
Управление Выполнение команд во времени (apt, pacman, rm, sed) Описание желаемого состояния в текстовом файле
Воспроизводимость Низкая (зависит от истории действий пользователя) Абсолютная (один конфиг = один результат на любой машине)
Разрешение конфликтов Ручное, часто через контейнеры (Docker) или виртуальные окружения Встроенное на уровне файловой системы
Откат изменений Сложен, часто требует бэкапов файловой системы (Timeshift, Btrfs snapshots) Мгновенный, встроен в архитектуру ОС

Декларативность сама по себе не нова (вспомните Ansible или Terraform), но NixOS уникальна тем, как глубоко эта концепция интегрирована в ядро системы. Это становится возможным благодаря особому хранилищу.

Анатомия чистоты: Как устроен /nix/store

Чтобы декларативный подход работал, NixOS полностью отказывается от стандарта FHS. В NixOS нет привычных директорий /bin или /lib. Вместо этого абсолютно всё — программы, библиотеки, конфигурационные файлы — хранится в едином месте: /nix/store.

Каждый объект в /nix/store лежит в собственной директории, имя которой начинается с криптографического хеша.

Например, путь к Python может выглядеть так: /nix/store/8n5z4...-python3-3.11.5/bin/python

Откуда берется этот хеш? Это не просто случайный набор символов. Хеш вычисляется на основе всех входных данных, необходимых для сборки этого пакета:

  • Исходного кода самого Python.
  • Версии компилятора (GCC), которым он собирался.
  • Всех библиотек, от которых он зависит (glibc, zlib, openssl).
  • Скрипта сборки.

Если изменится хотя бы один байт в исходном коде или обновится зависимость — изменится хеш. А значит, пакет будет установлен в совершенно новую директорию в /nix/store.

Этот механизм элегантно решает проблему «ада зависимостей». Если Программе А нужен OpenSSL 1.1, а Программе Б — OpenSSL 3.0, Nix просто скачает обе версии в разные директории с разными хешами. Программы будут жестко ссылаться (через абсолютные пути) только на свои версии библиотек. Они никогда не пересекутся и не сломают друг друга.

Неизменяемость и поколения (Generations)

Важнейшее свойство /nix/store — неизменяемость (immutability). Как только пакет собран и помещен в хранилище, его файлы становятся доступны только для чтения. Ни пользователь, ни программы не могут их изменить.

Но если всё неизменяемо, как обновлять систему?

В NixOS вы не обновляете систему «на месте». Когда вы меняете конфигурационный файл и просите систему применить изменения, Nix:

  1. Вычисляет, какие новые пакеты и конфиги нужны.
  2. Скачивает или собирает их в новые пути внутри /nix/store.
  3. Собирает из них новое состояние системы.
  4. Создает поколение (Generation).

Текущее состояние вашей операционной системы — это просто символическая ссылка (symlink), которая указывает на конкретное поколение в /nix/store. Обновление системы — это атомарная операция: Nix просто переключает один симлинк со старого поколения на новое.

Поскольку старые файлы в /nix/store не перезаписываются и не удаляются (пока вы сами не запустите сборщика мусора), предыдущее поколение остается полностью целым и работоспособным.

Если после обновления у вас не загружается графический интерфейс или отвалился Wi-Fi, вам не нужно искать логи и пытаться откатить пакеты. Вы просто перезагружаете компьютер, в меню загрузчика (GRUB/systemd-boot) выбираете предыдущее поколение, и система загружается ровно в том состоянии, в котором была до обновления. Это дает невероятную свободу: вы можете смело экспериментировать с настройками, зная, что систему невозможно сломать безвозвратно.

Что это значит для разработчика?

Для повседневной разработки декларативный подход NixOS означает конец эпохи «работает на моей машине».

Ваша рабочая станция становится полностью детерминированной. Конфигурационный файл — это единый источник истины. Вы можете выложить его на GitHub, скачать на новый ноутбук, запустить одну команду — и через 10 минут получить точную копию своей системы: с теми же версиями компиляторов, теми же настройками IDE и теми же утилитами командной строки.

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

Анатомия системы: Структура конфигурации и процесс сборки поколения

Анатомия системы: Структура конфигурации и процесс сборки поколения

Вы открываете текстовый файл, добавляете в него одно слово, запускаете команду, и вся операционная система трансформируется в точном соответствии с этим новым описанием. Никаких промежуточных скриптов установки, никаких скрытых изменений состояния. Чтобы понять, как декларативный текст превращается в загружаемую систему с изолированными зависимостями, необходимо заглянуть под капот механизма сборки.

Точка входа: configuration.nix

Центральным узлом управления традиционной NixOS является файл /etc/nixos/configuration.nix. Это не shell-скрипт, который пошагово выполняется при загрузке, а единый чертеж желаемого состояния системы.

Базовая структура этого файла выглядит так:

{ config, pkgs, ... }:
{
  imports = [ ./hardware-configuration.nix ];

  environment.systemPackages = with pkgs; [ git vim ];

  services.openssh.enable = true;
}

Этот код представляет собой функцию. Она принимает на вход набор пакетов (pkgs) и текущую конфигурацию (config), а возвращает ассоциативный массив (набор атрибутов), описывающий, что должно присутствовать в системе.

Директива imports позволяет дробить монолитный чертеж на логические блоки. Например, hardware-configuration.nix генерируется автоматически при установке и описывает файловые системы и параметры ядра для конкретного железа. Разделение конфигурации на модули делает систему читаемой и легко переносимой на другие машины.

Четыре этапа рождения поколения

Когда вы вносите изменения в конфигурацию и выполняете команду nixos-rebuild switch, запускается строгий конвейер трансформации текста в рабочую среду. Этот процесс делится на четыре изолированных этапа.

1. Вычисление (Evaluation)

На этом этапе пакетный менеджер Nix читает ваш configuration.nix и объединяет его с гигантским репозиторием рецептов — Nixpkgs. Nixpkgs содержит инструкции по сборке десятков тысяч программ и сервисов.

Парсер языка Nix вычисляет итоговое дерево конфигурации. Он проверяет, существуют ли запрошенные пакеты, разрешает конфликты опций и формирует полный граф зависимостей. На этом этапе ничего не скачивается и не компилируется — происходит только работа с логикой и текстом.

2. Инстанцирование (Instantiation)

Вычисленный граф зависимостей транслируется во внутренний язык пакетного менеджера — деривации.

Деривация (файл с расширением .drv) — это низкоуровневая, машинно-читаемая инструкция по сборке конкретного компонента системы. Она содержит точные пути ко всем зависимостям в /nix/store, переменные окружения и скрипт сборки.

Деривации — это эквивалент ассемблера в мире Nix. Пользователь пишет высокоуровневый код в configuration.nix, а система генерирует сотни .drv файлов. Каждая деривация описывает чистую функцию сборки, результат которой строго детерминирован.

Фундаментальный принцип формирования хеша для деривации можно выразить следующей зависимостью:

Hout=Hash(Ssrc+∑Hdep+Cenv)H_{out} = \text{Hash}(S_{src} + \sum H_{dep} + C_{env})

Где:

  • HoutH_{out} — итоговый криптографический хеш, который станет частью пути в /nix/store.
  • SsrcS_{src} — исходный код или бинарные файлы собираемой программы.
  • ∑Hdep\sum H_{dep} — сумма хешей всех зависимостей (библиотек, компиляторов), необходимых для сборки.
  • CenvC_{env} — конфигурация сборочного окружения (переменные среды, флаги компилятора).

Любое, даже микроскопическое изменение в исходном коде, обновлении библиотеки-зависимости или флаге компиляции неминуемо изменит HoutH_{out}. Это гарантирует, что система никогда не перезапишет существующий пакет незаметно.

3. Реализация (Realization)

Nix-демон берет сгенерированные .drv файлы и начинает их выполнять. Для каждой деривации он проверяет: есть ли уже директория с таким хешем в локальном /nix/store?

  • Если есть, сборка пропускается (кэширование).
  • Если нет, Nix обращается к бинарным кэшам в интернете. Если кто-то (например, официальная сборочная ферма NixOS) уже выполнил эту деривацию, готовый результат скачивается напрямую.
  • Если кэша нет, Nix запускает скрипт сборки в полностью изолированной песочнице (sandbox), лишенной доступа к сети и нестандартным путям, чтобы гарантировать воспроизводимость.

Результатом реализации является появление готовых бинарных файлов, конфигурационных файлов и библиотек в /nix/store.

4. Активация (Activation)

Когда все компоненты реализованы, система готова к переключению. Создается новое поколение (Generation) — профиль системы, объединяющий все новые пути из /nix/store.

Запускается скрипт активации нового поколения. Он выполняет атомарное переключение симлинков (о которых мы говорили ранее), обновляет загрузчик (GRUB или systemd-boot), чтобы новую систему можно было выбрать при старте, и дает команду менеджеру служб systemd перезапустить только те сервисы, чья конфигурация или бинарные файлы изменились.

Жизненный цикл одного изменения: Добавление Nginx

Рассмотрим весь конвейер на конкретном примере. Вы решили превратить вашу рабочую станцию в локальный веб-сервер для тестирования и добавили в configuration.nix строку: services.nginx.enable = true;.

Этап Что происходит под капотом
Вычисление Nix находит модуль Nginx в Nixpkgs. Модуль содержит логику: если enable = true, нужно добавить пакет nginx в систему, создать пользователя nginx и сгенерировать nginx.conf.
Инстанцирование Создаются деривации: одна для компиляции самого Nginx (если его хеш изменился), другая для генерации файла nginx.conf, третья для unit-файла systemd.
Реализация Nix скачивает бинарный Nginx из официального кэша в /nix/store/7x8y...-nginx-1.24.0/. Текстовый файл конфигурации генерируется локально и тоже кладется в хранилище как /nix/store/a1b2...-nginx.conf.
Активация Скрипт активации создает пользователя nginx, обновляет симлинк /etc/systemd/system/nginx.service на новый путь в хранилище и выполняет systemctl daemon-reload и systemctl start nginx.

Вы не писали bash-скриптов для создания пользователя, не копировали конфигурационные файлы в /etc/nginx/ и не управляли службами вручную. Вы лишь задекларировали намерение, а строгий четырехэтапный конвейер обеспечил его безопасное воплощение.

Чтобы этот конвейер работал безотказно и позволял описывать сколь угодно сложные инфраструктуры, NixOS использует собственный язык программирования. Именно его синтаксис и типы данных определяют, насколько гибко вы сможете управлять своей рабочей станцией.

Язык Nix: Синтаксис и типы данных для написания конфигов

Язык Nix: Синтаксис и типы данных для написания конфигов

В основе NixOS лежит парадокс: мы описываем систему декларативно, как статичный чертеж, но используем для этого полноценный язык программирования. Файл configuration.nix — это не YAML и не JSON. Это программа. На этапе вычисления (Evaluation) парсер не просто читает текст, он исполняет код, который в конечном итоге возвращает гигантскую структуру данных — описание того, как должна выглядеть ваша рабочая станция.

Чтобы уверенно управлять системой, нужно перестать воспринимать конфигурацию как магический набор параметров и начать читать её как код. Для этого необходимо освоить язык Nix.

Зачем конфигурации язык программирования?

Традиционные форматы конфигурации (JSON, TOML, YAML) отлично подходят для хранения статических данных. Но когда конфигурация охватывает целую операционную систему, возникают сложные задачи: нужно переиспользовать значения, писать условную логику («если включен Wayland, добавь такие-то пакеты») и генерировать настройки динамически.

Язык Nix был создан специально для решения этих задач. Он обладает тремя ключевыми свойствами:

  1. Функциональный: В Nix нет последовательности команд или циклов for/while. Всё является выражением, которое возвращает значение.
  2. Чистый (Pure): Результат выполнения кода зависит только от входных данных. Язык не может случайно прочитать файл из вашей домашней директории или сделать запрос в интернет во время вычисления (если это не задано строгим криптографическим хешем).
  3. Ленивый (Lazy): Код выполняется только тогда, когда его результат действительно нужен. Если вы описали сложную сборку пакета, но нигде не добавили его в систему, Nix даже не будет тратить время на парсинг его зависимостей.

Nix — это предметно-ориентированный язык программирования (DSL), единственная цель которого — создание дериваций и структур данных для их описания.

Базовые типы данных

Синтаксис Nix минималистичен и во многом напоминает JSON с «суперсилами». Начнем с примитивов.

Строки, числа и логические значения

Здесь всё стандартно, за исключением того, что Nix поддерживает многострочные строки с сохранением отступов.

"Обычная строка"
''
  Многострочная строка.
  Удобно для написания конфигов внутри конфигов
  (например, для Nginx или bash-скриптов).
''
42          # Целое число
1.5         # Число с плавающей точкой
true        # Логическое значение (true / false)

Пути (Paths) — уникальная особенность Nix

В большинстве языков путь к файлу — это просто строка. В Nix пути выделены в отдельный тип данных.

/etc/nixos/configuration.nix  # Это путь (без кавычек)
"/etc/nixos/configuration.nix" # А это строка

Разница фундаментальна. Если вы используете тип «путь» при описании пакета, Nix автоматически скопирует этот файл в /nix/store на этапе вычисления, чтобы гарантировать чистоту и неизменяемость сборки, и подставит вместо него новый путь из хранилища.

Интерполяция строк

Чтобы вставить значение переменной или результат вычисления внутрь строки, используется синтаксис ${выражение}.

"Путь к конфигурации: ${/etc/nixos/configuration.nix}"

Структуры данных: Списки и Наборы атрибутов

Для группировки данных Nix использует две основные структуры. Именно из них строится вся конфигурация системы.

Списки (Lists)

Список в Nix — это упорядоченный набор элементов, заключенный в квадратные скобки. Важное отличие: элементы разделяются пробелами, а не запятыми.

[ "git" "vim" "htop" ] # Правильно
[ "git", "vim" ]       # Ошибка синтаксиса

Списки могут содержать элементы разных типов, хотя на практике в NixOS они чаще всего используются для перечисления пакетов или модулей.

Наборы атрибутов (Attribute Sets)

Набор атрибутов (AttrSet) — это словарь, ассоциативный массив или объект (если проводить аналогии с другими языками). Это пары «ключ-значение», заключенные в фигурные скобки.

Два главных правила AttrSet:

  1. Пары разделяются знаком равенства =.
  2. Каждое выражение обязательно заканчивается точкой с запятой ;. Пропущенная ; — самая частая причина ошибок у новичков.
{
  name = "workstation";
  cores = 8;
  isServer = false;
}

AttrSets могут быть вложенными. В Nix есть удобный синтаксический сахар для вложенных атрибутов — объединение через точку:

Такая запись:

{
  networking.hostName = "nixos";
}

Абсолютно эквивалентна такой:

{
  networking = {
    hostName = "nixos";
  };
}

Рекурсивные наборы (rec)

По умолчанию ключи внутри AttrSet не знают друг о друге. Если вы попытаетесь сослаться на соседний ключ, Nix выдаст ошибку. Чтобы разрешить атрибутам ссылаться друг на друга, перед блоком ставится ключевое слово rec (recursive).

Обычный AttrSet (Ошибка) Рекурсивный AttrSet (Работает)
{<br>  name = "app";<br>  path = "/bin/" + name; (ошибка, name не найден)<br>} rec {<br>  name = "app";<br>  path = "/bin/" + name;<br>}

Локальные переменные: let ... in

Чтобы не дублировать код и сделать его читаемым, в Nix используется конструкция let ... in. Она позволяет объявить временные переменные, которые будут существовать только внутри следующего за in выражения.

let
  version = "1.2.0";
  url = "https://example.com/app-${version}.tar.gz";
in
  "Скачиваем исходники из: ${url}"

Всё, что объявлено в блоке let, автоматически становится доступным друг другу (блок работает рекурсивно, как rec).

Функции — сердце языка

В функциональном языке всё строится вокруг функций. Синтаксис функции в Nix предельно лаконичен: аргумент: тело_функции. Никаких return, никаких скобок для аргументов.

x: x + 1

Это анонимная функция, которая принимает x и возвращает x + 1.

Вызов функции происходит через пробел:

let
  addOne = x: x + 1;
in
  addOne 5  # Результат: 6

Деструктуризация аргументов

Чаще всего функции в Nix принимают не одно значение, а целый Набор атрибутов. Чтобы не писать «вытащи из аргумента ключ А и ключ Б», Nix позволяет сразу «распаковать» AttrSet в аргументах. Это называется деструктуризацией.

{ a, b }: a + b

Эта функция ожидает на вход AttrSet строго с ключами a и b. Если передать ей { a = 1; b = 2; c = 3; }, она выдаст ошибку, так как не знает, что делать с c.

Чтобы функция принимала указанные ключи, но игнорировала любые другие (неизвестные), используется многоточие ...:

{ a, b, ... }: a + b

Деконструкция configuration.nix

Теперь, зная синтаксис, давайте посмотрим на типичный файл configuration.nix не как на список настроек, а как на код.

{ config, pkgs, ... }:

{
  imports = [ ./hardware-configuration.nix ];

  networking.hostName = "nixos";

  environment.systemPackages = [
    pkgs.git
    pkgs.vim
  ];
}

Что здесь происходит на самом деле?

  1. Первая строка { config, pkgs, ... }: — это объявление функции. Весь ваш конфигурационный файл — это одна большая функция, которая принимает на вход AttrSet (содержащий ключи config, pkgs и другие) и обещает вернуть результат.
  2. Всё, что идет после двоеточия (со второй строки и до конца) — это тело функции.
  3. Тело функции представляет собой один большой Набор атрибутов (AttrSet), начинающийся с { и заканчивающийся }.
  4. Внутри этого набора мы определяем ключи (например, networking.hostName) и присваиваем им значения (строку "nixos").
  5. В environment.systemPackages мы передаем список (в квадратных скобках [ ]). Элементы списка — pkgs.git и pkgs.vim — это обращение к вложенным атрибутам git и vim внутри объекта pkgs, который наша функция получила в качестве аргумента на первой строке.

Когда вы запускаете nixos-rebuild, система берет эту функцию, передает ей гигантский словарь со всеми доступными пакетами Nixpkgs (в аргумент pkgs), получает обратно ваш AttrSet с настройками, и уже на его основе генерирует деривации для сборки поколения.

Понимание того, что конфигурация — это функция, возвращающая словарь, открывает путь к созданию сложных, модульных и надежных рабочих станций.