Монорепозиторий с Rust: организация кода для начинающих

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

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

Начать курс

Введение: Зачем нужен монорепозиторий в 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:

Блоги

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