$ 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
ГлавнаяБлогRust Cargo для команды: workspace, общие зависимости и единые версии библиотек

Rust Cargo для команды: workspace, общие зависимости и единые версии библиотек

$ sudo teach IT
·26 июля 2026 г.·10 мин·79
Rust Cargo для команды: workspace, общие зависимости и единые версии библиотек

Покажем, как организовать монорепо без хаоса: workspace, shared crates, настройка версий и feature flags. Статья поможет уменьшить “сюрпризы” при сборке на разных машинах.

Содержание
Модель монорепо в Cargo: что реально контролируетсяWorkspace — это не просто «удобная папка»Crates — единицы версионированияЕдиные версии не «магия», а контрактБазовая структура монорепо: типовой layoutworkspace в корне: правильный Cargo.tomlКак это использовать в командеЦентрализация зависимостей: [workspace.dependencies] и единые версииПочему это снижает «сюрпризы»Shared crates: когда общий код лучше выделитьПример: crates/core как библиотека-ядроПодводный камень: не превращать core в “god crate”Единые версии внутри workspace: lock-файл и политика обновленийCargo.lock как артефакт воспроизводимостиДиапазоны версий vs фиксированные версииFeature flags: единые правила для командыБазовый принцип: features должны быть детерминированными по входуПравильная практика: features только там, где они реально нужныСобственные features crates: пример feature flags без хаосаЧастая ошибка: включать features на уровне transitive dependenciesЕщё одна ошибка: разные версии features у зависимостейУсловные зависимости и единый контроль версийПодводный камень: дублирование зависимостейКоманды для команды: как собирать и тестировать одинаковоИзоляция конфигураций: profile, RUSTFLAGS и reproducible buildsprofiles и оптимизации.cargo/config.toml для единых флаговПример полного минимального набора Cargo.toml для монорепоrepo-root/Cargo.toml

Сборка Rust-проекта в одиночку — относительно предсказуемая история. Но стоит подключить команду, добавить CI и начать разворачивать сборки на разных машинах (включая разные версии Rust, разные профили сборки, разные OS и кэши), как хаос становится заметным: где-то «случайно» подтянулась другая версия зависимости, где-то включилась лишняя функция через features, а где-то crates.io ответил иначе, потому что обновления зависимостей происходили не синхронно.

Вывод из практики простой: если проект становится монорепой (или даже просто набором связанных репозиториев), нужно явно управлять границами: какие части живут вместе, какие зависимости общие, как фиксируются версии, и кто и как включает feature flags. Rust Cargo даёт для этого инструменты, но их легко использовать неправильно.

Ниже — практический разбор, как организовать монорепо на Rust без «сюрпризов», используя:

  • workspace для сборки набора crates как единого целого;
  • общие зависимости и единые версии через Cargo.toml на уровне workspace;
  • стратегии с shared crates (общие библиотеки внутри репо);
  • аккуратное управление features и условными зависимостями;
  • воспроизводимость: lock-файлы, версионирование и правила обновления.

Модель монорепо в Cargo: что реально контролируется

Прежде чем перейти к конфигурациям, важно понимать модель.

Workspace — это не просто «удобная папка»

[workspace] задаёт корневое пространство проекта: Cargo рассматривает перечисленные члены workspace как единый набор пакетов. Это даёт:

  • единый Cargo.lock на уровне workspace (в большинстве конфигураций);
  • единое разрешение версий зависимостей;
  • одинаковый набор членов при сборке через workspace.

Crates — единицы версионирования

Каждый crate — отдельный пакет: он может иметь свой Cargo.toml, свою структуру зависимостей и features. Но источники единства (версии зависимостей, lock-файл, общие настройки) задаются на верхнем уровне и/или через явные зависимости.

Единые версии не «магия», а контракт

Единство версий достигается не тем, что «Cargo сам разберётся», а тем, что вы:

  • фиксируете версии (обычно через lock-файл);
  • управляемо обновляете их (через общий процесс);
  • минимизируете «локальные» расхождения (например, разные диапазоны версий в разных crates);
  • централизуете зависимость на уровне workspace (или используете [workspace.dependencies]).

Базовая структура монорепо: типовой layout

Рассмотрим конфигурацию, которая хорошо работает в командах. Пусть у вас есть:

  • несколько бинарников (например, сервисы);
  • общие библиотеки;
  • утилиты.

Пример структуры:

code
repo-root/
  Cargo.toml                # workspace root
  Cargo.lock               # единый lock
  crates/
    api/
      Cargo.toml
      src/
    core/
      Cargo.toml
      src/
    storage/
      Cargo.toml
      src/
  services/
    auth-service/
      Cargo.toml
      src/
    billing-service/
      Cargo.toml
      src/

Вы заметите: у каждого crate свой Cargo.toml. Но корневой Cargo.toml описывает workspace и общие правила.


workspace в корне: правильный Cargo.toml

Минимальный корневой Cargo.toml выглядит так:

code
[workspace]
members = [
  "crates/core",
  "crates/api",
  "crates/storage",
  "services/auth-service",
  "services/billing-service",
]
resolver = "2"

Ключевые моменты:

  • resolver = "2" — рекомендуемая настройка для современных проектов. Она меняет алгоритм разрешения features и часто уменьшает неожиданные эффекты при зависимостях.
  • members должен включать все crates, которые вы хотите собирать и/или тестировать внутри монорепо.

Как это использовать в команде

Дальше в CI и для разработчиков можно закрепить одинаковые команды:

  • сборка: cargo build -p auth-service
  • тесты: cargo test -p core
  • проверка: cargo fmt --all и cargo clippy --workspace

Если вы каждый раз собираете «в разнобой», вы гарантированно получите несоответствия в lock-файлах и включённых features.


Централизация зависимостей: [workspace.dependencies] и единые версии

Самая практичная часть — закрепить версии основных внешних зависимостей так, чтобы разные crates не расходились по диапазонам.

Начиная с Cargo 2, удобно использовать секцию [workspace.dependencies]. Пример:

code
# repo-root/Cargo.toml
[workspace]
members = [
  "crates/core",
  "crates/api",
  "crates/storage",
  "services/auth-service",
  "services/billing-service",
]
resolver = "2"

[workspace.dependencies]
anyhow = "1.0.86"
thiserror = "1.0.61"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.117"
tokio = { version = "1.38.0", features = ["rt-multi-thread", "macros"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
reqwest = { version = "0.12.7", features = ["json", "rustls-tls"] }

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

code
# crates/core/Cargo.toml
[package]
name = "core"
version = "0.1.0"
edition = "2021"

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

tracing = { workspace = true }

Аналогично в services/auth-service/Cargo.toml:

code
[package]
name = "auth-service"
version = "0.1.0"
edition = "2021"

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

tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
reqwest = { workspace = true }
serde_json = { workspace = true }

Почему это снижает «сюрпризы»

Если вы не централизуете зависимости, в одном crate может быть tokio = "1", в другом — tokio = { version = "1.37", ... }, и в итоге Cargo будет разрешать версии исходя из диапазонов. Он, конечно, согласует зависимости в рамках lock-файла, но:

  • lock-файл может обновиться иначе, чем ожидает команда;
  • features на уровне зависимостей могут проявиться неожиданно;
  • разница в lock-файлах между ветками будет мешать воспроизводимости.

С [workspace.dependencies] вы задаёте единый источник истины — и это сильно упрощает code review.


Shared crates: когда общий код лучше выделить

В реальных проектах общая логика редко ограничивается «одним-двумя helper functions». Со временем появляются повторения:

  • сериализация доменных структур;
  • модели ошибок;
  • общие схемы конфигурации;
  • клиенты внешних сервисов;
  • адаптеры к БД.

В такой момент shared crates внутри workspace обычно лучше, чем копирование кода или path = с хаотичными зависимостями.

Пример: crates/core как библиотека-ядро

  • core хранит доменные типы и бизнес-ошибки;
  • api может зависеть от core для контрактов;
  • сервисы используют api и storage.

Пример зависимости:

code
# crates/api/Cargo.toml
[package]
name = "api"
version = "0.1.0"
edition = "2021"

[dependencies]
core = { path = "../core" }
serde = { workspace = true }
serde_json = { workspace = true }
code
# services/auth-service/Cargo.toml
[dependencies]
api = { path = "../../crates/api" }
storage = { path = "../../crates/storage" }
tokio = { workspace = true }

Подводный камень: не превращать core в “god crate”

Если core станет зависеть абсолютно от всего (включая HTTP-клиенты, конкретные адаптеры БД, особенности конкретной инфраструктуры), он превратится в узкое горлышко: любое изменение тянет за собой пересборку всего.

Практическая стратегия:

  • ядро: домен, ошибки, интерфейсы;
  • адаптеры: инфраструктурные детали можно держать отдельно (storage, clients, infrastructure crates);
  • сервисы: композиция и wiring.

Это уменьшает «каскады» зависимостей и улучшает время сборки.


Единые версии внутри workspace: lock-файл и политика обновлений

Cargo.lock как артефакт воспроизводимости

В монорепо важно, чтобы lock-файл был общим и коммитился в репозиторий. Тогда разработчики на разных машинах получают одинаковое граф зависимостей.

Практика для команд:

  • включить Cargo.lock в репозиторий (обычно так и делается для бинаров workspace);
  • в CI проверять, что lock-файл не меняется неожиданно.

Команда для обновления:

  • точечно обновлять зависимости через cargo update -p <crate>.
  • или разом обновлять: cargo update.

Но здесь критично: обновление должно быть управляемым процессом, а не результатом «кто-то у себя обновил».

Диапазоны версий vs фиксированные версии

В [workspace.dependencies] часто пишут «диапазон» вида "1.0.86" или "^1". Даже если вы оставляете диапазоны, итоговая версия будет зафиксирована lock-файлом. Поэтому ключевой механизм воспроизводимости — lock.

Если же вы хотите минимизировать риск расхождений даже без lock (например, в ранних стадиях), лучше явно задавать версии без «прыжков» (и всё равно коммитить lock).


Feature flags: единые правила для команды

Features — источник большинства реальных проблем в больших Rust-проектах: разные crates могут включать разные features одной и той же зависимости, и граф меняется. resolver = "2" помогает, но не решает всё.

Базовый принцип: features должны быть детерминированными по входу

В идеале:

  • на уровне workspace вы определяете «конфигурируемость»;
  • на уровне конкретных crates — включаете только то, что требуется;
  • избегаете «самовольных» features в каждом месте.

Правильная практика: features только там, где они реально нужны

Допустим, вы хотите использовать reqwest с TLS, но не хотите тянуть определённые backend’ы везде.

В [workspace.dependencies] можно указать дефолтный набор:

code
[workspace.dependencies]
reqwest = { version = "0.12.7", features = ["json", "rustls-tls"] }

Тогда все crates будут собираться одинаково. Но что если какой-то crate должен работать с native-tls, а другой — с rustls? Тогда возникает конфликт стратегии.

В таком случае лучше разделять зависимости на уровне crate через разные объявления, но делать это осознанно и документированно:

  • либо вынести разные зависимости в разные crates (например, clients-rustls и clients-native);
  • либо использовать Cargo features собственного crate и включать нужный вариант.

Собственные features crates: пример feature flags без хаоса

Допустим, core должен поддерживать режимы обработки:

  • metrics — включить интеграцию с системой метрик;
  • logging — выбрать реализацию логирования.

В crates/core/Cargo.toml:

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

[features]
default = ["logging"]
logging = ["dep:tracing"]
metrics = ["dep:metrics", "dep:metrics-exporter-prometheus"]

[dependencies]
tracing = { workspace = true, optional = true }

metrics = { version = "0.22.3", optional = true }
metrics-exporter-prometheus = { version = "0.16.0", optional = true }

Обратите внимание:

  • зависимости, завязанные на feature, помечены optional = true;
  • feature включает ровно нужные зависимости.

А теперь важно — кто будет включать эти features? Обычно сервисы.

В services/auth-service/Cargo.toml:

code
[dependencies]
core = { path = "../../crates/core", features = ["metrics"] }

Частая ошибка: включать features на уровне transitive dependencies

Если вы в core включите metrics, а auth-service ожидает «выключено», вы получите неожиданное поведение. Следите за тем, чтобы features не включались «по умолчанию» там, где логика должна быть управляемой на верхнем уровне.

Ещё одна ошибка: разные версии features у зависимостей

Когда разные crates включают разные features одной внешней зависимости, у вас может появляться ситуация «одна половина кода вызывает API, другая — нет». В Rust это проявляется как:

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

Практика: сведите features к минимально необходимым, а верхний уровень (сервисы) пусть определяет «сборочную конфигурацию».


Условные зависимости и единый контроль версий

В реальном проекте вы можете использовать разные реализации под платформы или фичи. Например, для storage один адаптер для PostgreSQL, другой — для SQLite.

В crates/storage/Cargo.toml:

code
[features]
default = ["postgres"]
postgres = ["dep:sqlx-postgres"]
sqlite = ["dep:sqlx-sqlite"]

[dependencies]
sqlx-postgres = { package = "sqlx", version = "0.8.3", optional = true, features = ["postgres", "runtime-tokio-native-tls"] }
sqlx-sqlite  = { package = "sqlx", version = "0.8.3", optional = true, features = ["sqlite",  "runtime-tokio-native-tls"] }

И включение на сервисном уровне:

code
# services/auth-service/Cargo.toml
storage = { path = "../../crates/storage", features = ["postgres"] }

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

Тут возможна путаница: фактически вы используете один crate sqlx, но в Cargo записываете его как «разные входы» через package = "sqlx". Это допустимо, но требует аккуратной конфигурации кода (какие модули компилируются, как они импортируются).

Лучше сделать тонкую прослойку в storage и спрятать различия за trait’ами/модулями, чтобы наружу интерфейс был одинаковым.


Команды для команды: как собирать и тестировать одинаково

Чтобы уменьшить сюрпризы, полезно зафиксировать «одинаковые пути» в документации (и желательно — скриптами/Makefile).

Рекомендованный набор команд:

  • Форматирование по всем crates:
    • cargo fmt --all
  • Проверка:
    • cargo clippy --workspace --all-targets -- -D warnings
  • Тесты:
    • cargo test --workspace
  • Сборка одного сервиса:
    • cargo build -p auth-service

Важно: в CI лучше запускать cargo test --workspace или минимум тесты ключевых crates. Иначе вы получите ситуацию, когда core компилируется, а сборка api падает на другом наборе features (или из-за другой ветки conditional compilation).


Изоляция конфигураций: profile, RUSTFLAGS и reproducible builds

Сборка может отличаться не только из-за зависимостей, но и из-за окружения компилятора.

profiles и оптимизации

Если в dev и release разные флаги, вы можете получить расхождения в логике, зависящей от cfg(debug_assertions) или тонких UB-проблем. Для команды полезно:

  • хранить профиль в Cargo.toml workspace (или .cargo/config.toml);
  • минимизировать «разъезд» по настройкам между разработчиками.

Пример добавления в корневой Cargo.toml:

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

.cargo/config.toml для единых флагов

Если в проекте нужна единая настройка (например, net.git-fetch-with-cli, build.jobs, rustflags), лучше положить её в .cargo/config.toml на уровне репозитория.

Пример:

code
# .cargo/config.toml
[build]
jobs = 8

[target.'cfg(all())']
rustflags = ["-C", "target-cpu=native"]

Но будьте осторожны: target-cpu=native может ухудшить воспроизводимость между машинами. Для production чаще используют более консервативные флаги.


Пример полного минимального набора Cargo.toml для монорепо

Чтобы собрать всё воедино, приведём «скелет» конфигурации.

repo-root/Cargo.toml

code
[workspace]
members = [
  "crates/core",
  "crates/api",
  "crates/storage",
  "services/auth-service",

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

Автор

$ sudo teach IT

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

Курс по теме

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

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

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

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

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

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

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

Перейти к курсу
Solidity DLC - поймай, если сможешь

Solidity DLC - поймай, если сможешь

Аудит смарт-контрактов на реальных взломах. The DAO, Parity, Wormhole, Ronin, Euler и ещё 7 разборов: что пошло не так, сколько потеряли и что бы спасло. Атаки и защиты на Foundry, план входа в профессию аудитора.

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

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

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

Перейти к курсу
Solidity pro - всё или ничего

Solidity pro - всё или ничего

Как пишут контракты, которые работают в основной сети. Наследование и OpenZeppelin, паттерны продакшена, оптимизация газа, ABI, delegatecall и подписи. 7 модулей, 33 урока, а в финале собственное dApp: мини-DAO с токеном, голосованием и казной.

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

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

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

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

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

18 сентября 2026 г.
1960
Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта
пишем

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта

Построим удобный интерфейс командной утилиты: структура подкоманд, чтение конфигов, валидация входных параметров и единая логика ошибок с понятными exit code. Сфокусируемся на UX для тех, кто запускает CLI в CI и в скриптах.

15 сентября 2026 г.
1390
Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

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

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

21 сентября 2026 г.
1970
Как начать с HTML и сделать первую страницу: пошагово и без сложных слов
html

Как начать с HTML и сделать первую страницу: пошагово и без сложных слов

Разберём, что нужно новичку, чтобы за 1–2 вечера собрать свою первую веб-страницу: структура HTML, базовые теги, ссылки и картинки, плюс простой чек-лист, как понять, что страница работает правильно.

2 ноября 2026 г.
20
Деплой без сюрпризов: как подготовить Docker-образ, переменные окружения и healthcheck
деплой

Деплой без сюрпризов: как подготовить Docker-образ, переменные окружения и healthcheck

Разберём практический чек-лист подготовки контейнера к продакшену: как собрать воспроизводимый образ, как правильно прокидывать конфиги/секреты через окружение и как настроить healthcheck, чтобы деплой не “успевал” раньше готовности сервиса.

18 сентября 2026 г.
1930
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
140

Комментарии

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

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

Содержание

Модель монорепо в Cargo: что реально контролируетсяWorkspace — это не просто «удобная папка»Crates — единицы версионированияЕдиные версии не «магия», а контрактБазовая структура монорепо: типовой layoutworkspace в корне: правильный Cargo.tomlКак это использовать в командеЦентрализация зависимостей: [workspace.dependencies] и единые версииПочему это снижает «сюрпризы»Shared crates: когда общий код лучше выделитьПример: crates/core как библиотека-ядроПодводный камень: не превращать core в “god crate”Единые версии внутри workspace: lock-файл и политика обновленийCargo.lock как артефакт воспроизводимостиДиапазоны версий vs фиксированные версииFeature flags: единые правила для командыБазовый принцип: features должны быть детерминированными по входуПравильная практика: features только там, где они реально нужныСобственные features crates: пример feature flags без хаосаЧастая ошибка: включать features на уровне transitive dependenciesЕщё одна ошибка: разные версии features у зависимостейУсловные зависимости и единый контроль версийПодводный камень: дублирование зависимостейКоманды для команды: как собирать и тестировать одинаковоИзоляция конфигураций: profile, RUSTFLAGS и reproducible buildsprofiles и оптимизации.cargo/config.toml для единых флаговПример полного минимального набора Cargo.toml для монорепоrepo-root/Cargo.toml