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.