Cargo: зависимости, версии и сборка
Инструментальный блок, который любят на собесах в командах с большим кодом. Спрашивают про lock-файл, семвер, фичи и время сборки, то есть про всё, что болит на реальном проекте.
Стержень: Cargo.toml описывает намерения, Cargo.lock фиксирует факт, а фичи и границы крейтов определяют, как долго вы будете ждать сборку.
// Формулировки: «зачем lock, если версии в toml?», «что значит serde = "1.2"?», «сборка стала 8 минут - что делаешь?»
Версии и lock
Запись "1.2" означает ^1.2: обновления внутри мажорной версии подтягиваются, переход на 2.0 - нет. Точную версию задают через =1.2.0, ветку - через ~1.2. Что именно выбрано из диапазона, видно в Cargo.lock.
Lock фиксирует версии всего дерева, включая транзитивные зависимости, благодаря этому сборка воспроизводима у всех и в CI. Для приложений его коммитят обязательно; для библиотеки он есть, но на пользователей не влияет: у них своё дерево и свой lock.
// Отсюда практика: обновление зависимостей - отдельный коммит с прогоном тестов, а не побочный эффект чужой сборки.
- caret-версия
- ^1.2: обновления в пределах мажорной версии
- Cargo.lock
- фиксация точных версий всего дерева
Фичи обязаны быть аддитивными
Если два ваших зависимых крейта просят у одной библиотеки разные фичи, она соберётся один раз - с их объединением. Поэтому фича должна что-то добавлять, а не менять поведение: взаимоисключающие флаги вроде sync и async ломают сборку у того, кому досталась чужая комбинация.
Отсюда же приём default-features = false: берём только нужное, чтобы не тянуть в бинарник половину экосистемы. Особенно заметно на крупных крейтах вроде tokio, где полный набор фич тянет и таймеры, и файловую систему, и процессы.
// Проверять реальный граф удобно через cargo tree: он показывает, кто и зачем притащил зависимость и какие фичи включены.
- аддитивность фич
- фича добавляет возможности, не меняя поведение
- default-features
- набор фич по умолчанию; часто отключают ради веса
Время сборки
Время съедают три вещи: мономорфизация дженериков, процедурные макросы и монокрейт, который пересобирается целиком на каждую правку. Workspace с несколькими крейтами даёт параллельность и точечную пересборку это обычно первый и самый эффективный шаг.
Дальше смотрят зависимости: cargo tree покажет дубли версий, cargo-udeps - неиспользуемые крейты. Тяжёлые derive-макросы в горячих модулях тоже стоят времени.
// А вот крутить профиль release ради скорости разработки бессмысленно: разработка идёт в debug, и там помогают cargo check и rust-analyzer, которые останавливаются до кодогенерации.
- workspace
- несколько крейтов с общим target и lock
- cargo check
- проверка типов без генерации машинного кода
Как отвечать: «Зачем нужен Cargo.lock, если версии указаны в Cargo.toml?»
В toml обычно стоит диапазон вроде "1.2", то есть допускаются любые обновления внутри мажорной версии. Lock записывает, какие именно версии были выбраны, включая транзитивные зависимости, и благодаря этому сборка воспроизводится одинаково у всех разработчиков и в CI. Для приложений его коммитят в репозиторий обязательно, иначе можно получить сюрприз от чужого минорного релиза. Для библиотеки он тоже существует, но на пользователей не влияет: у них своё дерево зависимостей и свой lock.
Ты объясняешь разницу «намерение против факта» и знаешь правило про приложения и библиотеки - на этом отличии часто и ловят.
На чём валятся
- −Не коммитят lock в приложении и получают разные версии у разработчиков и в CI.
- −Делают фичи взаимоисключающими и ломают сборку тем, кто тянет обе.
- −Думают, что "1.2" это ровно 1.2.0.
- −Держат монокрейт и жалуются на время сборки.
- −Крутят профиль release, надеясь ускорить цикл разработки.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 14, остальные разбираются в тренажёре.
- Почему фичи крейта должны быть аддитивными?A)Иначе крейт не пройдёт проверку при публикации на crates.ioB)Иначе фичи не включить из командной строкиC)Так требует система типов: фичи меняют сигнатуры функцийD)Cargo объединяет фичи всех потребителей в одну сборку
показать ответ и разбор
+D)Cargo объединяет фичи всех потребителей в одну сборку// разбор: Если два ваших зависимых крейта просят разные фичи одной библиотеки, она соберётся один раз с их объединением. Поэтому фича обязана что-то добавлять, а не менять поведение: взаимоисключающие флаги вроде sync и async ломают сборку у того, кому досталась чужая комбинация. Отсюда и приём с default-features = false.
- Что даёт workspace из нескольких крейтов?A)Общий target, единый lock и сборку одной командойB)Слияние крейтов в один бинарник при сборке в releaseC)Возможность обращаться к приватным элементам соседних крейтовD)Раздельные версии одной зависимости для каждого крейта
показать ответ и разбор
+A)Общий target, единый lock и сборку одной командой// разбор: Крейты в рабочем пространстве делят target, Cargo.lock и версии зависимостей, а cargo test прогоняет их разом. Это стандартный способ разрезать большой проект на слои — домен, хранилище, транспорт — и держать сборку быстрой. Приватность при этом остаётся крейтовой: соседи видят только pub.
- Сборка стала занимать восемь минут. Что обычно даёт эффект первым?A)Отключение инкрементальной сборкиB)Разрез на крейты и чистка тяжёлых зависимостейC)Увеличение числа codegen-units до максимумаD)Переход на opt-level = 0 в профиле release
показать ответ и разбор
+B)Разрез на крейты и чистка тяжёлых зависимостей// разбор: Время съедают мономорфизация дженериков, процедурные макросы и один жирный крейт, который пересобирается целиком на каждую правку. Разрез на крейты даёт параллельность и точечную пересборку, а cargo tree и cargo-udeps показывают, что тянется зря. Крутить профиль release смысла нет: разработка идёт в debug.
- Библиотека выходит в версии 1.4.0. Какое изменение сделает релиз ломающим по семверу?A)Добавление нового публичного типа в крейтB)Добавление варианта в enum, помеченный non_exhaustiveC)Метод в публичном трейте без реализации по умолчаниюD)Добавление новой фичи, выключенной по умолчанию
показать ответ и разбор
+C)Метод в публичном трейте без реализации по умолчанию// разбор: Новый метод без умолчания ломает все внешние реализации трейта — их авторам придётся дописывать код, то есть это мажорное изменение. Новый тип или фича ничего не ломают, а вариант в non_exhaustive enum безопасен ровно потому, что пользователи обязаны держать ветку по умолчанию. Такие тонкости проверяет cargo-semver-checks.
- Крейт помечен #![no_std]. Откуда в нём тогда берутся Vec и String?A)Из core: там лежит вся коллекционная часть стандартной библиотекиB)Из крейта alloc, который подключают отдельноC)Ниоткуда: под no_std остаются массивы фиксированной длиныD)Их приносит аллокатор, указанный через #[global_allocator]
показать ответ и разбор
+B)Из крейта alloc, который подключают отдельно// разбор: Стандартная библиотека расслоена на три части: core — без кучи и без ОС, alloc — всё, что требует аллокаций (Vec, String, Box, Rc), std — остальное плюс операционная система. Под no_std доступен core, а alloc подключается явным extern crate alloc при заданном глобальном аллокаторе. Так и живут прошивки и ядра: платформа дала кучу — коллекции вернулись.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.