LEARN X · ЗА 16 МИН

Nix

Экспресс-тур по языку Nix: типы, строки, множества, функции, деривации, flakes и nix-shell — весь язык на одной странице с комментариями.

Nix — это и пакетный менеджер, и встроенный в него функциональный язык конфигурации. Язык маленький, чисто функциональный и ленивый: одни и те же входные данные всегда дают одинаковый результат, поэтому сборки воспроизводимы. Ниже — весь язык за 16 минут, плотно закомментированным кодом. Запускать выражения можно в REPL: nix repl.

Что такое Nix

Nix описывает как собрать пакет или окружение чистой функцией от входов. Файлы языка имеют расширение .nix и содержат ровно одно выражение.

# Это комментарий до конца строки.

/* Это
   многострочный
   комментарий. */

# Весь файл .nix — это ОДНО выражение, которое вычисляется в значение.
# Никаких операторов и «строк выполнения»: только вычисление выражений.
# Чистота: нет переменных окружения, времени, случайности — отсюда
# воспроизводимость сборок.
42  # файл, состоящий из этого, вычисляется в число 42

Базовые типы

# Целые числа
1 + 2          # => 3
7 / 2          # => 3   (целочисленное деление!)
7.0 / 2        # => 3.5 (число с плавающей точкой)

# Булевы значения и логика
true && false   # => false  (И)
true || false   # => true   (ИЛИ)
!true           # => false  (НЕ)
1 == 1          # => true
3 != 4          # => true

# null — отсутствие значения
null

# Пути — это отдельный тип (НЕ строка!)
./config.nix    # путь относительно текущего файла
/etc/hostname   # абсолютный путь
~/projects       # путь относительно домашней директории
<nixpkgs>       # путь из NIX_PATH (угловые скобки — особый синтаксис)

Строки

Строки бывают обычные (в двойных кавычках) и многострочные (в двух одинарных). Внутри работает интерполяция через ${...}.

"привет"                 # обычная строка
"a" + "b"                # => "ab"  (конкатенация плюсом)

# Интерполяция: ${выражение} подставляет значение в строку
let name = "мир"; in
"Привет, ${name}!"       # => "Привет, мир!"

# Экранирование: \" \\ \n работают как обычно
"кавычка: \" и таб:\tконец"

# Многострочная строка в двойных одинарных кавычках ''...''
# Удобно для конфигов: общий отступ автоматически срезается.
''
  server {
    listen 80;
    name ${name};   # интерполяция тут тоже работает
  }
''

# Внутри ''...'' буквальный ${ экранируется как ''${
''цена: ''${не интерполяция}''

Списки и множества

Список (list) — элементы через пробел в квадратных скобках. Множество атрибутов (attribute set) — пары ключ-значение в фигурных скобках.

# Список: элементы разделяются ПРОБЕЛАМИ, не запятыми!
[ 1 2 3 ]
[ "a" "b" (1 + 2) ]      # элементы-выражения берут в скобки
[ 1 2 ] ++ [ 3 4 ]       # => [ 1 2 3 4 ]  (++ конкатенирует списки)

# Attribute set (множество атрибутов) — аналог словаря/объекта
{
  name = "codechick";
  port = 8080;
  nested = { a = 1; b = 2; };   # вложенные множества
}

# Каждая пара ОБЯЗАТЕЛЬНО заканчивается точкой с запятой ;

# rec — рекурсивное множество: атрибуты видят друг друга
rec {
  width  = 10;
  height = 20;
  area   = width * height;   # => 200, ссылается на соседние ключи
}

Доступ к атрибутам

let
  cfg = { host = "localhost"; port = 5432; db = { name = "app"; }; };
in
[
  cfg.host          # => "localhost"  (доступ через точку)
  cfg.db.name       # => "app"        (вложенный доступ)
  cfg.timeout or 30 # => 30  (or — значение по умолчанию, если ключа нет)
  (cfg ? port)      # => true  (? проверяет наличие ключа)
]

Конструкция with вносит атрибуты множества в область видимости, а inherit копирует имена из окружения.

# with множество; — даёт прямой доступ к ключам без префикса
with { a = 1; b = 2; };
a + b               # => 3  (вместо set.a + set.b)

# inherit x; — сокращение для  x = x;
let
  port = 8080;
  host = "0.0.0.0";
in {
  inherit port host;     # то же, что  port = port; host = host;
  # inherit (set) a b; берёт a и b ИЗ множества set:
  # inherit (config) timeout;  =>  timeout = config.timeout;
}

Функции

Функция — это аргумент: тело. Несколько аргументов делаются каррированием (функция возвращает функцию).

# Функция одного аргумента: x — параметр, после : — тело
x: x + 1            # анонимная функция «прибавь единицу»

# Вызов: пробелом, без скобок
(x: x + 1) 10       # => 11

# Несколько аргументов = каррирование (функция возвращает функцию)
let add = a: b: a + b;
in add 3 4          # => 7  (add 3 вернёт функцию b: 3 + b)

# Аргумент-множество: деструктуризация ключей { a, b }:
let area = { width, height }: width * height;
in area { width = 4; height = 5; }   # => 20

# Значения по умолчанию (?) и сбор остатка (...)
let greet = { name, greeting ? "Привет", ... }:
      "${greeting}, ${name}!";
in greet { name = "Эрнест"; extra = 1; }   # => "Привет, Эрнест!"

Выражение let

let ... in ... вводит локальные привязки, видимые в теле после in.

let
  x = 10;
  y = 20;
  sum = x + y;        # привязки видят друг друга независимо от порядка
in
  sum * 2             # => 60

# let привязки ленивые: вычисляются только при использовании.
# Неиспользованная привязка с ошибкой НЕ упадёт.
let a = 1; b = throw "никогда"; in a   # => 1

Условия

# if/then/else — это ВЫРАЖЕНИЕ, всегда возвращает значение.
# Ветка else обязательна.
if 2 > 1 then "да" else "нет"     # => "да"

let n = 7;
in if n < 0 then "отрицательное"
   else if n == 0 then "ноль"
   else "положительное"          # => "положительное"

# assert проверяет условие перед вычислением выражения
let port = 8080;
in assert port > 0; "порт ${toString port}"

Импорты

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

# import путь — загружает и вычисляет файл
import ./config.nix            # вернёт значение из config.nix

# Если файл содержит функцию, её можно сразу вызвать:
import ./add.nix { a = 1; b = 2; }

# Импорт стандартной библиотеки nixpkgs.
# <nixpkgs> — путь из NIX_PATH; передаём пустые настройки { }.
let pkgs = import <nixpkgs> { };
in pkgs.hello                  # деривация пакета hello

# builtins — встроенные функции, доступны без импорта
builtins.length [ 1 2 3 ]      # => 3
builtins.toString 42           # => "42"

Деривации

Деривация (derivation) — это описание сборки: рецепт, который Nix превращает в результат в /nix/store. Напрямую derivation почти не пишут — используют обёртки из nixpkgs.

# Низкоуровневый примитив derivation (для понимания, в реальности редок):
derivation {
  name = "пример";
  system = "x86_64-linux";       # целевая платформа
  builder = "/bin/sh";           # чем собирать
  args = [ "-c" "echo привет > $out" ];  # $out — путь результата в сторе
}

# На практике используют stdenv.mkDerivation из nixpkgs:
let pkgs = import <nixpkgs> { };
in pkgs.stdenv.mkDerivation {
  pname = "myapp";
  version = "1.0";
  src = ./.;                     # исходники
  buildInputs = [ pkgs.gcc ];    # зависимости сборки
  buildPhase = "gcc -o myapp main.c";
  installPhase = "mkdir -p $out/bin && cp myapp $out/bin/";
}

Пакеты nixpkgs

nixpkgs — огромная коллекция пакетов и функций. Обычно его импортируют как pkgs.

let
  pkgs = import <nixpkgs> { };
in {
  # Готовые пакеты — это атрибуты множества pkgs
  python = pkgs.python311;
  git    = pkgs.git;

  # lib — библиотека полезных функций
  upper  = pkgs.lib.toUpper "nix";        # => "NIX"
  joined = pkgs.lib.concatStringsSep ", " [ "a" "b" ];  # => "a, b"

  # callPackage автоматически подставляет зависимости из pkgs
  # по именам аргументов функции пакета:
  myPkg = pkgs.callPackage ./mypkg.nix { };
  # если ./mypkg.nix = { gcc, zlib }: ...  — gcc и zlib возьмутся из pkgs
}

Flakes

Flake — современный способ упаковать проект: файл flake.nix с фиксированными входами (inputs) и выходами (outputs). Версии входов пишутся в flake.lock — отсюда полная воспроизводимость.

# flake.nix — это множество ровно с двумя ключами: inputs и outputs
{
  description = "Пример flake";

  # inputs — внешние зависимости (другие flake'и)
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
  };

  # outputs — ФУНКЦИЯ от входов, возвращает множество результатов
  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      # пакет по умолчанию: собирается через  nix build
      packages.${system}.default = pkgs.hello;

      # окружение разработки: входим через  nix develop
      devShells.${system}.default = pkgs.mkShell {
        buildInputs = [ pkgs.python311 pkgs.git ];
      };
    };
}

Окружения nix-shell

nix-shell создаёт временное окружение с нужными инструментами, не устанавливая их в систему. Описывается через mkShell.

# shell.nix — Nix автоматически подхватывает его при вызове  nix-shell
let
  pkgs = import <nixpkgs> { };
in
pkgs.mkShell {
  # пакеты, доступные внутри оболочки (в $PATH)
  buildInputs = [
    pkgs.python311
    pkgs.nodejs_20
    pkgs.postgresql
  ];

  # shellHook — команды, выполняемые при входе в оболочку
  shellHook = ''
    echo "Окружение готово: $(python --version)"
    export APP_ENV=dev
  '';
}
# Запуск:  nix-shell      — войти в оболочку
#          nix-shell --run "python app.py"   — выполнить и выйти

Готовое dev-окружение целиком

Соберём всё вместе: воспроизводимое окружение для проекта на Python с зависимостями и переменными.

let
  # Фиксируем nixpkgs (на практике версию пинят через flake.lock)
  pkgs = import <nixpkgs> { };

  # Список инструментов проекта одной привязкой
  tools = with pkgs; [
    python311
    python311Packages.pip
    python311Packages.flask
    git
    ripgrep
  ];
in
pkgs.mkShell {
  buildInputs = tools;

  # Переменные окружения как обычные атрибуты
  PROJECT = "codechick";

  shellHook = ''
    echo "== Dev-окружение ${"\${PROJECT}"} =="
    echo "Python: $(python --version)"
    echo "Запусти:  flask run"
  '';
}
# nix-shell поднимет ТОЧНО такое же окружение у любого разработчика —
# в этом вся суть воспроизводимости Nix.
Поддержать проект