Язык Nix сам по себе довольно простой, его можно характеризовать как чисто функциональный json на стероидах. Но что такое чистые функции и стероиды у json-а?
(Чистая) Функциональность – Нет принципов ООП или ПП. Функции работают по принципу чистого λ-исчисления: 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
144Attrsets – 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
]Остальные типы вам хорошо знакомы: 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. Они смотрят именно на сигнатуру паттерн матчинга.
stdenv.mkDerivation – функция из дефолтного пекедж сета nixpkgs, использующая только примитивы. Она предоставляет обширный инструментарий, пробежимся по СОВСЕМ базовому:
pname и version – вместе формируют name, можно использовать его. Нужно для того, чтобы nix мог идентифицировать пакет.src – путь к сурсам. функции типа fetchgit автоматом качают его в стор и возвращают путь. О сторе позже.Фазы. Вот это, пожалуй, самое интересное
phases, когда он опущен, никс обычно справляется с тем, чтобы вызвать нужные.$src (путь к сурсам) и $out (директория, где будет выход пакета).nativeBuildInputs и buildInputs
nativeBuildInputs доступны в PATH итд при сборке НА ХОСТЕ.buildInputs активнее продвигают косвенные пути, в т.ч. для рантайма. Либы, которые надо прилинковать – сюда./nix/store. В ней хранятся директории с названиями <хеш>-<name>. Это – выходные пути пакетов./nix/store/aaa...-hello-99.99/ будет иметь у себя в зависимостях /nix/store/bbb...-ncurses-vvv/Вот мы написали функцию. А как собрать пакет?
Legacy метод. Быстро, не очень репродуктивно (каналы). Создаём default.nix:
let ourHelloPackage = { stdenv, ... }: ...; # Код выше
in { pkgs ? import <nixpkgs> {} }: # Функция с дефолтным аргументом. <nixpkgs> -- системный канал nixpkgs.
pkgs.callPackage ourHelloPackage {} # {} -- доп аргументы, нам не нужны.Запускаем nix-build и видим ./result – симлинку на путь в сторе.
Флейки. Репродуцируются засчёт 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. (Да и вообще, вики невероятно полезное.)
callPackage смотрит в его сигнатуру, чтобы дать вам часть сета, а не весь.Функцию пакета можно (и нужно) выносить в отдельный файл и писать callPackage ./package.nix {}: Файл package.nix:
{ stdenv, ... }: ... # Тот самый кодКиллер-фича callPackage – оверрайд
ALREADYCALLEDHELLO.override { ncurses = NOTncurses; }
# Такая штука не должна собираться. Но суть в том, что любой аргумент функции, прокинутой через callPackage, можно вот так легко заменить.Это был очень краткий и скомканный экскурс в мир функциональных пакетов. Я считаю, что упомянул важные моменты, а дальше вы свободны гуглить и использовать Nix(OS), как вам вздумается.