Перейти к основному содержимому

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")] компилирует функцию только при включённой feature http.
  • 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 builddevБыстрая итерация, много отладочной информации
cargo build --releasereleaseПродакшен: оптимизация, меньший бинарник
cargo testtestКак 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-обновления применяют вне графика.

Полезный цикл:

  1. cargo update -p имя_крейта;
  2. cargo test --workspace;
  3. cargo clippy --workspace -- -D warnings;
  4. отдельный 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.

Чем раньше структурировать манифест, тем проще поддерживать проект через год.


Связанные материалы