Монорепозиторий с 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