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

// заметки

Nix rootless: сборка не стартует и store-хеш под GC

Локальная сборка падает с unable to start build process, а захардкоженный путь к nss-cacert удаляет nix-collect-garbage. Два фикса: sandbox и вычисление пути

Опубликовано: 2026-10-176 мин чтения
Nixrootlesssandboxgarbage collectionTLS

Локальная сборка в rootless Nix (установка без демона и без root, store в $HOME) падает с error: unable to start build process, хотя готовые бинарники из cache.nixos.org скачиваются без ошибок. Вторая поломка тише: путь к корневым сертификатам захардкожен и уже числится в списке мёртвых, так что первый nix-collect-garbage ломает TLS. Ниже обе причины, два фикса и чек-лист безопасной уборки store

Контекст: CachyOS, Nix 2.24.10, установка без демона, store в $HOME/.nix. Номера версий зависят от канала, дальше речь о схеме: rootless-инсталляция и обвязка вокруг неё

Симптом

Сборка не стартует, загрузка идёт

Из кеша приехал stdenv (базовое окружение сборки), но derivation (рецепт сборки пакета), которую нужно собрать локально, не стартовала

$ nix build .#default --no-link --print-out-paths
copying path '/nix/store/...-stdenv-linux-no-cc' from 'https://cache.nixos.org'...
error: unable to start build process

Substituters (подстановка готовых бинарей из кеша) при этом работают. Ломаются только те derivations, которые нужно собрать локально

Сертификаты в списке мёртвых

Путь к корневым сертификатам был прописан константой

# было: путь зашит константой
# CACERT=/nix/store/<hash>-nss-cacert-<version>/etc/ssl/certs/ca-bundle.crt
# export NIX_SSL_CERT_FILE="$CACERT"

nix-store --gc --print-dead показал этот путь среди мусора. Строка выглядит рабочей, пока конкретный nss-cacert жив. До поломки оставался один запуск nix-collect-garbage -d: после него сертификаты исчезли бы, а загрузки упали бы с невнятной TLS-ошибкой

Диагностика

Пять команд для диагностики

# режим песочницы
nix config show sandbox

# что GC планирует удалить, ничего не трогая
nix-store --gc --print-dead | head -30

# хардкоды store-путей в своей обвязке
grep -rn "/nix/store/[a-z0-9]\{32\}" ~/.local/bin/ ~/.config/direnv/ ~/.config/fish/

# целостность store и размер
nix-store --verify --check-contents
du -sh ~/.nix
  • nix config show sandbox возвращает true — значит Nix пытается изолировать сборку
  • --print-dead только печатает план: путь к nss-cacert в выводе означает, что его удалят
  • grep ищет 32-символьные store-хеши в ваших скриптах и конфигах direnv и fish
  • nix-store --verify --check-contents проверяет целостность store, если есть подозрение на повреждённые пути
  • du -sh ~/.nix полезен, чтобы понимать объём перед уборкой

Если --print-dead показывает пути из ваших скриптов, сначала замените их глобом или gcroot, и только потом запускайте уборку. Пустой вывод по grep означает, что зашитых хешей в обвязке нет

Причина

Сначала о сборке, затем о сертификатах: две нити идут параллельно и не пересекаются

Песочница требует вложенного user-namespace

Rootless-установка без демона работает в одном user-namespace, который поднимает обёртка вроде nix-enter (имя примерное). Для сборки Nix пытается создать второй, вложенный namespace, а вложенность в такой конфигурации запрещена. Один уровень работает, два — нет, поэтому sandbox = true сдаётся на старте. Установка и обёртка разобраны отдельно: Nix на CachyOS: установка невозможна — rootless-запуск

Substituters сборку не запускают: скачивание из кеша не требует песочницы. Отсюда простой признак — падает ровно то, что нужно собрать локально

Store-хеш — адрес конкретной сборки, а не константа

/nix/store/<hash>-<name> — это путь конкретного результата. Он живёт, пока на него ссылается профиль, gcroot или другая derivation. Захардкоженная строка сама путь не удерживает: он удалится, когда на него не останется ссылок профиля или gcroot

Решение

Сборка: sandbox = false и пустой build-users-group

# $HOME/.nix/etc/nix/nix.conf
sandbox = false
build-users-group =

build-users-group = оставлен пустым: группы nixbld в rootless-установке нет, сборка идёт от текущего пользователя. Предупреждение про отсутствующую группу сборщиков после этого пропадает

Цена решения: изоляции локальных сборок нет. Код из flake.nix выполняется с правами пользователя, поэтому недоверенные flake так собирать нельзя

Сертификаты: глоб вместо константы

CACERT=""
for _c in /nix/store/*-nss-cacert-*/etc/ssl/certs/ca-bundle.crt; do
    [ -r "$_c" ] && CACERT="$_c"
done
[ -n "$CACERT" ] || CACERT=/etc/ssl/certs/ca-certificates.crt
export NIX_SSL_CERT_FILE="$CACERT"

Цикл перебирает все nss-cacert в store и запоминает последний читаемый файл. Если ни одного не осталось, берётся системный bundle. В fish-конфиге логика та же: обход глоба, затем фолбэк. Кэшировать найденное значение в конфиге нельзя: при следующей уборке оно снова может стать мёртвым, а глоб выполняется при каждом старте оболочки и берёт актуальный путь

Для путей, которые должны жить долго, остаётся gcroot: отдельная ссылка, удерживающая store-путь от удаления. Она требует шага после каждой пересборки, зато не зависит от текущего содержимого store

Проверка

Проверяем три вещи: режим песочницы, что сборка доходит до конца и что сертификаты читаются

nix config show sandbox          # ожидаем false
nix build .#default --no-link --print-out-paths
test -r "$NIX_SSL_CERT_FILE" && echo "certs ok"

nix config show sandbox должен вернуть false, иначе локальная сборка снова упрётся в sandbox. У nix build флаг --no-link оставляет результат в store, не трогая профиль, а --print-out-paths печатает путь: если команда дошла до конца и вернула путь, сборка прошла. test -r проверяет, что NIX_SSL_CERT_FILE указывает на читаемый файл, а не на удалённый путь. После уборки путь не обязан совпадать с прежним: глоб подхватит другой экземпляр nss-cacert, а если его не осталось — системный bundle, поэтому test -r важнее сверки со старой строкой

Перед nix-collect-garbage -d учтите: -d удаляет старые поколения профиля, и откатиться к ним уже не получится. Сначала посмотрите план через nix-store --gc --print-dead и историю через nix profile history, а при сомнениях снимите копию: cp -a ~/.nix ~/.nix.bak. Копия занимает место, зато возвращает store и профиль к состоянию до уборки

Чек-лист после nix-collect-garbage -d

  • ls ~/.nix/store/*-nix-direnv-*/share/nix-direnv/direnvrc — если пусто, вернуть пакет через nix profile install nixpkgs#nix-direnv
  • test -r "$NIX_SSL_CERT_FILE" — сертификаты на месте, TLS не сломан
  • новый fish в проекте: echo $APP_ENV не пуст, jq --version показывает версию из store

Профиль после уборки ломать не страшно: обёртка, которая поднимает namespace, берёт Nix из store, а не из профиля. Что это за обёртка — в заметке Nix devShell в fish: PATH молча не подхватывается

Подводные камни

  • sandbox = true не мешает скачиванию: substituters работают при любой настройке песочницы, поэтому на неё указывает падение только локальных сборок
  • Хардкод store-хеша сам путь не удерживает: /nix/store/<hash>-<name> удалится, когда на него не останется ссылок профиля или gcroot. Либо глоб, либо gcroot
  • nix-collect-garbage -d необратим для старых поколений профиля: откат к удалённому поколению невозможен, поэтому план смотрят до запуска
  • Мёртвость пути проверяют до уборки: --print-dead показывает план удаления, не меняя store
  • Отключение песочницы расширяет доверие: локальные сборки недоверенного кода идут с вашими правами
  • Тот же класс ошибок — вшитые store-пути в обвязке nix-direnv: nix-direnv: «minimum required bash version is 4.4»

Итог

Две поломки выглядят разными, но растут из одного: rootless-установка живёт без демона и без прав на изоляцию. sandbox = false возвращает локальные сборки, глоб по store убирает зависимость от конкретного хеша, а nix-store --gc --print-dead перед уборкой показывает, что вы потеряете. Обёртка, которая поднимает namespace, разобрана отдельно: Nix на CachyOS: установка невозможна — rootless-запуск

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