Монорепозиторий с Rust: организация кода для больших проектов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение

С ростом проекта на Rust возникает вопрос: как организовать код, чтобы он оставался поддерживаемым, масштабируемым и удобным для команды? Один из популярных подходов — монорепозиторий (monorepo). В этой статье мы разберем, что такое монорепозиторий, как его организовать с помощью Cargo workspace, и приведем практические примеры. Вы узнаете, как структурировать код, управлять зависимостями и избежать типичных ошибок.

Что такое монорепозиторий и зачем он нужен в Rust?

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

Преимущества монорепозитория для Rust:

  • Единая версия зависимостей — все крейты используют одинаковые версии внешних пакетов, что снижает конфликты.
  • Упрощение рефакторинга — изменения в одной библиотеке сразу видны во всех зависимых крейтах.
  • Сквозная интеграция — можно легко запускать тесты и линтинг для всего проекта.
  • Централизованная документация — все документы и CI/CD находятся в одном месте.

Однако монорепозиторий подходит не всем. Для маленьких проектов или команд с разными циклами разработки лучше выбрать мультирепо. Но для экосистемы Rust, где модульность и переиспользование кода — норма, монорепозиторий становится мощным инструментом.

Организация монорепозитория с Cargo Workspace

Cargo поддерживает механизм workspace — это группа крейтов, которые собираются вместе и используют общий Cargo.lock. Рассмотрим базовую структуру.

Создание workspace

Создайте корневой каталог и файл Cargo.toml без секции [package], а с секцией [workspace]:

# корневой Cargo.toml
[workspace]
members = [
    "core",
    "cli",
    "web",
]

Теперь создайте подкаталоги core, cli, web. В каждом из них должен быть свой Cargo.toml с типичной структурой пакета. Например, для core:

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

[dependencies]
serde = { version = "1.0", features = ["derive"] }

Для cli и web можно указать зависимость от my_core:

# cli/Cargo.toml
[package]
name = "my_cli"
version = "0.1.0"
edition = "2021"

[dependencies]
my_core = { path = "../core" }
clap = { version = "4.0", features = ["derive"] }

Теперь при запуске cargo build из корня соберутся все крейты, а Cargo.lock будет общим.

Структура папок и модулей

Рекомендуемая структура монорепозитория для Rust:

my_monorepo/
├── Cargo.toml          # workspace
├── Cargo.lock
├── core/               # библиотека с общими типами
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
├── cli/                # консольное приложение
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
├── web/                # веб-сервер (например, на Actix)
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
├── tests/              # интеграционные тесты (опционально)
│   └── integration_test.rs
├── scripts/            # скрипты для CI/CD
└── README.md

Внутри каждого крейта используйте стандартную модульную структуру Rust. Например, в core/src/lib.rs:

// core/src/lib.rs
pub mod models;
pub mod utils;

А в core/src/models.rs:

// core/src/models.rs
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct User {
    pub id: u64,
    pub name: String,
    pub email: String,
}

Такая организация позволяет легко переиспользовать User в других крейтах.

Управление публичными зависимостями и версиями

Одна из главных задач в монорепозитории — избежать дублирования зависимостей. Cargo workspace решает это автоматически: все крейты используют один Cargo.lock. Но есть нюансы.

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

Если несколько крейтов используют одну и ту же библиотеку (например, serde), Cargo подтянет её один раз. Однако если крейты требуют разные версии, возникнет конфликт. Чтобы этого избежать, централизуйте зависимости в корневом Cargo.toml с помощью секции [workspace.dependencies] (доступно с Rust 1.64+):

# корневой Cargo.toml
[workspace]
members = ["core", "cli", "web"]

[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }

Теперь в Cargo.toml каждого крейта можно ссылаться на эти зависимости без указания версии:

# cli/Cargo.toml
[dependencies]
my_core = { path = "../core" }
serde.workspace = true
tokio.workspace = true

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

Публичные vs приватные зависимости

В монорепозитории часто выделяют публичные крейты (которые публикуются в crates.io) и приватные (только для внутреннего использования). Чтобы явно указать, что крейт не должен публиковаться, добавьте в его Cargo.toml:

[package]
publish = false

                
                

Блоги

Рекомендаций по книгам