Nix. Hacking your first package.
Intro
Киллер-фича Nix это воспроизводимое развертывание окружения. Одной командой:
$ nix develop
После этого мы получаем ровно такой же шел в котором и разрабатывался пакет. Это очень сильно упрощает жизнь т.к. пропадает головная боль с затаскиванием зависимостей и настройкой инфраструктуры. Иногда глянешь исходники на гитхабе и вроде сразу понятно что исправить нужно, но потом посмотришь на конфигурацию CI/DI, чтобы прикинуть как девелоперское окружение развернуть для запуска тестов, офигеешь, закроешь вкладку и забьешь. И мир чуточку лучше не стал. Т.е. проблема в довольно высоком начальном пороге входа. Nix же снижает его почти до нуля. И это очень сильно упрощает эксперименты, если для подготовки нужно выполнить одну команду, то почему бы не попробовать? А патчить будем как ни странно сам Nix.
Problem
Nix не понимает пути в которых есть @
.
$ nix eval --expr '/path/p@a'
error: syntax error, unexpected '@', expecting end of file
at «string»:1:8:
1| /path/p@
| ^
Хотя этот символ вполне допустим в пути ФС. Например это мешает развернуть Home Manager на машине с доменной авторизацией. Это не бага, это всего лишь излишне строгая проверка, а значит ее всегда можно отломать.
Prepare environment
Ссылка инструкцию находиться прямо в README.md проекта на гитхабе. Клонируем репу, заходим в эту директорию и поднимаем шелл для разработки.
$ git clone https://github.com/NixOS/nix.git
$ cd nix
$ nix develop .#native-clangStdenvPackages
Окружение сборки на Clang выбрано потому что там есть преднастроенный clangd
. Посмотреть доступные окружения можно командной nix flake show
, нужный нам раздел devShells
. Теперь запускаем сборку с мониторингом.
$ make clean && bear -- make -j$NIX_BUILD_CORES install
Bear эта такая штука, которая мониторит команды запуска компилятора и генерирует compile_commands.json
. Этот файлик подхватывается clangd
выступающим в роли lsp-сервера.
Теперь нам остается только запустить свою любимую IDE и попросить ее использовать в качестве lsp-сервера clangd
, который она найдет первым в переменной окружения PATH
. IDE при этом может быть любая, лишь бы умела в LSP. Ну или хотя бы смогла понять compile_commands.json
. На этом шаге мы получаем проект развернутый в IDE с нормальной навигацией по исходникам (goto to definition/declaration).
Hack hack hack
Поиск в исходниках текста сообщения об ошибке ничего не дал, оно генерируется динамически, придется трассировать. По идее, если происходит ошибка, то скорее всего будет выброшено исключение. Добавляем в конфигурацию отладчика (gdb) команду catch throw
чтобы он останавливался в точке выброса исключения и несколько раз нажимаем F5 пока не увидим в локальных переменных что-то похожее на наш путь с @
.
И тут мы понимаем что вляпались в формальную грамматику и отправляемся читать документацию на Bison.
неожиданно, правда?)
В парсере определен токен path
, он может состоять из нескольких лексем PATH
, HPATH
, SPATH
, PATH_END
:
%type <e> start expr expr_function expr_if expr_op
%type <e> expr_select expr_simple expr_app
%type <list> expr_list
%type <attrs> binds
%type <formals> formals
%type <formal> formal
%type <attrNames> attrs attrpath
%type <string_parts> string_parts_interpolated
%type <ind_string_parts> ind_string_parts
%type <e> path_start string_parts string_attr
%type <id> attr
%token <id> ID
%token <str> STR IND_STR
%token <n> INT
%token <nf> FLOAT
%token <path> PATH HPATH SPATH PATH_END
%token <uri> URI
%token IF THEN ELSE ASSERT WITH LET IN REC INHERIT EQ NEQ AND OR IMPL OR_KW
%token DOLLAR_CURLY /* == ${ */
%token IND_STRING_OPEN IND_STRING_CLOSE
%token ELLIPSIS
Определение лексем предсказуемо находится в лексере. Здесь определена лексема PATH
, которая может состоять из лексем PATH_CHAR
, лексема же PATH_CHAR
определена как множество конкретных символов:
ANY .|\n
ID [a-zA-Z\_][a-zA-Z0-9\_\'\-]*
INT [0-9]+
FLOAT (([1-9][0-9]*\.[0-9]*)|(0?\.[0-9]+))([Ee][+-]?[0-9]+)?
PATH_CHAR [a-zA-Z0-9\.\_\-\+]
PATH {PATH_CHAR}*(\/{PATH_CHAR}+)+\/?
PATH_SEG {PATH_CHAR}*\/
HPATH \~(\/{PATH_CHAR}+)+\/?
HPATH_START \~\/
SPATH \<{PATH_CHAR}+(\/{PATH_CHAR}+)*\>
URI [a-zA-Z][a-zA-Z0-9\+\-\.]*\:[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~\*\']+
Ну что же, давайте попробуем добавить туда нашу собачку @
diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l
index a3a8608d9..b57a01cde 100644
--- a/src/libexpr/lexer.l
+++ b/src/libexpr/lexer.l
@@ -114,7 +114,7 @@ ANY .|\n
ID [a-zA-Z\_][a-zA-Z0-9\_\'\-]*
INT [0-9]+
FLOAT (([1-9][0-9]*\.[0-9]*)|(0?\.[0-9]+))([Ee][+-]?[0-9]+)?
-PATH_CHAR [a-zA-Z0-9\.\_\-\+]
+PATH_CHAR [a-zA-Z0-9\.\_\-\+\@]
PATH {PATH_CHAR}*(\/{PATH_CHAR}+)+\/?
PATH_SEG {PATH_CHAR}*\/
HPATH \~(\/{PATH_CHAR}+)+\/?
Собираем, проверяем
$ make install
$ outputs/out/bin/nix eval --expr '/path/p@a'
/path/p@a
Win!
Deploy
Патч традиционно получаем при помощи git diff > enable_at_in_path.patch
, теперь его нужно прикрутить к конфигурации, я это сделал через overlay
nixOverlay = final: prev:
{
nix = prev.nix.overrideAttrs (old: {
patches = (old.patches or []) ++ [
./enable_at_in_path.patch
];
});
};
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [
nixOverlay
];
};
Retro
Вот так довольно простыми и воспроизводимыми действиями получилось внести нужное изменение и прикрутить его к своей системе. После множества мучений с установкой зависимостей выглядит это как "а что, так можно было?".