Глава 16: Cargo в деталях

Содержание: Структура Cargo.toml: зависимости, метаданные Управление версиями зависимостей Профили сборки: release, debug Команды Cargo: build, run, test Примеры: настройка сложного проекта Управление итоговыми файлами и статическая линковка Упражнение: Добавить зависимость и использовать её Заключение

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

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

Что мы будем изучать в этой главе? Мы начнём с детального разбора структуры конфигурационного файла Cargo.toml, который лежит в основе каждого проекта Rust. Вы узнаете, как задавать метаданные, управлять зависимостями и настраивать фичи. Затем мы перейдём к управлению версиями зависимостей: как работает SemVer, зачем нужен Cargo.lock и как обновлять библиотеки. Далее разберём профили сборки — debug и release, включая тонкости оптимизации и управления итоговыми файлами, чтобы ваши бинарники попадали в чистую папку, готовую для деплоя.

После этого мы углубимся в команды Cargo: от базовых cargo build и cargo run до мощных cargo test и cargo doc. Вы узнаете, как писать тесты всех типов — юнит-тесты, интеграционные и документационные — и как генерировать профессиональную документацию с примерами. Мы также рассмотрим практические примеры настройки сложных проектов с несколькими бинарниками и библиотеками. Особое внимание уделим вопросам, которые часто возникают у разработчиков: как изолировать итоговый бинарник в отдельной папке (например, с помощью --out-dir) и как настроить статическую линковку для создания полностью независимых исполняемых файлов, например, с использованием таргета x86_64-unknown-linux-musl.

В конце главы вас ждёт практическое упражнение: вы добавите зависимость в проект и используете её в коде, закрепив полученные знания. Эта лекция построена так, чтобы быть максимально самодостаточной: каждый раздел содержит подробные объяснения, примеры кода с комментариями и практические советы. Мы будем двигаться от простого к сложному, с избыточным покрытием всех аспектов, чтобы вы могли не только понять, как работает Cargo, но и применять его в реальных проектах с учётом всех нюансов. Независимо от вашего уровня подготовки, эта глава даст вам глубокое понимание инструмента, который станет вашим лучшим помощником в мире Rust. Приготовьтесь к погружению — и давайте начнём!


1. Структура Cargo.toml: зависимости, метаданные

Cargo.toml — это краеугольный камень любого проекта Rust. Этот файл, написанный в формате TOML (Tom’s Obvious, Minimal Language), определяет всё: от имени и версии проекта до списка зависимостей и настроек сборки. TOML — это простой, но строгий язык конфигурации, который сочетает читаемость с точностью. В этом разделе мы разберём каждую секцию Cargo.toml с примерами, объяснениями и практическими советами. Вы узнаете, как правильно настроить проект, чтобы он был не только функциональным, но и готовым к публикации или совместной разработке. Погрузимся в детали!

Секция [package]

Секция [package] — это метаданные вашего проекта. Она обязательна, так как Cargo использует её для идентификации проекта, генерации бинарников и публикации на crates.io. Здесь вы задаёте основные характеристики, которые делают ваш проект уникальным и понятным для других разработчиков.

Основные поля:

Дополнительные поля:

Пример минимального Cargo.toml:

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

Этот пример создаёт базовый проект с именем simple_app, версией 0.1.0 и редакцией 2021. Cargo сгенерирует бинарник с таким же именем при сборке.

Пример полного Cargo.toml:

[package]
name = "complex_app"
version = "0.2.3"
edition = "2021"
authors = ["Алексей Петров <alex@example.com>", "Мария Иванова <maria@example.com>"]
description = "Многофункциональное приложение на Rust"
license = "MIT OR Apache-2.0"
homepage = "https://example.com/complex_app"
repository = "https://github.com/user/complex_app"
readme = "README.md"
keywords = ["rust", "cargo", "utility"]
categories = ["command-line-utilities", "development-tools"]

Здесь мы добавили все возможные метаданные. Такой файл готов к публикации на crates.io: он информативен, содержит лицензию и ссылки на ресурсы. Поле license = "MIT OR Apache-2.0" указывает двойное лицензирование, что популярно в Rust-сообществе.

Комментарии и практические советы:

Секция [dependencies]

Секция [dependencies] перечисляет внешние библиотеки (crates), необходимые для работы вашего проекта. Это ядро функциональности, которое вы добавляете к своему коду.

Формат записи:

Пример с пояснениями:

[dependencies]
serde = "1.0.152"                       # Точная версия для сериализации
rand = { version = "0.8.5", features = ["small_rng"] }  # С фичами для компактного RNG
tokio = { version = "1.20", optional = true }  # Опциональная асинхронная библиотека
my_local_crate = { path = "../my_local_crate" }  # Локальная зависимость
experimental = { git = "https://github.com/user/experimental", branch = "dev" }  # Из git

Объяснение параметров:

Cargo автоматически загружает зависимости из реестра crates.io, если не указан path или git. Версии обрабатываются с учётом SemVer, о чём мы поговорим в следующем разделе.

Секция [dev-dependencies]

Секция [dev-dependencies] предназначена для зависимостей, используемых только во время разработки — для тестов, бенчмарков или утилит. Они не включаются в финальный бинарник при сборке с --release.

Пример и назначение:

[dev-dependencies]
criterion = "0.4.0"        # Для бенчмарков производительности
pretty_assertions = "1.3"  # Улучшенные сообщения об ошибках в тестах

Здесь criterion используется для измерения производительности кода, а pretty_assertions улучшает вывод ошибок в тестах, показывая различия между ожидаемым и фактическим результатом.

Секция [build-dependencies]

Секция [build-dependencies] перечисляет зависимости, необходимые для скрипта сборки build.rs. Этот скрипт выполняется перед основной компиляцией и может генерировать код или выполнять другие задачи.

Пример и назначение:

[build-dependencies]
cc = "1.0.79"      # Компиляция C-кода
bindgen = "0.65"   # Генерация биндингов для FFI

cc позволяет компилировать C-код, а bindgen генерирует Rust-биндинги для C-библиотек. Используется, например, для интеграции с внешними системами через FFI.

Секция [features]

Секция [features] определяет пользовательские фичи для условной компиляции. Это позволяет включать или отключать части кода и зависимости в зависимости от нужд проекта.

Пример и использование:

[features]
default = ["basic"]    # Фичи по умолчанию
basic = []             # Пустая фича (флаг)
async = ["tokio"]      # Включает tokio

[dependencies]
tokio = { version = "1.20", optional = true }

Здесь default активирует фичу basic при сборке без указания фич. Фича async включает tokio. Запуск с фичей:

cargo build --features async

Практические советы для всех секций


2. Управление версиями зависимостей

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

Описание SemVer (MAJOR.MINOR.PATCH)

SemVer — это стандарт версионирования, принятый в Rust и многих других экосистемах. Он состоит из трёх чисел, разделённых точками: MAJOR.MINOR.PATCH. Например, версия 1.2.3 означает:

SemVer позволяет Cargo автоматически определять, какие обновления безопасны. Например, если вы указали serde = "1.0.0", то обновление до 1.0.1 (патч) или 1.1.0 (минор) считается совместимым, а до 2.0.0 — нет, так как это может сломать ваш код. Понимание SemVer — это основа для работы с зависимостями в Rust.

Указание версий ("1.0.0", "^1.0.0", "~1.0.0", диапазоны)

В Cargo.toml вы указываете версии зависимостей с помощью спецификаторов. Cargo поддерживает несколько форматов, которые дают вам гибкость в выборе подходящих версий.

Каждый спецификатор подходит для разных случаев. Например, "^1.0.0" хорош для большинства зависимостей, так как позволяет получать новые функции и исправления, сохраняя совместимость. "~1.0.0" полезен, если вы хотите только исправления ошибок, а "1.0.0" — если нужна строгая фиксация.

Пример в Cargo.toml

Вот как это выглядит в реальном Cargo.toml:

[dependencies]
serde = "^1.0"        # Любая совместимая версия от 1.0.0 до <2.0.0
rand = "~0.8.5"       # Только патчи: от 0.8.5 до <0.9.0
tokio = ">=1.15, <1.25"  # Диапазон версий
log = "0.4.17"        # Точная версия

В этом примере serde может обновляться до 1.1.0, но не до 2.0.0. rand ограничен патчами (0.8.6 подойдёт, 0.9.0 — нет). tokio находится в диапазоне, а log зафиксирован на 0.4.17.

Файл Cargo.lock: назначение, пример, правила использования

Cargo.lock — это файл, который фиксирует точные версии всех зависимостей (включая транзитивные) после первой сборки проекта. Он генерируется автоматически при выполнении cargo build или cargo update и обеспечивает воспроизводимость сборки.

Назначение:

Пример Cargo.lock:

[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
 "libc 0.2.139",
 "rand_core 0.6.4",
]

Здесь зафиксирована версия rand (0.8.5) с её зависимостями и контрольной суммой для проверки целостности.

Правила использования:

Чтобы проверить содержимое:

cat Cargo.lock | grep rand

Обновление зависимостей (cargo update, -p)

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

Пример:

$ cat Cargo.lock | grep rand
version = "0.8.4"
$ cargo update
$ cat Cargo.lock | grep rand
version = "0.8.5"

Здесь rand обновился с 0.8.4 до 0.8.5, так как это совместимо с "~0.8.5". Для конкретного crate:

cargo update -p rand

Инструменты анализа (cargo tree, cargo outdated)

Cargo предоставляет дополнительные утилиты для анализа зависимостей.

Эти инструменты помогают выявлять конфликты версий и поддерживать проект в актуальном состоянии.

Практические советы


3. Профили сборки: release, debug

Cargo поддерживает профили сборки, которые определяют, как компилятор rustc обрабатывает ваш код. Профили — это способ настроить баланс между скоростью компиляции, производительностью бинарника и удобством отладки. По умолчанию Cargo предлагает два основных профиля: debug для разработки и release для продакшена. В этом разделе мы разберём их особенности, настройки и управление итоговыми файлами, чтобы вы могли собирать бинарники так, как вам нужно — вплоть до изоляции их в чистую папку для деплоя. Погрузимся в детали!

Профиль debug: особенности, пример

Профиль debug используется по умолчанию, когда вы запускаете cargo build или cargo run без дополнительных флагов. Его главная цель — обеспечить быструю компиляцию и удобство отладки, жертвуя производительностью.

Особенности:

Пример:

$ cargo build
   Compiling my_app v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
$ ls target/debug/
my_app  my_app.d

Здесь my_app — это скомпилированный бинарник, а my_app.d — файл с метаданными для отладки. Размер бинарника будет больше из-за отсутствия оптимизаций:

$ du -h target/debug/my_app
4.2M    target/debug/my_app

Профиль debug идеален для разработки: вы быстро проверяете изменения и можете использовать отладчик для поиска ошибок.

Профиль release: особенности, пример

Профиль release активируется с флагом --release (cargo build --release). Его цель — создать максимально оптимизированный бинарник для продакшена, жертвуя скоростью компиляции.

Особенности:

Пример:

$ cargo build --release
   Compiling my_app v0.1.0
    Finished release [optimized] target(s) in 1.45s
$ ls target/release/
my_app  my_app.d  deps/  incremental/

Бинарник my_app меньше и быстрее, чем в debug:

$ du -h target/release/my_app
1.8M    target/release/my_app

Однако в target/release/ появляются дополнительные файлы (deps/, incremental/), что может быть неудобно для деплоя. Мы разберём это ниже в разделе управления итоговыми файлами.

Настройка профилей в Cargo.toml (opt-level, lto, codegen-units, strip)

Вы можете переопределить настройки профилей в Cargo.toml, чтобы адаптировать их под свои нужды. Это делается в секциях [profile.dev] и [profile.release].

Пример:

[profile.dev]
opt-level = 1          # Лёгкая оптимизация даже в debug
debug-assertions = true  # Включить проверки

[profile.release]
opt-level = 3          # Максимальная оптимизация
lto = "thin"           # Thin Link-Time Optimization
codegen-units = 1      # Минимум параллелизма для лучшей оптимизации
strip = "symbols"      # Удалить символы

Объяснение параметров:

С этими настройками release создаёт компактный и быстрый бинарник, а debug становится чуть быстрее благодаря opt-level = 1.

Управление итоговыми файлами

Одной из частых задач при сборке является управление итоговыми файлами. По умолчанию Cargo помещает бинарники в target/debug/ или target/release/, но эти папки содержат не только исполняемый файл, но и промежуточные артефакты, что неудобно для деплоя. Давайте разберём, как это исправить.

Проблема с target/release/

После cargo build --release в target/release/ оказываются:

Если вы хотите скопировать бинарник в другое место (например, для деплоя), приходится вручную выбирать только my_app, что неудобно.

Решение с --out-dir

Начиная с Rust 1.58, Cargo поддерживает флаг --out-dir, который копирует только финальный бинарник в указанную папку, оставляя промежуточные файлы в target/.

Пример:

$ cargo build --release --out-dir dist
$ ls dist/
my_app
$ ls target/release/
my_app  my_app.d  deps/  incremental/

Теперь dist/ содержит только my_app, готовый для копирования куда угодно. Папку dist нужно создать заранее, так как Cargo этого не делает.

Автоматизация через build.rs

Если вы хотите автоматизировать процесс, можно использовать скрипт сборки build.rs, который выполняется перед компиляцией.

Пример build.rs:

use std::fs;

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    if std::env::var("PROFILE").unwrap() == "release" {
        let binary_name = "my_app";  // Замените на имя вашего проекта
        let source = format!("target/release/{}", binary_name);
        let dest_dir = "dist";
        fs::create_dir_all(dest_dir).unwrap();
        fs::copy(&source, format!("{}/{}", dest_dir, binary_name)).unwrap();
    }
}

Использование:

$ cargo build --release
$ ls dist/
my_app

Скрипт проверяет, что сборка происходит в профиле release, создаёт папку dist и копирует туда бинарник. Добавьте build.rs в корень проекта и укажите имя бинарника, соответствующее name в Cargo.toml.

Практические советы


4. Команды Cargo: build, run, test

Cargo предоставляет набор команд, которые делают разработку на Rust удобной и эффективной. Эти команды покрывают всё: от компиляции кода до запуска тестов и генерации документации. В этом разделе мы разберём основные команды — cargo build, cargo run, cargo test и cargo doc — с примерами, флагами и нюансами. Мы также коснёмся дополнительных команд и дадим практические советы, чтобы вы могли использовать Cargo на полную мощность. Погружаемся в детали!

cargo build

Описание: Команда cargo build компилирует проект, создавая исполняемые файлы или библиотеки. Это основа работы с Cargo, которая переводит ваш код из исходников в бинарники.

Флаги:

По умолчанию сборка происходит в профиле debug, а бинарник попадает в target/debug/. С --release — в target/release/.

Пример:

$ cargo build
   Compiling my_app v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
$ ls target/debug/
my_app  my_app.d

$ cargo build --release --out-dir dist
   Compiling my_app v0.1.0
    Finished release [optimized] target(s) in 1.45s
$ ls dist/
my_app

В первом случае мы собрали проект в debug, во втором — в release с копированием бинарника в dist/. Флаг --out-dir упрощает деплой, изолируя итоговый файл.

cargo run

Описание: Команда cargo run компилирует проект и сразу запускает основной бинарник. Это удобный способ проверить код в действии без лишних шагов.

Флаги: Аналогичны cargo build:

Пример: Для проекта с src/main.rs:

fn main() {
    println!("Привет, мир!");
}
$ cargo run
   Compiling my_app v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/my_app`
Привет, мир!

$ cargo run --release
   Compiling my_app v0.1.0
    Finished release [optimized] target(s) in 1.45s
     Running `target/release/my_app`
Привет, мир!

Команда сначала компилирует, а затем выполняет my_app. В release запуск быстрее благодаря оптимизациям.

cargo test

Описание: Команда cargo test запускает все тесты в проекте: юнит-тесты, интеграционные и документационные. Это мощный инструмент для проверки корректности кода, который компилирует тесты в специальном режиме и выполняет их параллельно.

Типы тестов:

Примеры кода для каждого типа:

Юнит-тесты в src/lib.rs:

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn test_add_positive() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, -2), -3);
    }

    #[test]
    #[should_panic(expected = "overflow")]
    fn test_overflow() {
        add(i32::MAX, 1);
    }

    #[test]
    #[ignore = "too slow"]
    fn test_slow() {
        std::thread::sleep(std::time::Duration::from_secs(2));
        assert!(true);
    }
}

Интеграционные тесты в tests/integration.rs:

use my_app::add;

#[test]
fn test_integration() {
    assert_eq!(add(5, 7), 12);
}

Документационные тесты в src/lib.rs:

/// Adds two numbers.
/// 
/// ```rust
/// use my_app::add;
/// assert_eq!(add(2, 2), 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Флаги:

Пример запуска:

$ cargo test
running 4 tests
test tests::test_add_positive ... ok
test tests::test_add_negative ... ok
test tests::test_overflow ... ok
test tests::test_slow ... ignored

running 1 test
test test_integration ... ok

Doc-tests my_app
running 1 test
test src/lib.rs - add (line 5) ... ok

test result: ok. 5 passed; 0 failed; 1 ignored

$ cargo test -- --nocapture

Здесь тесты проходят, а test_slow пропущен из-за #[ignore]. С --nocapture вы увидите вывод, если добавите println!.

Использование зависимостей (например, pretty_assertions):

Добавим в Cargo.toml:

[dev-dependencies]
pretty_assertions = "1.3"

Изменим тест:

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

    #[test]
    fn test_add_pretty() {
        assert_eq!(add(2, 2), 5);  // Показывает diff при ошибке
    }
}

Если тест провалится, pretty_assertions выведет подробное сравнение ожидаемого и фактического результата.

cargo doc

Описание: Команда cargo doc генерирует HTML-документацию на основе комментариев в коде. Она сканирует /// (внешние комментарии) и //! (внутренние комментарии), создавая файлы в target/doc/.

Пример документированного кода: В src/lib.rs:


//! Библиотека для математических операций
//!
//! # Возможности
//! - Сложение чисел

/// Добавляет два числа и возвращает результат.
///
/// # Аргументы
/// * `a` - Первое число
/// * `b` - Второе число
///
/// # Примеры
/// ```rust
/// use my_app::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Генерация документации:

$ cargo doc
   Documenting my_app v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.45s
$ ls target/doc/
my_app  ...

Открыть в браузере:

$ cargo doc --open

Флаги:

Пример с приватными элементами:

$ cargo doc --document-private-items

Что происходит при использовании этого флага?

Допустим, у вас есть такой код в lib.rs:

/// Это публичная функция
pub fn public_function() {
    println!("Я публичный!");
}

/// Это приватная функция
fn private_function() {
    println!("Я приватный!");
}

Когда использовать?

Дополнительные параметры

Вы можете комбинировать --document-private-items с другими опциями cargo doc:

Другие команды

Практические советы


5. Примеры: настройка сложного проекта

До сих пор мы разбирали Cargo на уровне отдельных концепций: структуру Cargo.toml, профили сборки, команды и зависимости. Теперь пришло время собрать всё вместе и показать, как настроить сложный проект с несколькими бинарниками и библиотекой. В этом разделе мы создадим пример проекта — мини-приложение для обработки данных, которое будет включать библиотеку с общей логикой и два бинарника: сервер и клиент. Вы увидите, как организовать Cargo.toml, структуру проекта, код и как собрать и запустить всё это. Этот пример станет мостом между теорией и практикой, демонстрируя возможности Cargo в реальном сценарии. Погружаемся!

Пример Cargo.toml с несколькими бинарниками

Для сложного проекта с несколькими исполняемыми файлами мы используем секцию [[bin]] в Cargo.toml. Это позволяет указать несколько бинарников, каждый со своим именем и исходным файлом. Также добавим библиотеку и зависимости.

Пример Cargo.toml:

[package]
name = "data_processor"
version = "0.1.0"
edition = "2021"
description = "Приложение для обработки данных с сервером и клиентом"
authors = ["Ваше Имя "]
license = "MIT"

[lib]
name = "processor_lib"
path = "src/lib.rs"

[[bin]]
name = "server"
path = "src/bin/server.rs"

[[bin]]
name = "client"
path = "src/bin/client.rs"

[dependencies]
serde = { version = "1.0", features = ["derive"] }  # Сериализация данных
tokio = { version = "1.20", features = ["full"], optional = true }  # Асинхронность

[features]
default = ["async"]
async = ["tokio"]

Объяснение:

Этот Cargo.toml задаёт проект с общей библиотекой и двумя бинарниками, которые могут использовать её функциональность.

Структура проекта

Структура проекта должна соответствовать настройкам в Cargo.toml. Вот как будет выглядеть дерево файлов:

data_processor/
├── Cargo.toml
├── src/
│   ├── lib.rs          # Общая библиотека
│   ├── bin/
│   │   ├── server.rs   # Бинарник сервера
│   │   └── client.rs   # Бинарник клиента

Объяснение:

Такая структура типична для проектов с несколькими бинарниками в Rust. Библиотека в lib.rs позволяет вынести общий код, избегая дублирования.

Код для lib.rs и бинарников

Теперь добавим код, чтобы показать, как всё работает вместе.

src/lib.rs:

//! Библиотека для обработки данных

use serde::{Serialize, Deserialize};

/// Структура для представления данных
#[derive(Serialize, Deserialize, Debug)]
pub struct Data {
    id: u32,
    value: String,
}

/// Создаёт новый объект данных
pub fn create_data(id: u32, value: &str) -> Data {
    Data {
        id,
        value: value.to_string(),
    }
}

/// Сериализует данные в JSON
pub fn to_json(data: &Data) -> String {
    serde_json::to_string(data).unwrap_or_else(|e| format!("Ошибка: {}", e))
}

Эта библиотека определяет структуру Data и функции для работы с ней. Она использует serde для сериализации в JSON.

src/bin/server.rs:

#[tokio::main]
async fn main() {
    let data = processor_lib::create_data(1, "Серверные данные");
    let json = processor_lib::to_json(&data);
    println!("Сервер отправляет: {}", json);
}

Сервер использует tokio для асинхронного выполнения и библиотеку processor_lib для создания и сериализации данных.

src/bin/client.rs:

fn main() {
    let data = processor_lib::create_data(2, "Клиентские данные");
    let json = processor_lib::to_json(&data);
    println!("Клиент получил: {}", json);
}

Клиент работает синхронно, демонстрируя, что библиотека универсальна для разных подходов.

Сборка и запуск

Теперь соберём и запустим проект, чтобы увидеть всё в действии.

Сборка:

$ cargo build
   Compiling data_processor v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.23s
$ ls target/debug/
client  client.d  server  server.d  ...

В target/debug/ появляются два бинарника: client и server. Библиотека компилируется автоматически как зависимость.

Запуск:

$ cargo run --bin server
   Compiling data_processor v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.23s
     Running `target/debug/server`
Сервер отправляет: {"id":1,"value":"Серверные данные"}

$ cargo run --bin client
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/client`
Клиент получил: {"id":2,"value":"Клиентские данные"}

Флаг --bin имя указывает, какой бинарник запустить. Второй запуск быстрее благодаря инкрементальной компиляции.

Сборка для релиза с изоляцией:

$ cargo build --release --out-dir dist
   Compiling data_processor v0.1.0
    Finished release [optimized] target(s) in 2.34s
$ ls dist/
client  server

С --out-dir dist оба бинарника попадают в чистую папку dist/, готовые для деплоя.

Запуск из dist/:

$ ./dist/server
Сервер отправляет: {"id":1,"value":"Серверные данные"}
$ ./dist/client
Клиент получил: {"id":2,"value":"Клиентские данные"}

Теперь бинарники можно копировать куда угодно без лишних файлов.


6. Управление итоговыми файлами и статическая линковка

После сборки проекта в Rust итоговые файлы — бинарники — часто нужно подготовить для деплоя или запуска на других системах. Однако стандартный процесс оставляет их в папке target/ вместе с промежуточными артефактами, а динамическая линковка может создавать зависимости от системных библиотек. В этом разделе мы разберём два ключевых аспекта: как изолировать итоговые бинарники в чистую папку и как настроить статическую линковку для создания полностью независимых исполняемых файлов. Мы рассмотрим проблемы, решения, примеры и автоматизацию, чтобы вы могли гибко управлять результатами сборки. Погружаемся в детали!

Смена папки сборки

По умолчанию Cargo помещает бинарники в target/debug/ или target/release/, но эти папки содержат не только исполняемые файлы, что усложняет их перенос. Давайте разберём, как это исправить.

Проблема с target/release/

После выполнения cargo build --release в target/release/ вы увидите:

Пример:

$ cargo build --release
   Compiling my_app v0.1.0
    Finished release [optimized] target(s) in 1.45s
$ ls target/release/
my_app  my_app.d  deps/  incremental/

Если вы хотите скопировать бинарник для деплоя, вам нужно вручную извлечь только my_app, игнорируя остальные файлы. Это неудобно, особенно при автоматизации.

Решение с --out-dir

Cargo (с Rust 1.58) предлагает флаг --out-dir, который копирует только финальные бинарники в указанную папку, оставляя промежуточные файлы в target/. Это идеальное решение для изоляции результатов сборки.

Пример:

$ mkdir dist  # Создаём папку заранее
$ cargo build --release --out-dir dist
   Compiling my_app v0.1.0
    Finished release [optimized] target(s) in 1.45s
$ ls dist/
my_app
$ ls target/release/
my_app  my_app.d  deps/  incremental/

Теперь dist/ содержит только my_app, готовый для копирования. Обратите внимание: Cargo не создаёт dist/ автоматически, так что используйте mkdir или скрипт.

Для нескольких бинарников (как в предыдущем разделе):

$ cargo build --release --out-dir dist
$ ls dist/
client  server

Автоматизация через build.rs

Если вы хотите встроить изоляцию в процесс сборки, используйте скрипт build.rs. Он выполняется перед компиляцией и может копировать бинарники автоматически.

Пример build.rs:

use std::fs;

fn main() {
    println!("cargo:rerun-if-changed=build.rs");  // Перезапуск при изменении скрипта
    if std::env::var("PROFILE").unwrap() == "release" {
        let binary_name = "my_app";  // Замените на имя вашего проекта
        let source = format!("target/release/{}", binary_name);
        let dest_dir = "dist";
        fs::create_dir_all(dest_dir).unwrap();
        fs::copy(&source, format!("{}/{}", dest_dir, binary_name)).unwrap();
    }
}

Поместите этот файл в корень проекта. Теперь при сборке:

$ cargo build --release
   Compiling my_app v0.1.0
    Finished release [optimized] target(s) in 1.45s
$ ls dist/
my_app

Скрипт проверяет профиль (release), создаёт dist/ и копирует туда бинарник. Для нескольких бинарников добавьте их имена вручную или используйте переменные окружения Cargo.

Примеры

Ручной подход с копированием:

$ cargo build --release && mkdir -p dist && cp target/release/my_app dist/
$ ls dist/
my_app

Скрипт деплоя:

#!/bin/bash
cargo build --release --out-dir dist
echo "Бинарники в dist/:"
ls dist/

Сохраните как deploy.sh, сделайте исполняемым (chmod +x deploy.sh) и запускайте: ./deploy.sh.

Статическая линковка

Статическая линковка встраивает все зависимости (включая стандартную библиотеку и внешние библиотеки) в бинарник, делая его независимым от системы. Это полезно для деплоя на старые системы или в контейнеры вроде scratch.

Статическая линковка стандартной библиотеки (x86_64-unknown-linux-musl)

По умолчанию Rust использует динамическую линковку со стандартной библиотекой (libstd), которая зависит от системной libc (например, glibc). Для статической линковки используйте таргет x86_64-unknown-linux-musl, основанный на musl — легковесной статической C-библиотеке.

Шаги:

Бинарник теперь полностью статический. Проверим:

$ ldd target/x86_64-unknown-linux-musl/release/my_app
    not a dynamic executable

Это значит, что он не требует внешних библиотек и будет работать на любой совместимой системе.

Статическая линковка C-зависимостей (например, openssl с vendored)

Если проект использует crates с C-зависимостями (например, openssl), они по умолчанию линкуются динамически. Для статической линковки используйте фичу vendored, которая заставляет Cargo компилировать библиотеку самостоятельно.

Пример Cargo.toml:

[dependencies]
openssl = { version = "0.10", features = ["vendored"] }

Сборка:

$ cargo build --release --target x86_64-unknown-linux-musl
$ ldd target/x86_64-unknown-linux-musl/release/my_app
    not a dynamic executable

Фича vendored загружает и компилирует OpenSSL статически. Это требует build-dependencies вроде cc:

[build-dependencies]
cc = "1.0"

Настройка в .cargo/config.toml

Для упрощения можно задать настройки в .cargo/config.toml в корне проекта:

[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
rustflags = ["-C", "link-arg=-static"]

Это указывает компоновщик и флаги для статической линковки. Теперь просто:

$ cargo build --release --target x86_64-unknown-linux-musl

Примеры и проверка (ldd)

Полный процесс:

$ rustup target add x86_64-unknown-linux-musl
$ cargo build --release --target x86_64-unknown-linux-musl --out-dir dist
$ ls dist/
my_app
$ ldd dist/my_app
    not a dynamic executable
$ du -h dist/my_app
2.3M    dist/my_app

Сравним с динамической сборкой:

$ cargo build --release --out-dir dist_dynamic
$ ldd dist_dynamic/my_app
    linux-vdso.so.1 ...
    libc.so.6 ...
$ du -h dist_dynamic/my_app
1.8M    dist_dynamic/my_app

Статический бинарник больше, но независим. Перенесите его на другую машину и проверьте:

$ scp dist/my_app user@remote:/tmp
$ ssh user@remote /tmp/my_app

Практические советы


7. Упражнение: Добавить зависимость и использовать её

Теория — это важно, но настоящие навыки приходят с практикой. В этом разделе мы предлагаем вам небольшое упражнение, которое поможет закрепить знания о работе с зависимостями в Cargo. Вы добавите внешнюю библиотеку в проект, подключите её в коде и проверите результат. Это простая задача, но она охватывает ключевые шаги: настройку Cargo.toml, импорт зависимостей и их использование в Rust. Давайте применим всё, что мы изучили, на практике — и заодно немного повеселимся с генерацией случайных чисел!

Задание: добавить rand и сгенерировать число

Ваша цель — создать простое приложение, которое генерирует случайное число в заданном диапазоне и выводит его на экран. Для этого нужно:

Библиотека rand — это стандартный инструмент в Rust для работы со случайными числами. Она предоставляет удобные функции для генерации случайных значений, и мы будем использовать её базовую функциональность. Если у вас ещё нет проекта, создайте его с помощью cargo new random_app --bin и следуйте инструкциям ниже.

Решение: изменения в Cargo.toml и код

Давайте шаг за шагом решим задачу, добавив зависимость и написав код.

Шаг 1: Изменения в Cargo.toml

Откройте Cargo.toml и добавьте rand в секцию [dependencies]. Мы будем использовать версию 0.8.5, которая стабильна и широко поддерживается.

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

[dependencies]
rand = "0.8.5"  # Библиотека для генерации случайных чисел

После этого сохраните файл. Cargo автоматически загрузит rand и её зависимости при следующей сборке. Мы указали точную версию "0.8.5", но можно использовать "^0.8" для совместимых обновлений — это зависит от ваших предпочтений (см. раздел 3 о версиях).

Шаг 2: Код в src/main.rs

Теперь заменим содержимое src/main.rs на код, который использует rand для генерации случайного числа:

use rand::Rng;  // Подключаем трейт Rng для генерации чисел

fn main() {
    // Создаём генератор случайных чисел для текущего потока
    let mut rng = rand::thread_rng();
    
    // Генерируем случайное число от 1 до 100 (включительно)
    let number = rng.gen_range(1..=100);
    
    println!("Случайное число: {}", number);
}

Сохраните файл. Этот код:

Шаг 3: Сборка и запуск

Выполните команды в терминале:

$ cargo run
   Compiling rand v0.8.5
   Compiling random_app v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.67s
     Running `target/debug/random_app`
Случайное число: 42

При первом запуске Cargo загрузит rand, скомпилирует проект и выведет случайное число (ваше число будет отличаться!). Повторный запуск будет быстрее благодаря кешированию:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/random_app`
Случайное число: 87

Разбор

Давайте разберём, что мы сделали, и как это связано с изученным материалом.

Добавление зависимости:

Использование в коде:

Сборка и запуск:

Расширения (для любопытных):

Это упражнение — маленький, но важный шаг к освоению Cargo. Вы научились добавлять зависимости, подключать их в коде и видеть результат. Теперь вы можете экспериментировать с другими библиотеками, например, serde для JSON или tokio для асинхронности, применяя тот же подход.


8. Заключение

Мы подошли к финалу главы, и это был насыщенный путь через одну из самых важных частей экосистемы Rust — Cargo. За это время мы разобрали инструмент до мельчайших деталей, превратив его из "чёрного ящика" в понятного и мощного помощника. Давайте подведём итоги того, что мы изучили, и посмотрим, как применить эти знания в ваших будущих проектах. А затем я приглашу вас не останавливаться на теории, а перейти к практике, чтобы закрепить всё, что вы узнали. Поехали!

Итоги главы

Эта глава была настоящим глубоким погружением в Cargo, и мы охватили всё, что нужно для уверенной работы с проектами на Rust — от базовых настроек до продвинутых техник. Давайте вспомним ключевые моменты:

Каждый из этих разделов — это кирпичик в фундаменте вашего мастерства работы с Cargo. Мы не просто прошлись по поверхности, а углубились в детали: от синтаксиса TOML до проверки статической линковки с ldd. Теперь у вас есть полное представление о том, как Cargo управляет проектами, и вы можете настроить его под любые нужды — от маленьких скриптов до крупных приложений.

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

Призыв к практике

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

Не бойтесь ошибаться — компилятор Rust и Cargo помогут вам найти и исправить проблемы. Каждый запуск cargo build, каждая строка в Cargo.toml — это шаг к тому, чтобы стать уверенным разработчиком. Попробуйте что-то своё: может быть, утилиту для командной строки или сервер на tokio. Главное — начните!

Эта глава — не конец, а начало. Cargo — это инструмент, который будет с вами на всём пути изучения Rust, и теперь у вас есть знания, чтобы использовать его эффективно. Откройте терминал, создайте новый проект с cargo new и дайте волю своему творчеству. Мы прошли теорию вместе, а практика — за вами. Удачи в ваших Rust-приключениях, и до встречи в следующей главе!