$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогCargo для боёв: workspace, фичи, зависимости и воспроизводимые сборки

Cargo для боёв: workspace, фичи, зависимости и воспроизводимые сборки

$ sudo teach IT
·19 июля 2026 г.·10 мин·52
Cargo для боёв: workspace, фичи, зависимости и воспроизводимые сборки

Поговорим о том, как организовать проекты через workspace, управлять фичами и зависимостями. Научимся делать сборки стабильными и предсказуемыми.

Содержание
Workspace как основа монорепозиторияКогда workspace нужен, а когда вреденБазовая структураОбщие зависимости и workspace dependenciesОбщие параметры сборки: profiles и lintsУправление фичами: “feature engineering”, а не хаосЧто такое feature в Cargo и почему с ними легко ошибитьсяПринцип: фичи должны быть “продуктовыми”, а не “техническими”Пример: аккуратные features в библиотекеКак избегать дублирования логики в разных crate’ахЕщё одна типичная ошибка: “feature = std” без планаТонкости: “feature names” и семантика defaultЗависимости: версии, локальные пути и единая картина graphpath-зависимости внутри workspacecrate публикуется? Не смешивайте локальные настройки с “package API”Переопределение версий и patchКонфликт фич между версиями зависимостейСтабильные сборки: Cargo.lock, параметры и “что именно вы фиксируете”Cargo.lock: когда он нужен и где он лежит“Воспроизводимая сборка” — это не одно и то же, что “одинаковая бинарь”Закрепляйте toolchain: rust-toolchain.tomlСтабилизация окружения сборкиВстроенная информация в бинарь: версии и метаданныеПрактика: как собирать workspace под конкретные целиМатрица команд для dev/CIПроверка “feature matrix”: не пытайтесь перебрать всёПодводные камни, о которых забывают до первой “боевой” проблемы1) “Включил feature — сломал совместимость с другими crate’ами”2) Default features как скрытая договорённость3) Не фиксируете toolchain4) Не используете --locked в релизной ветке5) Смешивание локальных и публикуемых настроекРепродуцируемость на практике: чеклист для команды

Rust славится предсказуемостью сборок и строгим компилятором, но на практике «стабильность» чаще всего ломается не из‑за Rust, а из‑за архитектуры проекта: как вы организовали workspace, как задали фичи, как устроили зависимости и насколько аккуратно относитесь к версиям и параметрам сборки. Cargo при этом остаётся главным инструментом управления всем этим — и именно на уровне «настройки под реальную жизнь» проявляется разница между учебными проектами и боевыми репозиториями.

В этой статье разберём, как проектировать Rust‑репозиторий так, чтобы он:

  • удобно развивался в нескольких связанных крейтах,
  • корректно включал/выключал фичи без хаоса,
  • минимизировал дрейф зависимостей,
  • выдавал воспроизводимые (или хотя бы максимально детерминированные) сборки.

Workspace как основа монорепозитория

Когда workspace нужен, а когда вреден

Workspace в Cargo — это способ объединить несколько пакетов (crates) в одном репозитории, чтобы управлять общими настройками и зависимостями, не дублируя конфигурацию.

Практические признаки, что workspace вам нужен:

  • в репозитории есть несколько библиотек/бинари‑крейтов, которые развиваются вместе;
  • есть общие типы/утилиты, которые логично переиспользовать между компонентами;
  • вы хотите единое управление версиями и общими зависимостями в рамках репозитория;
  • вы делаете интеграционные тесты, которые зависят от внутренних крейтов.

Признаки, что workspace может быть лишним:

  • репозиторий — один crate и не планируется расширяться;
  • внутренние «модули» фактически всё время остаются внутри одного crate и не требуют отдельного публикационного/сборочного цикла;
  • вы не готовы поддерживать связность между crate’ами (workspace добавляет дисциплину, а не только удобство).

Базовая структура

Типичный layout:

code
myrepo/
  Cargo.toml
  crates/
    core/
      Cargo.toml
      src/lib.rs
    api/
      Cargo.toml
      src/lib.rs
    cli/
      Cargo.toml
      src/main.rs

Корневой Cargo.toml объявляет workspace:

code
[workspace]
resolver = "2"
members = [
  "crates/core",
  "crates/api",
  "crates/cli",
]

Ключевое здесь — resolver = "2". В современных проектах его стоит использовать почти всегда: он корректнее учитывает особенности зависимостей по фичам между разными частями dependency graph.

Общие зависимости и workspace dependencies

В реальной жизни вы быстро начинаете повторять версии зависимостей во многих Cargo.toml. Cargo позволяет централизовать это через workspace.dependencies и затем переиспользовать в членах workspace.

Пример корневого Cargo.toml:

code
[workspace]
resolver = "2"
members = ["crates/core", "crates/api", "crates/cli"]

[workspace.dependencies]
anyhow = "1.0.86"
serde = { version = "1.0.203", features = ["derive"] }
tokio = { version = "1.38.0", features = ["rt-multi-thread", "macros"] }

Теперь в crates/api/Cargo.toml:

code
[package]
name = "api"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }

Плюсы:

  • единый контроль версий;
  • меньше «плавающих» отличий между crate’ами;
  • проще обновлять зависимость.

Минусы (важные): workspace.dependencies не отменяет необходимости думать о фичах. Если в одном crate вам нужна serde с derive, а в другом — нет, вы всё равно унаследуете набор фич, заданный в workspace. Иногда это нормально, но в больших проектах приходится балансировать: часть фич держать централизованно, часть — локально.

Общие параметры сборки: profiles и lints

Если репозиторий активно разрабатывается, вы почти наверняка захотите единообразные profiles (например, для CI) и настройки качества (clippy/rustfmt). В корневом Cargo.toml можно задать профили:

code
[profile.dev]
opt-level = 1
debug = 2

[profile.release]
lto = true
codegen-units = 1
panic = "abort"

Для workspace это особенно полезно: разные crate’ы будут собираться в одном стиле — и воспроизводимость повышается.

Отдельно отмечу: panic = "abort" меняет семантику падений и может влиять на трассировку и поведение в критических местах. Это решение лучше принимать осознанно.


Управление фичами: “feature engineering”, а не хаос

Что такое feature в Cargo и почему с ними легко ошибиться

Cargo features — это именованные опциональные возможности крейта. Они позволяют:

  • включать/выключать код компиляцией (cfg(feature = "..."));
  • включать фичи зависимостей (транзитивно);
  • сокращать бинарь/время компиляции для определённых сценариев использования.

Но именно из‑за транзитивности и «протекания» фичи часто превращаются в хаос: один crate включает feature у другого, тот — у третьего, а в итоге получаем набор комбинаций, которые тестами покрыты слабо.

Принцип: фичи должны быть “продуктовыми”, а не “техническими”

Хорошая практика — проектировать features как сценарии:

  • server / client
  • postgres / sqlite
  • tls / no-tls
  • metrics

Плохая практика — делать features уровня «включить модуль X для отладки Y» без ясной пользовательской цели. Тогда появляются бесконечные комбинации, которые никто не тестирует.

Пример: аккуратные features в библиотеке

crates/core/Cargo.toml:

code
[package]
name = "core"
version = "0.1.0"
edition = "2021"

[features]
default = ["std"]
std = []
async-runtime = ["tokio/rt"]

В коде:

code
// crates/core/src/lib.rs

#[cfg(feature = "std")]
pub fn use_std() {
    // ...
}

#[cfg(feature = "async-runtime")]
pub async fn run_async() {
    // ...
}

Здесь мы видим важную вещь: feature async-runtime не реализует async сам по себе, он лишь включает feature у зависимости (tokio/rt). Cargo поддерживает такую маршрутизацию.

Как избегать дублирования логики в разных crate’ах

Частая проблема: feature объявлен в одном crate, но код инициализации собранного бинарника разнесён между несколькими. В итоге включение/выключение фич перестаёт быть очевидным.

Решение:

  1. Выберите один «источник правды» (обычно — библиотечный crate, который определяет контракт).
  2. Бинарный crate (cli, server) должен лишь включать нужные features целевой библиотеки.

Например, crates/cli/Cargo.toml:

code
[package]
name = "cli"
version = "0.1.0"
edition = "2021"

[dependencies]
core = { path = "../core", default-features = false, features = ["std", "async-runtime"] }
tokio = { workspace = true }

Обратите внимание на default-features = false. Это часто критично: по умолчанию в зависимости может включаться код, который вам не нужен (например, std или TLS). Отключая дефолтные фичи, вы собираете «минимальный профиль», который контролируете явно.

Ещё одна типичная ошибка: “feature = std” без плана

Feature std — популярная практика в библиотеке (особенно при поддержке no_std). Но если ваш проект не разворачивает no_std, то feature std превращается в привычку, не дающую пользы, и увеличивает поверхность настроек.

Вывод: features должны решать конкретные задачи. Не делайте «фичи ради фич».

Тонкости: “feature names” и семантика default

default = [] или default = ["..."] — это тоже часть API вашего crate. Пользователь привык к дефолтам; изменение дефолтов — ломающее изменение.

Если crate развивается, заранее продумайте:

  • какие сценарии действительно “most common”;
  • что должно включаться автоматически при cargo add/обычной зависимости;
  • что лучше оставить опциональным.

Зависимости: версии, локальные пути и единая картина graph

path-зависимости внутри workspace

Внутренние crate’ы обычно подключаются через path или через workspace dependencies с path (по факту — всегда path внутри одного репозитория).

crates/api/Cargo.toml:

code
[dependencies]
core = { path = "../core" }

Это просто, но часто забывают один нюанс: Cargo.lock общий для workspace, и при изменении версий в одном crate это отражается во всей сборке. Поэтому важно держать версионирование и фичи под контролем.

crate публикуется? Не смешивайте локальные настройки с “package API”

Если вы планируете публикацию части crates, следите, чтобы:

  • path зависимости не попадали в опубликованный manifest (Cargo обычно не публикует path как dependency, но ошибки в конфигурации встречаются);
  • workspace-специфичные зависимости не ломали сборку вне репозитория.

На практике это означает: crates/* должны быть в состоянии собраться отдельно (хотя бы cargo test), если вы запускаете их изнутри crate. Это дисциплина, которая экономит часы расследований.

Переопределение версий и patch

Когда в реальном проекте появляются конфликты версий или нужно временно применить фикс, Cargo предлагает [patch].

Пример: вы хотите принудительно использовать конкретную версию или локальную ветку:

code
[patch.crates-io]
serde = { git = "https://github.com/serde-rs/serde", rev = "..." }

Это мощно, но осторожно: patch влияет на весь workspace dependency graph. Хорошая практика — держать patch временно и документировать причину (в комментарии или отдельном issue).

Конфликт фич между версиями зависимостей

Даже если версия зависимости совпадает, набор фич может различаться между crate’ами. В ранних резолверах это иногда давало неожиданные результаты, поэтому resolver = "2" — базовый must.

Но полностью проблема не исчезает: вам всё равно нужно следить за тем, какие фичи включают ваши crate’ы у общих зависимостей.


Стабильные сборки: Cargo.lock, параметры и “что именно вы фиксируете”

Cargo.lock: когда он нужен и где он лежит

Для бинарных проектов и workspaces использование Cargo.lock — почти обязательная практика. Для библиотек cargo publish может пересоздавать lock, но для CI и для воспроизводимости в репозитории lock крайне полезен.

Правило: воспроизводимость сборок обеспечивается lock‑файлом и тем, на каких параметрах вы собираете.

В workspace Cargo.lock обычно находится в корне репозитория. Он фиксирует версии зависимостей, но не фиксирует фичи вашей сборки. То есть:

  • при изменении feature-набора можно получить другой набор включённых зависимостей (или другие зависимости внутри одного и того же пакета);
  • но версии тех, что попали в graph, будут соответствовать lock.

“Воспроизводимая сборка” — это не одно и то же, что “одинаковая бинарь”

Термины полезно разделять:

  • Воспроизводимость на уровне графа зависимостей: одинаковые версии крейтов при одинаковых параметрах.
  • Репродуцируемость на уровне артефакта: одинаковый .wasm/bin/staticlib побайтно.

Второе сложнее: влияет компилятор, флаги, окружение, генерация debug info, встроенные метаданные сборки и т.п. Однако уже “граф” и “параметры сборки” — большой шаг к предсказуемости.

Закрепляйте toolchain: rust-toolchain.toml

Самый частый источник “у нас не воспроизводится” — разные версии Rust и разный набор стандартной библиотеки.

Добавьте rust-toolchain.toml:

code
[toolchain]
channel = "1.78.0"
components = ["rustfmt", "clippy"]
targets = ["x86_64-unknown-linux-gnu"]

CI и разработчики, как правило, будут автоматически использовать нужную версию.

Стабилизация окружения сборки

Даже с одинаковым toolchain бинарь может отличаться из-за:

  • переменных окружения (SOURCE_DATE_EPOCH, RUSTFLAGS);
  • генерации путей (--remap-path-prefix);
  • различий в файловых системах и путях к workspace.

Для практических задач, если вы хотите снизить вариативность, используйте RUSTFLAGS в CI и/или местами cargo build --locked. Пример:

code
cargo build --workspace --release --locked

--locked заставляет Cargo не менять Cargo.lock. Если граф требует обновления lock — команда завершится ошибкой. Это дисциплинирует.

Встроенная информация в бинарь: версии и метаданные

Если вы используете crate_version или env!, то метаданные сборки будут зависеть от того, что вы передаёте через env/manifest. Чтобы это контролировать:

  • избегайте “случайного” чтения времени сборки из окружения;
  • если нужны timestamps — фиксируйте SOURCE_DATE_EPOCH.

Пример в CI (conceptually):

code
export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)
cargo build --release --locked

Это не гарантирует побайтовую идентичность, но делает результаты более устойчивыми.


Практика: как собирать workspace под конкретные цели

Матрица команд для dev/CI

Для реального проекта полезны несколько “профилей сборки”:

  • обычная разработка (быстро, с отладкой),
  • релиз под выпуск (строгие профили, locked),
  • проверка фич (компиляция с разными наборами features).

Примеры команд:

code
# Быстрая сборка всего workspace
cargo build --workspace

# Релиз, строго с lock-файлом
cargo build --workspace --release --locked

# Проверка тестов
cargo test --workspace --locked

# clippy по всем крейтам
cargo clippy --workspace --all-targets --locked

Проверка “feature matrix”: не пытайтесь перебрать всё

Комбинаций features может быть очень много. Практичный подход:

  1. Определите 2–4 наиболее важных конфигурации.
  2. Прогоняйте CI по ним.
  3. Остальные конфигурации проверяйте точечно по мере изменения кода.

Команда для включения/выключения features — через --features и --no-default-features на уровне конкретного пакета.

Если бинарный crate cli зависит от core, а core имеет features, то чаще всего управление делается через зависимости в cli/Cargo.toml или через cargo build -p cli --features .... Но следите, чтобы семантика не расползалась: один и тот же feature должен означать одно и то же.


Подводные камни, о которых забывают до первой “боевой” проблемы

1) “Включил feature — сломал совместимость с другими crate’ами”

Если crate A включает feature X у зависимости B, а crate C делает по‑другому — вы можете получить кодовые пути, которые не тестировались вместе. Решение: тестируйте комбинации хотя бы для “важных” сценариев и держите features на уровне сценариев.

2) Default features как скрытая договорённость

Если вы меняете default в библиотеке, downstream может неожиданно получить другой код. Изменения дефолтов — это изменения API.

3) Не фиксируете toolchain

Разные версии Rust способны изменить диагностические сообщения, поведение лінтера и иногда даже кодогенерацию (особенно в крайних оптимизациях).

4) Не используете --locked в релизной ветке

Это приводит к тому, что “релиз из главной” и “релиз вчера вечером” могут отличаться зависимостями без явного коммита с обновлением lock.

5) Смешивание локальных и публикуемых настроек

Публикуемые crate’ы должны быть “самодостаточными”. Иначе вы получите ситуацию: в workspace оно работает, а как только вы публикуете — разваливается.


Репродуцируемость на практике: чеклист для команды

Ниже — набор шагов, которые реально улучшают воспроизводимость и уменьшают “не у меня”:

  1. Единый layout workspace: корневой Cargo.toml с members и resolver = "2".
  2. Cargo.lock коммитится (для приложений и CI).
  3. cargo build/test --locked в релизных и проверочных пайплайнах.
  4. Фиксированная версия Rust через rust-toolchain.toml.
  5. Tooling единый: clippy/rustfmt версии через components.
  6. features как сценарии, а не как набор кусочков.
  7. **feature matrix в CI

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Cargo – для начинающих!

Курс по теме

Cargo – для начинающих!

С нуля до уверенной работы с Rust-проектами. Подходит для начинающих и тех, кто уже пишет на Rust. Разберитесь с Cargo раз и навсегда — и перестаньте копировать команды вслепую.

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1000
Typer для API-подобных CLI: зависимости, подкоманды и формирование “контракта” команды
typer

Typer для API-подобных CLI: зависимости, подкоманды и формирование “контракта” команды

Покажем, как сделать CLI предсказуемым: единый стиль флагов, повторное использование зависимостей и корректные ответы об ошибках. Статья поможет превратить утилиты в инструменты для команды.

26 июля 2026 г.
630
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Workspace как основа монорепозиторияКогда workspace нужен, а когда вреденБазовая структураОбщие зависимости и workspace dependenciesОбщие параметры сборки: profiles и lintsУправление фичами: “feature engineering”, а не хаосЧто такое feature в Cargo и почему с ними легко ошибитьсяПринцип: фичи должны быть “продуктовыми”, а не “техническими”Пример: аккуратные features в библиотекеКак избегать дублирования логики в разных crate’ахЕщё одна типичная ошибка: “feature = std” без планаТонкости: “feature names” и семантика defaultЗависимости: версии, локальные пути и единая картина graphpath-зависимости внутри workspacecrate публикуется? Не смешивайте локальные настройки с “package API”Переопределение версий и patchКонфликт фич между версиями зависимостейСтабильные сборки: Cargo.lock, параметры и “что именно вы фиксируете”Cargo.lock: когда он нужен и где он лежит“Воспроизводимая сборка” — это не одно и то же, что “одинаковая бинарь”Закрепляйте toolchain: rust-toolchain.tomlСтабилизация окружения сборкиВстроенная информация в бинарь: версии и метаданныеПрактика: как собирать workspace под конкретные целиМатрица команд для dev/CIПроверка “feature matrix”: не пытайтесь перебрать всёПодводные камни, о которых забывают до первой “боевой” проблемы1) “Включил feature — сломал совместимость с другими crate’ами”2) Default features как скрытая договорённость3) Не фиксируете toolchain4) Не используете --locked в релизной ветке5) Смешивание локальных и публикуемых настроекРепродуцируемость на практике: чеклист для команды