cargo build долго обновляет индекс crates.io: sparse-протокол

, 7 мин чтения

Строка Updating crates.io index знакома каждому, кто собирает проекты на Rust. Обычно она проскакивает за секунду, но иногда висит минутами, а потом сборка падает с spurious network error. Разберём, как Cargo получает индекс и зависимости, и какие настройки делают загрузку быстрой и предсказуемой.

Как Cargo получает индекс

Индекс crates.io хранит метаданные всех версий всех пакетов: зависимости, фичи, контрольные суммы. Раньше Cargo клонировал его целиком как git-репозиторий с GitHub. Первый клон занимал заметное время, а потом индекс приходилось регулярно подтягивать, даже если вам нужна была одна новая зависимость.

В Rust 1.68 появился sparse-протокол, а начиная с Rust 1.70 он используется для crates.io по умолчанию. В этом режиме Cargo запрашивает по HTTPS с https://index.crates.io/ только файлы тех пакетов, которые участвуют в разрешении зависимостей, и кэширует их. Архивы самих пакетов с расширением .crate скачиваются отдельно, с static.crates.io.

Проверьте версию командой cargo --version. Если она старше 1.70, первым делом обновите тулчейн: rustup update stable. Переход на sparse не требует правок в проекте: Cargo.toml и Cargo.lock остаются прежними, меняется только способ, которым Cargo получает метаданные.

Включён ли sparse у вас

Даже на свежем Cargo протокол мог остаться переопределённым в конфигурации. Cargo читает настройки из нескольких мест: .cargo/config.toml в каталоге проекта и во всех родительских каталогах, затем ~/.cargo/config.toml, а поверх них переменные окружения CARGO_*. Проверьте все места:

env | grep CARGO_
cat ~/.cargo/config.toml
cat .cargo/config.toml

Если где-то стоит protocol = "git", уберите строку или явно укажите sparse:

[registries.crates-io]
protocol = "sparse"

То же самое задаётся переменной окружения, это удобно в CI: CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse. Косвенный признак правильного режима: каталог индекса в ~/.cargo/registry/index/ называется с префиксом index.crates.io-, а не github.com-.

Повторы и таймауты

Временные сбои Cargo повторяет сам. В выводе это выглядит примерно так:

warning: spurious network error (2 tries remaining): [28] Timeout was reached

Само по себе такое предупреждение не страшно: Cargo повторил запрос, и если следующая попытка прошла, сборка продолжится. Плохо, когда попытки заканчиваются и сборка падает. По умолчанию повторов три, а один HTTP-запрос ждёт до 30 секунд. На нестабильном канале увеличьте оба значения в ~/.cargo/config.toml:

[net]
retry = 10

[http]
timeout = 60

В CI удобнее задавать те же значения переменными окружения, не трогая файлы: CARGO_NET_RETRY=10 CARGO_HTTP_TIMEOUT=60 cargo build. Основные сетевые параметры:

КлючПеременнаяПо умолчаниюСмысл
net.retryCARGO_NET_RETRY3сколько раз повторять запрос при сетевых ошибках, похожих на временные
http.timeoutCARGO_HTTP_TIMEOUT30таймаут одного HTTP-запроса в секундах
http.low-speed-limitCARGO_HTTP_LOW_SPEED_LIMIT10если средняя скорость ниже этого числа байт в секунду дольше http.timeout, соединение считается слишком медленным и повторяется
http.multiplexingCARGO_HTTP_MULTIPLEXINGtrueHTTP/2 с мультиплексированием, значение false переключает на HTTP/1.1

По умолчанию Cargo отправляет много запросов через одно HTTP/2-соединение. Обычно это быстрее, но если между вами и интернетом стоит прокси или антивирус, который проверяет HTTPS, такое соединение иногда зависает. Попробуйте CARGO_HTTP_MULTIPLEXING=false cargo fetch. Если с HTTP/1.1 загрузка идёт ровно, закрепите multiplexing = false в секции [http].

Прокси Cargo берёт из своей настройки http.proxy, из http.proxy в глобальном конфиге git или из переменных HTTPS_PROXY и http_proxy. Если запросы уходят не туда, проверьте все три места.

Git-зависимости и git-fetch-with-cli

Зависимости вида { git = "..." } Cargo по умолчанию скачивает встроенной библиотекой git. У неё есть ограничения: она не читает ~/.ssh/config, для SSH требует запущенный ssh-agent с добавленным ключом и не понимает короткую запись адреса вида git@example.com:user/repo.git. Используйте полную форму ssh://git@example.com/user/repo.git.

Если вам нужны настройки из ~/.ssh/config, например другой порт или ключ, или помощник учётных данных, переключите Cargo на системный git:

[net]
git-fetch-with-cli = true

Или разово: CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build. Тогда загрузку выполняет обычная команда git, и на неё действуют все ваши настройки git и SSH.

Где Cargo хранит кэш

Всё скачанное лежит в ~/.cargo, если не задана переменная CARGO_HOME:

КаталогЧто внутри
registry/index/локальная копия индекса или его фрагментов
registry/cache/скачанные архивы пакетов .crate
registry/src/распакованные исходники, которые читает компилятор
git/db/голые клоны git-зависимостей
git/checkouts/рабочие копии нужных коммитов

Внутреннюю структуру каталога Cargo официально не фиксирует, но удалять отсюда любую часть безопасно: Cargo восстановит недостающее, распаковав архив или скачав его заново. Свежие версии Cargo сами удаляют давно неиспользуемые файлы: скачанные из сети через три месяца без обращений, а те, что можно восстановить локально, через месяц. Частоту проверки задаёт [cache] auto-clean-frequency, значение "never" отключает автоочистку.

Для CI документация Cargo советует кэшировать не весь ~/.cargo, а только registry/index/, registry/cache/ и git/db/. Если нужны программы из cargo install, добавьте bin/ и файлы .crates.toml и .crates2.json. Иначе исходники попадут в кэш дважды: архивом и в распакованном виде.

Скачать заранее и собирать без сети

Если сеть то появляется, то пропадает, разделите загрузку и сборку:

cargo fetch
cargo build --offline

cargo fetch скачивает все зависимости из Cargo.lock. После этого команды Cargo работают без сети, пока не изменится lock-файл. Флаг --target ограничивает загрузку зависимостями для нужной платформы.

Тот же приём ускоряет сборку в Docker. Скопируйте сначала только Cargo.toml и Cargo.lock и запустите cargo fetch отдельным слоем, а исходники копируйте следующим шагом. Пока зависимости не меняются, Docker берёт слой с ними из кэша и не скачивает их заново при каждой правке кода. Учтите, что Cargo для разбора манифеста нужен хотя бы один целевой файл, поэтому на этом шаге обычно создают пустой src/main.rs или src/lib.rs.

Режимы сборки:

  • --offline запрещает Cargo обращаться к сети. Если чего-то нет в кэше, будет ошибка, а при разрешении новых зависимостей Cargo ограничится уже скачанными версиями.
  • --locked требует, чтобы Cargo.lock существовал и не менялся. Подходит для CI.
  • --frozen равносилен --locked и --offline одновременно.

Для полностью автономной сборки, например на сервере без доступа в интернет, используйте cargo vendor. Команда копирует исходники всех зависимостей в каталог vendor/ и печатает фрагмент конфигурации, который нужно положить в .cargo/config.toml проекта:

[source.crates-io]
replace-with = "vendored-sources"

[source.vendored-sources]
directory = "vendor"

Тот же механизм замены источника работает с корпоративным зеркалом crates.io: вместо directory указывается registry = "sparse+https://адрес-зеркала/". Точный адрес берите у администраторов зеркала.

Частые ошибки и что они значат

СообщениеЧто происходитЧто делать
spurious network errorвременный сетевой сбой, Cargo повторяет запросувеличить net.retry и http.timeout
failed to download from https://static.crates.io/...не скачался архив пакетапроверить доступность static.crates.io, повторить cargo fetch
failed to fetch https://github.com/rust-lang/crates.io-indexиспользуется старый git-протокол индексавключить sparse, обновить Rust
failed to authenticate when downloading repositoryне прошла авторизация для git-зависимостиssh-add, полный адрес ssh://, net.git-fetch-with-cli = true
waiting for file lock on package cacheкэш занят другим процессом Cargo, часто фоновой проверкой в редакторедождаться завершения или остановить второй процесс
failed to select a version for the requirement, в режиме --offlineв локальном кэше нет подходящей версии пакетаодин раз выполнить cargo fetch с доступом к сети

Последнюю ошибку Cargo в офлайн-режиме сопровождает подсказкой, что этот режим иногда приводит к неожиданным ошибкам разрешения зависимостей. Обычно это значит, что нужной версии нет среди скачанных архивов, хотя индекс о ней может знать. Если ожидание файла кэша длится долго, проверьте, не идёт ли параллельно cargo check из редактора или вторая сборка в соседнем терминале.

Как найти медленное место

Включите подробный журнал сетевых запросов:

CARGO_HTTP_DEBUG=true CARGO_LOG=network=debug cargo fetch

Так видно, какой адрес отвечает медленно: индекс или архивы пакетов. Время соединения можно сравнить через curl:

curl -o /dev/null -s -w 'connect %{time_connect}  tls %{time_appconnect}  total %{time_total}\n' https://index.crates.io/config.json

Если соединение устанавливается долго, а сам Cargo настроен правильно, причина в маршруте до серверов crates.io. В этом случае помогает защищённое подключение через ближайшую к ним локацию.

Коротко

  • Обновите Rust до 1.70 или новее: sparse-протокол включится по умолчанию.
  • Уберите protocol = "git" из конфигов и проверьте CARGO_REGISTRIES_CRATES_IO_PROTOCOL.
  • На нестабильной сети увеличьте net.retry и http.timeout.
  • При зависаниях через прокси попробуйте CARGO_HTTP_MULTIPLEXING=false.
  • Для приватных git-зависимостей включите net.git-fetch-with-cli.
  • Скачивайте зависимости заранее через cargo fetch и собирайте с --offline или --frozen.
  • Для сборки без интернета используйте cargo vendor.
  • В CI кэшируйте registry/index/, registry/cache/ и git/db/.

Стабильный канал для работы

Один ключ, 20 стран, до 5 устройств на Standard. Возврат в течение 7 дней.

Все тарифы