Мій основний комп’ютер працює на NixOS. Це Framework Desktop з процесором AMD AI Max 300-ї серії, на якому запущено Wayland-композитор niri та оболонка noctalia. Конфігурація складається з двадцяти одного .nix файлу (всього 26 файлів у репозиторії), а один білд створює одночасно і систему, і користувацьке середовище.
Список пакетів — найнудніша частина будь-якого конфігу. Я ж хочу зосередитися на внутрішній структурі, оскільки три ключові рішення визначають усе інше. Мені не потрібно вручну оновлювати список імпортів. Десктопні програми налаштовуються через створення окремих пакетів, а не через dot-файли. І лише один файл на 39 рядків керує кольорами всього інтерфейсу.
Кожне з цих рішень має свої нюанси, які я виявив на власному досвіді. Спочатку розберемо саму конфігурацію, а потім — де саме вона може «вкусити».
Флейк — це лише заготовка
Ось повний flake.nix. Лише вхідні дані (inputs) та один рядок вихідних (outputs).
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
import-tree.url = "github:vic/import-tree";
wrapper-modules.url = "github:BirdeeHub/nix-wrapper-modules";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
stylix = {
url = "github:nix-community/stylix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; }
(inputs.import-tree ./modules);
}
Останній рядок робить усю магію. flake-parts забезпечує модульну систему для самого flake, а import-tree рекурсивно передає йому кожен .nix файл із директорії ./modules. Додаєте новий файл у дерево — і він автоматично підключається. Немає потреби синхронізувати список imports = [ ... ], що зазвичай є найнуднішою частиною підтримки Nix-конфігурації.
Варто чітко окреслити компроміси, оскільки вони не очевидні відразу:
- Кожен файл у
modules/має бути модулем flake-parts (рівніflake.*,perSystem,configтощо). Якщо покласти туди звичайний NixOS-модуль, евалюація завершиться помилкою, яка не вкаже на реальну причину. import-treeігнорує файли, назва яких починається з підкреслення. Це зручно:_scratch.nixможна тримати в репозиторії без активації.- Файли без розширення
.nixповністю ігноруються, тому випадковий JSON-файл ні на що не вплине.
Через перше правило два елементи живуть поза межами modules/. Файл palette.nix лежить у корені, бо це звичайний attrset, а не модуль. Директорія users/ містить модулі Home Manager, які також не є модулями flake-parts, тому вони імпортуються за шляхом. Це має свою ціну: файли в users/ доводиться прописувати вручну.
Модулі — це значення, а не файли
У цій конфігурації нічого не імпортується за прямим шляхом до файлу (окрім згаданого вище винятку). Натомість кожен файл визначає іменований output.
flake.nixosModules.stylixTheme = { pkgs, lib, ... }: { /* ... */ };
Інші файли використовують його за іменем через self:
imports = [
self.nixosModules.frameworkHardware
self.nixosModules.niri
self.nixosModules.stylixTheme
self.nixosModules.devServices
];
Повний ланцюжок від виходу flake до стилізованого додатка виглядає так:
flake.nixosConfigurations.framework (hosts/framework/default.nix)
├─ nixos-hardware framework-desktop-amd-ai-max-300-series
├─ self.nixosModules.frameworkConfiguration (hosts/framework/configuration.nix)
│ ├─ self.nixosModules.frameworkHardware (hosts/framework/hardware.nix)
│ ├─ self.nixosModules.niri (features/niri.nix)
│ ├─ self.nixosModules.stylixTheme (features/stylix.nix)
│ └─ self.nixosModules.devServices (features/devservices.nix)
└─ home-manager.nixosModules.home-manager
└─ users."steve" = users/steve.nix
└─ users/features/*.nix (вказані вручну)
self та inputs передаються через specialArgs та extraSpecialArgs, тому модулі NixOS та Home Manager мають доступ до виходів flake:
flake.nixosConfigurations.framework = inputs.nixpkgs.lib.nixosSystem {
specialArgs = { inherit self inputs; };
modules = [
# ...
{
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
backupFileExtension = "hm-bak";
extraSpecialArgs = { inherit self inputs; };
users."steve" = import ../../../users/steve.nix;
};
}
];
};
Home Manager працює як NixOS-модуль, а не автономно, тому nixos-rebuild switch — єдина команда, яка мені потрібна. Системні та користувацькі налаштування активуються синхронно.
Рядок із backupFileExtension важливіший, ніж здається. Home Manager відмовляється перезаписувати файли, які він не створював. Встановлення розширення для бекапу дозволяє перейменувати старий файл, щоб активація пройшла успішно.
Два контексти обчислення
Це концепція, яка пояснює більшість незрозумілих помилок у flake-parts. Її варто засвоїти якомога раніше.
Існує два окремі світи:
flake.*— обчислюються один раз для всього flake (наприклад,nixosConfigurationsтаnixosModules).perSystem— обчислюються окремо для кожної архітектури (x86_64-linuxтощо), кожна зі своїмpkgs. Це стосуєтьсяpackages,devShellsтаformatter.
modules/parts.nix оголошує системи (у моєму випадку — чотири), навіть якщо використовується лише одна:
{
config.systems = [
"x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"
];
}
Головна пастка: perSystem не бачить конфігурацію NixOS. Це зовсім інший процес обчислення з власним pkgs і без доступу до config.*. Це має два наслідки.
По-перше, пропрієтарні пакети. Я встановлюю nixpkgs.config.allowUnfree = true всередині модуля NixOS, але це не діє на pkgs, які бачить perSystem. Якщо звернутися до pkgs.vscode всередині perSystem, обчислення перерветься. Саме тому гарячі клавіші в моєму конфігу викликають програми через PATH, а не через пряме посилання на пакет:
# Свідомо не lib.getExe pkgs.vscode
"Mod+C".spawn-sh = "code";
Це також дозволяє не тримати 400 МБ редактора в closure композитора, що пришвидшує роботу.
По-друге, кольори тем. config.lib.stylix.colors так само недоступний із perSystem. Саме тому моя палітра — це звичайний файл, який можна імпортувати, а не значення, що зчитується зі Stylix під час обчислення.
Загальне правило: щоб поділитися чимось через цю межу, це має бути просте значення у файлі, який обидві сторони можуть зробити import.
Програми — це пакети, а не dot-файли
Ні niri, ні noctalia не налаштовуються через опції Home Manager або ручні конфіги. Натомість perSystem створює попередньо налаштований wrapper за допомогою wrapper-modules:
perSystem = { pkgs, lib, self', ... }: {
packages.myNiri = inputs.wrapper-modules.wrappers.niri.wrap {
inherit pkgs;
settings = {
spawn-at-startup = [ [ (lib.getExe self'.packages.myNoctalia) ] ];
layout.gaps = 5;
binds = {
"Mod+Return".spawn-sh = lib.getExe pkgs.ghostty;
"Mod+Q".close-window = { };
# ...
};
};
};
};
Модуль NixOS просто встановлює цей пакет:
flake.nixosModules.niri = { pkgs, ... }: {
programs.niri = {
enable = true;
package = self.packages.${pkgs.stdenv.hostPlatform.system}.myNiri;
};
};
Уся конфігурація композитора тепер знаходиться всередині derivation. Змінили гарячу клавішу — отримали новий store path. У ~/.config нічого не пишеться, тому конфігурація нікуди не «пливе».
Де ця модель дає збій
Я хочу бути відвертим щодо проблем: деякі програми орієнтовані на GUI. Їхні панелі налаштувань редагують конфіг-файл, а не просто накладають зміни. Noctalia — одна з них. За замовчуванням wrapper вказує змінній середовища на конфіг у Nix store, який доступний лише для читання. Усі зміни, які я робив у самій програмі, тихо не зберігалися, тоді як справжній файл у ~/.config ігнорувався.
Ще гірше те, що самі налаштування просто відкидалися. Noctalia завантажує конфіг через QML JsonAdapter, який ігнорує невідомі ключі без жодних повідомлень у логах. Ручний блок Nix із ключами на кшталт bar.height створював оболонку з дефолтними параметрами, і жодна система не казала мені, чому.
Два висновки, які корисні не лише для цієї програми:
- Перевіряйте ключі за реальною схемою програми. У моєму випадку істина ховалася в
$out/share/noctalia-shell/Assets/settings-default.json. - Перевіряйте, чи програма розраховує сама записувати свій конфіг. Якщо так, то шлях у store — погане рішення. Wrapper-modules має механізм
outOfStoreConfig, але це компроміс, на який треба йти свідомо.
Декларативний шар конфігурації та програма, що сама зберігає свій стан, завжди конфліктуватимуть, і програма зазвичай перемагає мовчки.
Один файл для всіх кольорів
palette.nix — це звичайний attrset формату base16 (Catppuccin Mocha), який ні від чого не залежить:
{
base00 = "1e1e2e"; # base: фон за замовчуванням
base01 = "181825"; # mantle: світліший фон
# ...
base0D = "89b4fa"; # blue: функції, заголовки
base0E = "cba6f7"; # mauve: ключові слова
}
Stylix використовує ці дані та стилізує близько 130 додатків: GTK, Qt, VS Code, ghostty, fuzzel, bat, GRUB та інші. Шрифти налаштовуються так само. Жодна програма в цьому конфігу не має власних налаштувань кольору.
Редагування одного файлу перефарбовує весь десктоп після наступного перезбірання.
Але тут є дві пастки з перевизначенням (override). Таргети Stylix використовують mkDefault, тому будь-яке явне налаштування у вашому конфігу мовчки переможе, і здаватиметься, що Stylix нічого не робить. Якщо ви самі прописали font-family або workbench.colorTheme — ось чому тема не застосовується.
Структура та конвенції
flake.nix inputs + один рядок outputs
palette.nix кольори base16, не модуль
modules/
parts.nix список систем
hosts/framework/
default.nix nixosConfigurations.framework
configuration.nix системний конфіг
hardware.nix генерований файл, UUID та модулі ядра
features/
niri.nix пакет композитора + NixOS-модуль
noctalia.nix пакет оболонки
stylix.nix стилізація
devservices.nix бази даних у контейнерах
users/
steve.nix індекс користувача, ідентичність + імпорти
features/ файли за напрямками
cli.nix desktop.nix dev.nix direnv.nix firefox.nix
git.nix shell.nix ssh.nix terminal.nix vscode.nix
Поділ іде за областю дії (scope), а не за темою. Усе, що стосується користувача, лежить у users/features/: пакети, аліаси, скрипти в ~/.local/bin. Системні налаштування — у configuration.nix.
Це працює завдяки тому, що home.packages — це список, а home.shellAliases — attrset. Кожен файл може додавати до них свої значення, і вони об'єднуються без конфліктів.
Важливі нюанси
Flakes бачать лише файли, що відстежуються в git. Якщо ви створили .nix файл, але не зробили git add, білд його не побачить, а помилка виглядатиме як відсутність опції. Також тримайте .gitignore максимально лаконічним.
Не запускайте nix flake update через sudo. Це оновлює flake.lock, який належить вашому користувачу. Якщо зробити це від імені root, файл змінить власника, і наступні спроби оновлення без привілеїв видаватимуть помилку доступу.
Фіксуйте образи контейнерів за digest, а не за тегом. oci-containers підтягує образ лише тоді, коли його немає локально. Тег :18 зафіксує версію, яка була актуальною на момент першого запуску. Digest робить це відтворюваним:
image = "docker.io/library/postgres:18@sha256:3a82e1f56c8f0f5616a11103ac...";
Цикл розробки
Мій цикл перевірки складається з двох або трьох команд:
nix flake check # перевірка всіх виходів
sudo nixos-rebuild build --flake .#framework # збірка без активації
sudo nixos-rebuild switch --flake .#framework # застосування
Для роботи над десктопом збірка одного пакета значно швидша за повний ребілд системи:
nix build .#myNiri
nix build .#myNoctalia
Це головна перевага підходу з обгорнутими пакетами (wrapped packages). Зміна гарячої клавіші — це збірка пакета, а не активація всієї системи. Ви можете перевірити згенерований конфіг прямо в store:
nix build .#myNoctalia --no-link --print-out-paths
Що дає такий дизайн
- Одна команда:
rebuildстворює і систему, і середовище користувача. Ніяких окремихhome-manager switch. - Автоматизація: Ніякої бухгалтерії з імпортами в
modules/. Новий файл — нова функція. - Контроль: Десктоп-конфігурація адресована за контентом (content-addressed). Жодних мутабельних файлів, що можуть змінитися з часом.
- Єдність: Один файл для всієї палітри кольорів.
Яка ціна
- Контекстна межа: Треба постійно пам'ятати про розділення між flake та
perSystem. Помилки тут не вказують на справжню причину. - Усе або нічого: Автоімпорт вимагає, щоб кожен файл був модулем певного типу.
- Проблеми з GUI-програмами: Усе, що хоче самостійно записувати свій стан, конфліктує з Nix.
- Тихі відмови: Це найскладніше. Stylix, JSON-адаптери або не додані в git файли не видають помилок — вони просто створюють систему, де ваші налаштування ігноруються.
Головна звичка, яку варто виробити: перевіряйте згенерований артефакт, а не тільки вхідні дані, які ви передали.