cargo build долго обновляет индекс crates.io: sparse-протокол
Строка 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.retry | CARGO_NET_RETRY | 3 | сколько раз повторять запрос при сетевых ошибках, похожих на временные |
| http.timeout | CARGO_HTTP_TIMEOUT | 30 | таймаут одного HTTP-запроса в секундах |
| http.low-speed-limit | CARGO_HTTP_LOW_SPEED_LIMIT | 10 | если средняя скорость ниже этого числа байт в секунду дольше http.timeout, соединение считается слишком медленным и повторяется |
| http.multiplexing | CARGO_HTTP_MULTIPLEXING | true | HTTP/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 --offlinecargo 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/.