Введение: Зачем нужен монорепозиторий в Rust?
Когда вы начинаете изучать Rust, первое, с чем вы сталкиваетесь — это одиночный проект, созданный через cargo new. Но что делать, когда ваш проект разрастается: появляются общие библиотеки, несколько исполняемых файлов (например, сервер и клиент) или вы хотите вынести повторяющийся код в отдельный пакет?
Здесь на помощь приходит монорепозиторий (monorepo) — это подход, при котором код нескольких взаимосвязанных проектов (библиотек и исполняемых файлов) хранится в одном репозитории. Rust предоставляет для этого встроенное решение — Cargo Workspaces (рабочие пространства).
В этой статье мы разберем, как организовать монорепозиторий в Rust с нуля: начиная от структуры папок и заканчивая практическими примерами. Вы узнаете, как правильно разделять код на модули, подключать локальные зависимости и управлять сборкой.
1. Что такое Cargo Workspace и как его создать?
Рабочее пространство (workspace) — это набор крейтов (crates), которые используют один и тот же файл Cargo.lock и общую директорию target. Это позволяет избежать конфликтов версий зависимостей и ускоряет компиляцию.
Создание структуры монорепозитория
Создадим простой монорепозиторий с двумя крейтами: библиотекой utils и исполняемым файлом app.
mkdir my_monorepo
cd my_monorepo
cargo new utils --lib
cargo new app
Теперь создадим корневой файл Cargo.toml, который объявит workspace:
[workspace]
members = [
"utils",
"app",
]
Важно: Убедитесь, что в файлах utils/Cargo.toml и app/Cargo.toml нет секции [package] с полем workspace — она не нужна. Cargo сам поймет, что это часть workspace.
Структура проекта
После создания у вас получится такая структура:
my_monorepo/
├── Cargo.toml # корневой workspace
├── utils/
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
└── app/
├── Cargo.toml
└── src/
└── main.rs
2. Организация кода: библиотеки и модули
Теперь добавим код в библиотеку utils. Допустим, мы хотим создать общие функции для работы с числами и строками.
Код библиотеки (utils/src/lib.rs)
pub mod math;
pub mod strings;
Создадим файлы модулей:
// utils/src/math.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
// utils/src/strings.rs
pub fn greet(name: &str) -> String {
format!("Привет, {}!", name)
}
pub fn to_uppercase(text: &str) -> String {
text.to_uppercase()
}
Подключение библиотеки в исполняемом файле
В файле app/Cargo.toml укажем зависимость от локального крейта:
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
utils = { path = "../utils" }
Теперь в app/src/main.rs мы можем использовать функции из utils:
use utils::math;
use utils::strings;
fn main() {
let sum = math::add(5, 10);
println!("5 + 10 = {}", sum);
let greeting = strings::greet("Мир");
println!("{}", greeting);
let upper = strings::to_uppercase("rust rocks");
println!("{}", upper);
}
Запустите проект из корня монорепозитория командой:
cargo run -p app
Вы увидите вывод:
5 + 10 = 15
Привет, Мир!
RUST ROCKS
3. Расширенная организация: несколько исполняемых файлов
Часто в монорепозитории нужно иметь несколько исполняемых файлов. Например, сервер и клиент. Rust позволяет это сделать без создания отдельных папок.
Добавление второго бинарника
В папке app создайте подпапку src/bin и добавьте файл server.rs:
// app/src/bin/server.rs
fn main() {
println!("Сервер запущен!");
}
Теперь обновите app/Cargo.toml, указав пути к бинарникам:
[[bin]]
name = "client"
path = "src/main.rs"
[[bin]]
name = "server"
path = "src/bin/server.rs"
Теперь вы можете запускать каждый из них отдельно:
cargo run -p app --bin client
cargo run -p app --bin server
Совет: используйте условные зависимости
Если один из бинарников требует специфических зависимостей (например, tokio для сервера), вы можете указать их опционально:
[dependencies]
utils = { path = "../utils" }
tokio = { version = "1", optional = true }
[features]
server = ["tokio"]
И в коде используйте #[cfg(feature = "server")] для условной компиляции.
4. Продвинутые техники: вложенные workspace и профили
Вложенные workspace
Если ваш проект становится очень большим, можно создавать вложенные workspace. Например, для отдельной группы крейтов:
[workspace]
members = [
"utils",
"app",
"tools/*",
]
Здесь tools/* означает, что все подпапки внутри tools/ будут включены в workspace.
Профили сборки
Для ускорения разработки можно настроить профили в корневом Cargo.toml: