Монорепозиторий с Rust: организация кода на практике

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

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

Начать курс

Монорепозиторий с Rust: организация кода на практике

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

Что такое Cargo Workspace и зачем он нужен

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

  • Единая система сборки — одна команда cargo build собирает все крейты.
  • Общие зависимости — дублирующиеся зависимости разрешаются один раз, что уменьшает размер итогового бинарника.
  • Упрощённое тестирование — можно запустить тесты для всех крейтов одной командой.
  • Согласованность версий — все компоненты используют одни и те же версии библиотек.

Монорепозиторий особенно полезен для микросервисов, библиотек общего назначения или проектов с чётким разделением на слои (например, ядро, API, CLI).

Создание структуры монорепозитория

Начнём с практического примера. Предположим, мы разрабатываем систему управления задачами. У нас будет три крейта:

  • core — библиотека с бизнес-логикой.
  • cli — исполняемый файл для командной строки.
  • server — веб-сервер (исполняемый файл).

Сначала создадим корневую директорию и файл Cargo.toml для workspace:

mkdir task_manager
cd task_manager
# Создаём корневой Cargo.toml
touch Cargo.toml

В корневом Cargo.toml указываем workspace:

[workspace]
members = [
    "core",
    "cli",
    "server",
]

Теперь создадим каждый крейт с помощью cargo new:

cargo new core --lib
cargo new cli
cargo new server

После этого структура будет выглядеть так:

task_manager/
├── Cargo.toml          # Корневой workspace
├── core/
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
├── cli/
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
└── server/
    ├── Cargo.toml
    └── src/
        └── main.rs

Обратите внимание: корневой Cargo.toml не содержит секции [package], только [workspace]. Это важно для корректной работы.

Управление зависимостями между крейтами

Одна из ключевых задач в монорепозитории — правильно настроить зависимости между крейтами. В нашем примере cli и server зависят от core. Укажем это в их Cargo.toml:

Для cli/Cargo.toml:

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

[dependencies]
core = { path = "../core" }
clap = { version = "4.0", features = ["derive"] }  # Внешняя зависимость для CLI

Для server/Cargo.toml:

[package]
name = "server"
version = "0.1.0"
edition = "2021"

[dependencies]
core = { path = "../core" }
actix-web = "4"  # Внешняя зависимость для веб-сервера

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

Теперь напишем немного кода в core/src/lib.rs:

pub struct Task {
    pub id: u32,
    pub title: String,
    pub completed: bool,
}

impl Task {
    pub fn new(id: u32, title: &str) -> Self {
        Task {
            id,
            title: title.to_string(),
            completed: false,
        }
    }

    pub fn complete(&mut self) {
        self.completed = true;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_task_creation() {
        let task = Task::new(1, "Learn Rust");
        assert_eq!(task.title, "Learn Rust");
        assert!(!task.completed);
    }

    #[test]
    fn test_task_completion() {
        let mut task = Task::new(2, "Write article");
        task.complete();
        assert!(task.completed);
    }
}

А в cli/src/main.rs используем этот крейт:

use core::Task;
use clap::Parser;

#[derive(Parser)]
struct Args {
    #[arg(short, long)]
    title: String,
}

fn main() {
    let args = Args::parse();
    let mut task = Task::new(1, &args.title);
    println!("Created task: {}", task.title);
    task.complete();
    println!("Task completed: {}", task.completed);
}

Теперь можно собрать весь проект одной командой из корня:

cargo build --workspace

Или запустить тесты для всех крейтов:

cargo test --workspace

Обратите внимание: при использовании --workspace Cargo проверяет все крейты, что очень удобно для CI/CD.

Работа с общими конфигурациями и скриптами

В монорепозитории часто возникает потребность в общих настройках, например, для линтера clippy или форматтера rustfmt. Вы можете разместить файлы конфигурации в корне проекта, и они будут применяться ко всем крейтам. Например, создайте rust

Блоги

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