Перейти к содержимому
← Все заметки

// заметки

Nix devShell в fish: PATH молча не подхватывается

Пакеты из Nix devShell не подхватываются в fish: PATH указывает внутрь mount-namespace. Диагностика молчаливой подмены и прозрачный re-exec оболочки

Опубликовано: 2026-10-083 мин чтения
Nixfishdirenvrootless NixLinux

Версия 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 решает это и не ломает скрипты

Нужно решить такую задачу на вашем проекте?