Cargo — workspace, features и профили
Cargo — workspace, features и профили
Зачем углубляться в Cargo
После первой программы обычно кажется, что Cargo — это просто cargo new и cargo run. На учебном примере этого достаточно, но в рабочем проекте структура быстро становится шире.
Появляются библиотека + бинарник, несколько crate в одном репозитории, опциональные модули (features), разные режимы оптимизации (profiles) и иногда скрипт build.rs перед компиляцией.
Cargo остаётся единой точкой входа — зависимости, сборка, тесты и документация. Уверенное понимание Cargo.toml экономит много времени при разборе проблем на CI и в командной разработке.
Обзор инструментов: фреймворки и Cargo. Синтаксис: справочник.
Словарь
| Термин | Значение |
|---|---|
| Package (пакет) | То, что описано одним Cargo.toml — имя, версия, зависимости. |
| Crate | Единица компиляции: библиотека (rlib) или исполняемый файл. |
| Workspace | Несколько пакетов в одном репозитории с общим Cargo.lock. |
| Feature | Флаг условной компиляции и опциональных зависимостей. |
| Profile | Набор настроек компилятора: dev, release, test. |
| Cargo.lock | Зафиксированные точные версии зависимостей (для приложений коммитят в git). |
Один пакет — бинарник и библиотека вместе
Типичный сервис:
my-app/
Cargo.toml
src/
main.rs # точка входа: cargo run
lib.rs # логика, pub API, тесты
Разбор:
- Это минимальная структура пакета, где есть и исполняемый файл, и библиотечный слой.
main.rsотвечает за запуск приложения и wiring.lib.rsсодержит повторно используемую логику, которую удобно тестировать отдельно.- Такое разделение уменьшает размер
main.rsи повышает переиспользуемость кода. - Интеграционные тесты обычно импортируют именно библиотеку из
lib.rs.
Cargo.toml:
[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
[[bin]]
name = "my-app"
path = "src/main.rs"
По соглашению Cargo и так ищет src/main.rs и src/lib.rs; секция [[bin]] нужна, если бинарников несколько или путь нестандартный.
Практика: вся логика в lib.rs (pub fn run()), main.rs только вызывает my_app::run(). Тогда интеграционные тесты в tests/ подключают библиотеку как внешний клиент.
Пример тонкого main.rs:
// src/lib.rs
pub fn greet(name: &str) -> String {
format!("Привет, {name}")
}
// src/main.rs
fn main() {
println!("{}", my_app::greet("Cargo"));
}
Разбор:
greetживёт в библиотеке и доступен для unit/integration тестов.main.rsне содержит бизнес-логики, только запуск.- Имя crate в вызове (
my_app) берётся из[package].nameвCargo.toml. - Такой шаблон упрощает рефакторинг и повторное использование кода.
- При росте проекта
lib.rsделят на модули (routes,services).
Для динамической библиотеки под FFI:
[lib]
crate-type = ["cdylib"]
Разбор:
- Секция
[lib]настраивает параметры сборки библиотечного crate. crate-type = ["cdylib"]говорит Cargo выпускать динамическую библиотеку для внешних языков.- Это необходимо, когда Rust-код должен быть подключён из C/C++ или другой среды через FFI.
- Формат конечного файла зависит от ОС:
.dll,.so,.dylib. - Для чисто Rust-внутреннего использования такой тип обычно не нужен.
Собирается .dll / .so / .dylib — см. FFI.
Workspace (монорепозиторий)
Несколько связанных crate в одном git-репозитории:
acme/
Cargo.toml # корень: [workspace]
crates/
core/
Cargo.toml
src/lib.rs
api/
Cargo.toml
src/main.rs
Разбор:
- Корень содержит общий
Cargo.tomlс настройками всего workspace. - Папка
crates/хранит отдельные пакеты (модули) одного репозитория. core- библиотечный crate с общей бизнес-логикой.api- исполняемый crate (сервис/CLI), который используетcore.- Такая структура облегчает масштабирование проекта на несколько команд и областей ответственности.
Корневой Cargo.toml:
Код ITЗагрузка примера кода…
resolver = "2"— современные правила разрешения features зависимостей (рекомендуется для новых workspace).[workspace.dependencies]— общий каталог версий; в дочерних пакетах пишутserde = { workspace = true }.
crates/api/Cargo.toml:
[package]
name = "acme-api"
[dependencies]
acme-core = { path = "../core" }
tokio = { workspace = true }
serde = { workspace = true }
Команды из корня репозитория:
cargo build -p acme-api
cargo test --workspace
cargo run -p acme-api
Разбор:
-p acme-apiвыбирает конкретный пакет в workspace по имени.cargo build -p ...собирает только нужный crate и его зависимости.cargo test --workspaceзапускает тесты сразу всех участников workspace.cargo run -p ...запускает выбранный бинарник, если в workspace их несколько.- Такой режим удобен, когда монорепозиторий содержит много независимых сервисов.
Преимущества: один Cargo.lock, переиспользование кода через path = "..." без публикации на crates.io, согласованные версии tokio / serde во всех crate.
Features (условная сборка)
Feature — имя переключателя в Cargo.toml. Он может включать зависимость и куски кода через #[cfg(feature = "...")].
[dependencies]
reqwest = { version = "0.12", optional = true }
[features]
default = ["http"]
http = ["dep:reqwest"]
optional = true— зависимость тянется только если включена feature.default— что включено при обычномcargo build.http = ["dep:reqwest"]— синтаксис Cargo 1.60+: явная привязка feature к зависимости.
В коде:
#[cfg(feature = "http")]
pub fn fetch(url: &str) -> Result<String, reqwest::Error> {
reqwest::blocking::get(url)?.text()
}
Разбор:
#[cfg(feature = "http")]компилирует функцию только при включённой featurehttp.fetchвозвращаетResult, чтобы явно различать успешный ответ и сетевую ошибку.reqwest::blocking::get(url)выполняет синхронный HTTP-запрос.- Оператор
?пробрасывает ошибку наверх, если запрос не удался. .text()извлекает тело ответа какString.
Сборка:
cargo build --no-default-features
cargo build --features http
cargo test --all-features
Разбор:
--no-default-featuresвыключает набор флагов изdefault, проверяя "минимальную" конфигурацию.--features httpвключает конкретный feature-флаг вручную.cargo test --all-featuresпрогоняет тесты со всеми включаемыми возможностями.- Эти команды помогают ловить ошибки условной компиляции заранее.
- Практика полезна для библиотек, где пользователи выбирают разные комбинации features.
Важно для дизайна: features должны быть аддитивными (включение не ломает чужой код). Другой crate в графе зависимостей может снова включить вашу feature — полагаться на "мы её выключили" нельзя. Взаимоисключающие режимы лучше разнести по разным crate или документировать один главный feature.
Профили — dev, release, test
Профиль — набор флагов компилятора в Cargo.toml:
[profile.dev]
opt-level = 0
debug = true
[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
| Команда | Профиль | Назначение |
|---|---|---|
cargo build | dev | Быстрая итерация, много отладочной информации |
cargo build --release | release | Продакшен: оптимизация, меньший бинарник |
cargo test | test | Как dev + отладка для тестов |
opt-level— степень оптимизации (0 — почти без оптимизаций).lto— link-time optimization, дольше сборка, иногда меньше и быстрее бинарник.codegen-units = 1в release — одна единица генерации кода, лучше оптимизация, дольше компиляция.
Кастомный профиль для бенчмарков:
[profile.bench]
inherits = "release"
debug = true
Разбор:
[profile.bench]задаёт отдельные параметры сборки для бенчмарков.inherits = "release"берёт за основу оптимизированный релизный профиль.debug = trueдобавляет отладочную информацию, что упрощает профилирование.- Это позволяет анализировать производительность без потери трассировки символов.
- Настройка полезна для сравнения горячих участков кода и работы оптимизаций.
Зависимости — секции и версии
[dependencies]
anyhow = "1"
thiserror = "2"
[dev-dependencies]
tokio = { version = "1", features = ["rt", "macros"] }
[build-dependencies]
cc = "1"
| Секция | Когда используется |
|---|---|
dependencies | Основной код |
dev-dependencies | Только тесты, примеры, бенчмарки |
build-dependencies | Только скрипт build.rs |
Версия "1" на crates.io означает semver: совместимы обновления 1.x.y, но не 2.0.0. cargo update пересчитывает Cargo.lock. Для библиотек, публикуемых на crates.io, lock-файл обычно не коммитят; для приложений — коммитят, чтобы CI и коллеги собирали одинаково.
Совет: у тяжёлых зависимостей отключайте лишние default-features — ускоряет сборку у всех, кто подключит ваш crate.
Отключение лишних default-features:
[dependencies]
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
Разбор:
default-features = falseне тянет весь набор возможностей crate "по умолчанию".features = ["rustls-tls"]включает только нужный TLS-бэкенд.- Меньше features обычно означает быстрее сборку и меньше бинарник.
- Такой подход особенно важен для библиотек, которые подключают многие проекты.
- Перед публикацией библиотеки стоит явно документировать рекомендуемый набор features.
build.rs — скрипт до компиляции
Файл build.rs в корне пакета Cargo запускает перед компиляцией Rust-кода. Типичные задачи:
- собрать C/C++ через
cc; - сгенерировать код (bindgen, protobuf);
- передать линковщику пути:
println!("cargo:rustc-link-search=..."); - сказать Cargo пересобрать при изменении файла:
println!("cargo:rerun-if-changed=...").
Пример:
// build.rs
fn main() {
cc::Build::new()
.file("native/add.c")
.compile("native_add");
println!("cargo:rerun-if-changed=native/add.c");
}
Подробнее про связывание с C — FFI на практике.
Полезные команды
cargo tree -p my-app # дерево зависимостей
cargo check # проверка типов без полной линковки (быстрее build)
cargo doc --open # локальная документация зависимостей и вашего кода
cargo clippy -- -D warnings # линтер
cargo fmt # единый стиль форматирования
Разбор:
cargo tree -p my-appпоказывает, какие crate и версии реально входят в граф зависимостей.cargo checkбыстро проверяет типы и заимствования без полной сборки бинарника.cargo doc --openгенерирует и открывает локальную документацию по вашему API и зависимостям.cargo clippy -- -D warningsусиливает контроль качества, делая предупреждения ошибками.cargo fmtприводит код к единому стилю и снижает шум в review.
Версионирование зависимостей на практике
Для командной разработки важно заранее договориться, как обновлять версии:
- минорные обновления (
1.4->1.5) можно делать регулярно по расписанию; - мажорные (
1.x->2.0) лучше выносить в отдельную задачу и прогонять тесты всего workspace; - критичные security-обновления применяют вне графика.
Полезный цикл:
cargo update -p имя_крейта;cargo test --workspace;cargo clippy --workspace -- -D warnings;- отдельный changelog по обновлению зависимости.
Так команда понимает, что изменилось и где искать регрессии.
Feature-флаги для библиотек и приложений
Feature-дизайн отличается по типу проекта:
| Тип проекта | Рекомендация |
|---|---|
| Библиотека | минимальный default, аддитивные features, чёткая документация комбинаций |
| Приложение | можно включать больше defaults, потому что вы контролируете итоговый бинарник |
Для библиотек особенно важно избегать скрытых побочных эффектов features: пользователь включает флаг и получает предсказуемо добавленную функциональность.
Что кладут в корневой CI для workspace
Минимальный рабочий набор:
- run: cargo check --workspace
- run: cargo test --workspace
- run: cargo test --workspace --all-features
- run: cargo fmt --check
- run: cargo clippy --workspace -- -D warnings
Разбор:
cargo check --workspaceдаёт самый быстрый сигнал, что весь репозиторий компилируется.cargo test --workspaceпроверяет функциональность по тестам всех crate.- Дополнительный прогон с
--all-featuresловит проблемы условной компиляции. cargo fmt --checkгарантирует, что стиль кода соблюдён.cargo clippy ... -D warningsне пропускает потенциально проблемные места в main-ветку.
Если в репозитории есть build.rs и FFI, полезно отдельно собирать Linux и Windows, чтобы сразу поймать платформенные расхождения линковки.
Анти-паттерны в Cargo.toml
- дублирование одних и тех же версий по десяткам crate вместо
workspace.dependencies; - очень тяжёлые default-features без необходимости;
- смешивание
lib-логики и запуска приложения в одном огромномmain.rs; - отсутствие комментариев к нестандартным профилям и
build.rs.
Чем раньше структурировать манифест, тем проще поддерживать проект через год.