// заметки
Nix devShell в fish: PATH молча не подхватывается
Пакеты из Nix devShell не подхватываются в fish: PATH указывает внутрь mount-namespace. Диагностика молчаливой подмены и прозрачный re-exec оболочки
Версия jq неожиданно старая, хотя flake (описание окружения в Nix) собирается без ошибок. Дело не в сборке: PATH указывает на пути, которых нет вне mount-namespace (изолированного дерева точек монтирования), и оболочка берёт системные бинарники. Ниже: как это заметить, почему так происходит и как один раз перезапустить fish внутри namespace
Контекст: rootless Nix, direnv, nix-direnv и fish в проекте с .envrc, где есть use flake (запуск окружения из flake)
Симптом
Ошибки нет, и это сбивает с толку. direnv allow завершается успешно, переменные из .env подхватываются, но инструменты берутся не из devShell (окружения разработки, которое собирает Nix)
$ echo $APP_ENV
development # dotenv работает
$ command -s jq
/usr/bin/jq # системный, а не из devShellПри этом PATH выглядит правильным: нужные записи на месте и стоят первыми
$ string split ":" $PATH | head -3
/nix/store/<hash>-git-2.55.0/bin
/nix/store/<hash>-jq-1.8.2-bin/bin
/nix/store/<hash>-curl-8.22.0-dev/binДиагностика
Первым делом проверьте, откуда оболочка реально берёт бинарь
$ command -s jq
/usr/bin/jqЕсли команда находится в /usr/bin, а не в /nix/store, часть записей PATH мертва. Второй тест показывает, внутри ли вы namespace
$ test -d /nix/store; and echo внутри; or echo снаружи
снаружиЭто единственный надёжный маркер. Переменной $NIX_STORE_DIR доверять нельзя: её можно выставить и снаружи
Когда команда падает и не называет виновника, помогает перехват бинаря: Кто вызывает unshare
Причина
Rootless Nix монтирует /nix внутри mount-namespace, изолированного дерева точек монтирования, которое поднимается без прав root. Снаружи namespace каталога /nix нет вообще
$ ls -d /nix/store/<hash>-jq-1.8.2-bin
ls: невозможно получить доступ: Нет такого файла или каталога
$ nix-enter ls -d /nix/store/<hash>-jq-1.8.2-bin
/nix/store/<hash>-jq-1.8.2-binОболочка не считает отсутствующий каталог ошибкой: она пропускает мёртвую запись PATH и берёт следующее совпадение. Поэтому подмена и остаётся незамеченной
У динамически слинкованных бинарников зашит интерпретатор /nix/store/<glibc>/lib/ld-linux-x86-64.so.2, так что переписать PATH на ~/.nix/store/... не поможет
$ ~/.nix/store/<hash>-jq-1.8.2-bin/bin/jq --version
bash: .../jq: cannot execute: required file not foundЕсли direnv падает с явной ошибкой про unshare, это отдельный сценарий: nix-direnv: «unshare failed: Операция не позволена»
А почему запуск direnv снаружи ломает вшитые пути пакета, объясняется в другой заметке: nix-direnv: «minimum required bash version is 4.4»
Решение
Сборку flake недостаточно держать внутри namespace: сама оболочка тоже должна работать там. Добавьте в ~/.config/fish/config.fish
if status is-interactive
and not set -q __NIX_NS_REEXEC
and test -x "$HOME/.local/bin/nix-enter"
and test -d "$HOME/.nix/store"
and not test -d /nix/store
set -gx __NIX_NS_REEXEC 1
exec "$HOME/.local/bin/nix-enter" /usr/bin/fish -i -C "set -gx __NIX_NS_REEXEC 1"
endЧто здесь происходит. Если /nix/store ещё не смонтирован, оболочка один раз перезапускает себя (re-exec, повторный запуск) через обёртку nix-enter и передаёт флаг __NIX_NS_REEXEC. Дальше fish работает внутри namespace, где пути /nix/store валидны. Имя nix-enter это пример обёртки, у вас она может называться иначе, важна сама проверка
Проверка $HOME/.nix/store предполагает типовую раскладку rootless Nix. Если store у вас в другом каталоге, поправьте путь в условии
Четыре условия в if закрывают четыре разных сценария
status is-interactiveне ломает неинтерактивные скрипты неожиданным перезапускомnot set -q __NIX_NS_REEXECзащищает от бесконечного циклаtest -d "$HOME/.nix/store"проверяет, что Nix вообще установленnot test -d /nix/storeпроверяет, что мы ещё снаружи namespace
После перезапуска бинарь берётся из devShell
$ command -s jq
/nix/store/<hash>-jq-1.8.2-bin/bin/jqПроверка
Запустите три проверки ниже. Норма: мёртвых записей PATH нет, command -s jq указывает на /nix/store, а test -d /nix/store печатает «внутри»
# 1) мёртвые записи PATH
for p in (string split ":" $PATH)
test -n "$p"; or continue
test -d $p; or echo "МЁРТВЫЙ: $p"
end
# 2) откуда реально берётся бинарь
command -s jq
type -a jq
# 3) внутри ли namespace
test -d /nix/store; and echo внутри; or echo снаружиЕсли первый цикл печатает МЁРТВЫЙ для путей /nix/store, а command -s jq возвращает системный бинарь, оболочка работает снаружи namespace
Подводные камни
- Самая опасная поломка та, что не выдаёт ошибок: версия инструмента может молча оказаться системной. Если версия неожиданно старая, первым делом проверяйте
command -s - При неинтерактивных запусках, в скриптах и CI, re-exec не срабатывает: так задумано. Там оболочку нужно запускать внутри namespace заранее
Итог
Если у вас rootless Nix, direnv и fish, а пакеты из flake «не видны», PATH почти наверняка указывает внутрь namespace, тогда как оболочка работает снаружи. Прозрачный re-exec решает это и не ломает скрипты
Похожие заметки
nix-direnv: «unshare failed: Операция не позволена»
direnv с use flake падает на unshare failed, хотя dotenv работает. Разбираю причину (вложенный user-namespace) и показываю фикс обёртки над nix
Кто вызывает unshare: перехват бинаря через PATH
Команда падает и не называет виновника. Показываю приём: подменить бинарь логирующим скриптом через PATH и найти вызывающего без трассировки и прав root
nix-direnv: «minimum required bash version is 4.4»
nix-direnv падает с «minimum required bash version is 4.4», хотя bash новее. Разбираю вшитые пути /nix/store, почему текст врёт и как это исправить