сеньорчикОткрыть в Telegram
← вся теориятеория к собесу

Cargo: зависимости, версии и сборка

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, остальные разбираются в тренажёре.

  1. #rs_cargo1 / 5
    Почему фичи крейта должны быть аддитивными?
    A)Иначе крейт не пройдёт проверку при публикации на crates.io
    B)Иначе фичи не включить из командной строки
    C)Так требует система типов: фичи меняют сигнатуры функций
    D)Cargo объединяет фичи всех потребителей в одну сборку
    показать ответ и разбор
    +D)Cargo объединяет фичи всех потребителей в одну сборку

    // разбор: Если два ваших зависимых крейта просят разные фичи одной библиотеки, она соберётся один раз с их объединением. Поэтому фича обязана что-то добавлять, а не менять поведение: взаимоисключающие флаги вроде sync и async ломают сборку у того, кому досталась чужая комбинация. Отсюда и приём с default-features = false.

  2. #rs_cargo2 / 5
    Что даёт workspace из нескольких крейтов?
    A)Общий target, единый lock и сборку одной командой
    B)Слияние крейтов в один бинарник при сборке в release
    C)Возможность обращаться к приватным элементам соседних крейтов
    D)Раздельные версии одной зависимости для каждого крейта
    показать ответ и разбор
    +A)Общий target, единый lock и сборку одной командой

    // разбор: Крейты в рабочем пространстве делят target, Cargo.lock и версии зависимостей, а cargo test прогоняет их разом. Это стандартный способ разрезать большой проект на слои — домен, хранилище, транспорт — и держать сборку быстрой. Приватность при этом остаётся крейтовой: соседи видят только pub.

  3. #rs_cargo3 / 5
    Сборка стала занимать восемь минут. Что обычно даёт эффект первым?
    A)Отключение инкрементальной сборки
    B)Разрез на крейты и чистка тяжёлых зависимостей
    C)Увеличение числа codegen-units до максимума
    D)Переход на opt-level = 0 в профиле release
    показать ответ и разбор
    +B)Разрез на крейты и чистка тяжёлых зависимостей

    // разбор: Время съедают мономорфизация дженериков, процедурные макросы и один жирный крейт, который пересобирается целиком на каждую правку. Разрез на крейты даёт параллельность и точечную пересборку, а cargo tree и cargo-udeps показывают, что тянется зря. Крутить профиль release смысла нет: разработка идёт в debug.

  4. #rs_cargo4 / 5
    Библиотека выходит в версии 1.4.0. Какое изменение сделает релиз ломающим по семверу?
    A)Добавление нового публичного типа в крейт
    B)Добавление варианта в enum, помеченный non_exhaustive
    C)Метод в публичном трейте без реализации по умолчанию
    D)Добавление новой фичи, выключенной по умолчанию
    показать ответ и разбор
    +C)Метод в публичном трейте без реализации по умолчанию

    // разбор: Новый метод без умолчания ломает все внешние реализации трейта — их авторам придётся дописывать код, то есть это мажорное изменение. Новый тип или фича ничего не ломают, а вариант в non_exhaustive enum безопасен ровно потому, что пользователи обязаны держать ветку по умолчанию. Такие тонкости проверяет cargo-semver-checks.

  5. #rs_cargo5 / 5
    Крейт помечен #![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 при заданном глобальном аллокаторе. Так и живут прошивки и ядра: платформа дала кучу — коллекции вернулись.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.