Системы сборки для новичков: как быстро понять Cargo, зависимостии и feature flags
Разберём логику feature flags, workspace и воспроизводимость сборок на практике. Покажем, как читать ошибки Cargo и не утонуть в конфигурациях.
Содержание
Системы сборки для новичков: как быстро понять Cargo, зависимости и feature flags
В мире Rust почти всё начинается с Cargo: менеджера пакетов, системы сборки и инструмента для публикации. Для новичка Cargo может выглядеть как «магия»: то зависимости подтягиваются сами, то появляются feature’ы, то workspace внезапно меняет поведение сборки. На практике проблема обычно не в Rust, а в том, что новичок не выстроил ментальную модель: как Cargo выбирает зависимости, как он комбинирует feature’ы, почему сборка на одной машине отличается от другой.
Ниже — практический разбор логики сборки в Cargo, понятный на старте: как работают workspace, dependencies, feature flags, и как добиться воспроизводимости. Отдельно — как читать типичные ошибки Cargo и не утонуть в конфигурациях. В конце упомяну курс Cargo – для начинающих! как один из способов закрепить материал, если хочется систематизации.
Почему новички «тонут» именно в конфигурациях Cargo
Cargo кажется простым: cargo build, cargo test, cargo run. Но стоит зайти глубже, появляются термины:
- зависимости (
dependencies,dev-dependencies,build-dependencies); - версии и разрешения (
Cargo.lock, semver); - feature flags (включаем/выключаем куски кода);
- workspace (много пакетов в одном репозитории);
- флаги сборки (
--features,--all-features,--no-default-features); - профили и target’ы (
profile.*,--release, cross-compilation).
У проблемы две причины:
- Cargo — декларативный инструмент. Вы описываете намерение (какие зависимости и какие feature’ы), а Cargo оптимизирует что именно собрать.
- Сборка зависит от графа. Feature’ы «текут» вверх и вниз по дереву зависимостей, и итоговое множество feature’ов — результат объединения требований всех участников.
В этой статье мы построим модель, которая помогает предсказуемо понимать поведение Cargo без угадывания.
Базовая модель Cargo: граф зависимостей и манифесты
Что такое Cargo.toml на самом деле
Cargo.toml — это не только список зависимостей. Это декларация:
- имя пакета и его версия;
- какие зависимости нужны и с какими feature’ами;
- какие feature’ы определены у вашего пакета;
- как устроены workspace-компоновки;
- профили сборки (в том числе опции оптимизаций и отладочной информации).
У новичка часто одна мысль: «Cargo просто скачивает зависимости». Реальность: Cargo строит граф модулей сборки из:
- вашего пакета (
package); - ваших feature’ов (
[features]); - зависимостей и их feature’ов (указываются в
Cargo.tomlваших зависимостей); - workspace-структуры (если вы собираете не один пакет, а набор).
Роль Cargo.lock: воспроизводимость и «почему на другой машине иначе»
Главный маркер воспроизводимости — файл Cargo.lock.
- Для приложений (bin) обычно коммитят
Cargo.lockв репозиторий. - Для библиотек (lib) часто не коммитят, чтобы потребитель сам выбирал набор версий по своим constraints.
Что важно понять:
Cargo.lock фиксирует конкретные версии зависимостей и тем самым стабилизирует поведение сборки. Если Cargo.lock отсутствует (или не используется), Cargo будет резолвить версии заново, и вы можете получить другую комбинацию зависимостей.
Мини-проверка воспроизводимости:
# На машине разработчика
cargo build
# На другой машине (или в CI) с одинаковым репозиторием и lock-файлом
cargo build --locked
Флаг --locked заставляет Cargo использовать только версии, записанные в Cargo.lock. Если граф зависимостей уже изменился, Cargo сообщит об ошибке — это ровно то, что нужно для воспроизводимых сборок.
Типичная ошибка, с которой сталкиваются новички:
the lock file ... needs to be updated, but --locked was passed
Это не «сломалось». Это сигнал: либо нужно обновить lock-файл (и затем закоммитить), либо вы пытаетесь собрать с несовпадающим состоянием зависимостей.
Feature flags: как Cargo «сводит» наборы включений
Базовая механика feature flags
Feature flag в Cargo — это способ включать/выключать части функциональности на уровне зависимостей и вашего кода.
У вас может быть так:
[features]
default = ["serde"]
serde = ["dep:serde"]
В коде вы обычно используете cfg атрибуты:
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
#[cfg(feature = "serde")]
#[derive(Serialize, Deserialize)]
pub struct Payload {
pub value: String,
}
Но ключевое: feature’ы влияют на зависимости. В примере serde feature включает dep:serde — то есть включает саму зависимость.
Feature flags в зависимостях: включения распространяются
Самый частый источник путаницы: вы включили feature у своей зависимости — а теперь эффект проявился «в другом месте», потому что feature’ы агрегируются по графу.
Например, вы у себя включаете зависимость с feature’ами:
[dependencies]
my-lib = { version = "1.2", features = ["fast-mode"] }
Если my-lib внутри использует fast-mode и при этом включает другие feature’ы своих зависимостей — Cargo всё учтёт. В результате итоговый набор возможностей может стать шире, чем вы ожидали.
Правило мышления: итоговый граф feature’ов — объединение требований.
Если где-то в дереве включили feature X, то он будет включён в сборке, даже если другая часть считала, что он выключен.
Что делает default и чем опасно «всё включить»
По умолчанию Cargo включает default feature’ы зависимостей. Это удобно, но в сложных проектах приводит к «непонятному» поведению: вы добавили зависимость — и вместе с ней включились десятки функций и дополнительные зависимости.
Сигнал к дисциплине:
- для воспроизводимости и управляемости — явно контролируйте feature’ы;
- для диагностики — временно отключайте defaults.
Примеры команд:
# Включить только то, что вы явно указали
cargo build --no-default-features --features "serde"
# Для библиотеки (или диагностики) включить все default + дополнительные
cargo build --all-features
Подводный камень: --all-features часто включает несовместимые комбинации (разные ветки кода, несовпадающие ожидания). Поэтому как инструмент диагностики — да, как «нормальная сборка» для CI — не всегда.
Workspace: как устроено «собрать всё» и почему команды иногда ведут себя неожиданно
workspace как способ централизовать конфигурацию
workspace используется, чтобы объединить несколько пакетов в одном репозитории: библиотеки, примеры, интеграционные тесты, сервисы. При этом у каждого пакета остаётся свой Cargo.toml.
Пример корневого Cargo.toml workspace:
[workspace]
members = [
"crates/core",
"crates/api",
"crates/cli",
]
resolver = "2"
resolver = "2" — важная настройка, которая относится к разрешению feature’ов в зависимости. Она меняет поведение агрегации в некоторых сценариях, и в современных проектах её обычно включают, чтобы получить ожидаемую модель.
Как workspace влияет на сборку и тесты
Команда cargo build из корня workspace может:
- собрать пакет(ы) по умолчанию;
- либо потребовать явно указать
-p.
Новичок часто запускает cargo build в корне и считает, что собирается «всё». На практике Cargo по умолчанию может выбрать конкретный пакет, если workspace устроен иначе (например, если в корне есть свой package, а не только [workspace]).
Лучший способ не гадать:
- используйте
cargo metadataдля анализа; - явно указывайте пакет:
-p crate_name.
Например:
cargo test -p api
cargo build -p cli --release
Воспроизводимость в workspace: lock-файл общий или нет
Если у вас один репозиторий workspace, обычно создаётся один Cargo.lock в корне workspace (если вы его коммитите). Это хорошая практика для приложений, потому что все пакеты в одном репозитории фиксируют согласованный граф зависимостей.
Но в сложных проектах люди умудряются иметь несколько lock-файлов (например, если по ошибке запускают Cargo в разных директориях с разными root’ами). Тогда воспроизводимость ломается.
Практическая дисциплина:
- всегда запускайте команды из корня workspace;
- явно проверяйте, где лежит
Cargo.lock.
Как читать ошибки Cargo: быстрые паттерны и точные действия
Ошибка про версию: «кандидаты не найдены» или конфликты semver
Типовые сообщения:
- «failed to select a version for …»
- «versions that meet the requirements … were found, but … conflicts»
Логика: у вас конфликт ограничений версий между разными участниками графа. Решение обычно одно из:
- ослабить constraints (например, с
=1.2.3на^1.2); - выровнять версионные требования между зависимостями;
- использовать
[patch.crates-io]или[replace](в продвинутых кейсах).
Новичку сложно сразу понять, кто именно задаёт конфликт. В таких случаях помогает смотреть на дерево резолва, но часто достаточно cargo tree.
cargo tree -i some-crate
Параметр -i показывает «кто зависит от указанного», а -p можно использовать для прицельного анализа.
Ошибка про feature: «package … does not have these features»
Это классика. Например, вы написали:
[dependencies]
some-lib = { version = "1.0", features = ["unknown-feature"] }
Cargo честно говорит, что такого feature нет. Причины:
- feature переименовали;
- feature отключён из-за версии;
- feature является частью другого имени (иногда у crate могут быть похожие флаги).
Что делать:
- проверить документацию конкретной версии crate (а не README «последней» ветки);
- посмотреть
Cargo.tomlзависимого пакета (в локальном кеше или через источники); - временно собрать с
cargo update -p some-lib(только если вам действительно нужна новая версия).
Ошибка из-за lock-файла при CI: --locked и несоответствия
Если в CI вы запускаете сборку с --locked, а разработчик не закоммитил обновлённый Cargo.lock, вы получите ошибку, похожую на:
Cargo.lock needs to be updated but --locked was passed
Решение: обновить lock в локальной среде тем же способом, что в CI (обычно cargo build --release или cargo test), затем закоммитить Cargo.lock.
Важно: lock обновляется из-за изменения constraints или feature’ов. Частая причина: вы включили новый feature, а lock не обновили.
Практика: как собрать проект с контролем feature flags и workspace
Сценарий 1: вы управляете features в своём crate
Допустим, вы делаете библиотеку core с optional-функциональностью.
crates/core/Cargo.toml:
[package]
name = "core"
version = "0.1.0"
edition = "2021"
[features]
default = ["logging"]
logging = []
serde-support = ["dep:serde"]
[dependencies]
serde = { version = "1", optional = true }
Код:
#[cfg(feature = "logging")]
pub fn log_event() {
// ...
}
Сборка из workspace-корня:
cargo build -p core --no-default-features
cargo build -p core --features serde-support
Если тесты требуют serde-support, включите feature при запуске тестов:
cargo test -p core --features serde-support
Сценарий 2: вы контролируете features у зависимости
crates/api/Cargo.toml:
[dependencies]
core = { path = "../core" }
my-http = { version = "0.9", features = ["compression"] }
Теперь ваше API автоматически соберёт с сжатие. Но если вы хотите отключить compression на уровне всего проекта, вам потребуется не только --no-default-features, а ещё «переопределить» зависимость так, чтобы её feature’ы были отключены. Cargo не всегда умеет «обнулить» feature у зависимости командой на верхнем уровне; нужно явно указывать зависимость в вашем Cargo.toml с нужными feature’ами.
Отсюда правило: управление feature’ами зависимостей лучше делать явно в манифесте верхнего уровня, а не надеяться на флаги командной строки.
Воспроизводимость сборок: меньше сюрпризов в реальной жизни
Фиксируйте версии (и обновляйте осознанно)
- Коммитите
Cargo.lockдля приложений. - Используйте
--lockedв CI, чтобы ошибки проявлялись сразу. - Обновляйте зависимости централизованно и внятно: через
cargo update.
Пример дисциплины CI:
cargo test --locked
Если вы обновили версию зависимостей, вы ожидаете изменение lock-файла — значит, CI должен пройти после коммита.
Контролируйте feature matrix
Частая «мина»: вы тестируете проект в одном наборе feature’ов (например, по умолчанию), а в проде включаете другой. Cargo позволит вам собрать с разными feature комбинациями, но CI обычно это не покрывает.
Практический минимум:
- тест по умолчанию (
cargo test); - тест без default feature’ов (
cargo test --no-default-features); - тест с ключевым feature’ом(ами) (например,
--features serde-support).
Для больших проектов матрица растёт экспоненциально, но на старте важно закрыть хотя бы главные ветки.
Типичные ошибки начинающих (и как их избежать)
Ошибка 1: «workspace собирается как я ожидаю» — но нет
Решение: всегда смотрите, какой пакет(ы) выбран. Используйте:
cargo metadata --format-version 1 --no-deps
и/или запускайте с -p.
Ошибка 2: «Я выключил feature командой, но поведение не изменилось»
Причина почти всегда в том, что feature включён другой частью графа. Как проверить:
- используйте
cargo tree -f(в зависимости от версии Cargo и флагов); - смотрите feature selection через
cargo build -vv(подробный вывод иногда помогает увидеть, что именно Cargo решает).
Ошибка 3: «Разные машины собирают по-разному»
Причина: либо нет lock-файла, либо он не используется, либо различаются флаги (--features, --no-default-features, target’ы). Минимальный ответ — реплицируйте команды и фиксируйте lock.
Ошибка 4: «Я не понимаю, откуда берётся зависимость»
Проверьте:
cargo tree -i <crate_name>
И попробуйте найти в выводе, кто является источником. Обычно там находится конкретная зависимость, у которой включили feature или изменили constraints.
Упорядоченный путь новичка: как быстро разобраться без хаоса
Если вам нужно «быстро понять» Cargo именно как систему, а не как набор команд, полезен такой порядок:
- Осознайте различие между приложением и библиотекой: lock-файл, публикация, ответственность за версии.
- Научитесь читать манифесты:
dependencies,optional,features,default. - Понять feature propagation: итог — объединение требований графа.
- Workspace как контейнер: научитесь явно указывать
-pи не предполагать «собирается всё». - Воспроизводимость:
--lockedи зафиксированная матрица features. - Ошибки — как диагноз:
--locked→ mismatch lock; unknown-feature → несоответствие имён/версий; version conflicts → semver constraints и граф резолва.
И если вы хотите структурировать это обучение, довести до автоматизма работу с feature flags и видеть типовые паттерны на практике, хороший способ — курс Cargo – для начинающих!. Он обычно полезен тем, кому не хватает именно системного подхода и упражнений, а не «разрозненных советов».
Заключение
Cargo — не просто инструмент сборки, а система принятия решений: резолв зависимостей, агрегация feature flags и организация workspace-команд. Новички часто «тонут» не из-за сложности Rust, а из-за отсутствия модели: где именно решается включение features, почему lock-файл влияет на воспроизводимость, и почему workspace может выбирать пакеты иначе, чем ожидается.
Практический вывод простой: чтобы не утонуть в конфигурациях, действуйте через дисциплину.
- Фиксируйте зависимости через
Cargo.lockи используйте--locked. - Управляйте feature flags явно и проверяйте, что итоговый набор соответствует ожиданиям.
- В workspace не полагайтесь на догадки — используйте
-p
Комментарии
Пока нет комментариев