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. Приготовьтесь к погружению — и давайте начнём!
Cargo.toml: зависимости, метаданные
Cargo.toml — это краеугольный камень любого проекта Rust. Этот файл, написанный в формате TOML (Tom’s Obvious, Minimal Language), определяет всё: от имени и версии проекта до списка зависимостей и настроек сборки. TOML — это простой, но строгий язык конфигурации, который сочетает читаемость с точностью. В этом разделе мы разберём каждую секцию Cargo.toml с примерами, объяснениями и практическими советами. Вы узнаете, как правильно настроить проект, чтобы он был не только функциональным, но и готовым к публикации или совместной разработке. Погрузимся в детали!
[package]
Секция [package] — это метаданные вашего проекта. Она обязательна, так как Cargo использует её для идентификации проекта, генерации бинарников и публикации на crates.io. Здесь вы задаёте основные характеристики, которые делают ваш проект уникальным и понятным для других разработчиков.
Основные поля:
name: Уникальное имя проекта. Должно быть валидным идентификатором Rust (буквы, цифры, _, -, без пробелов). Определяет имя бинарника или crate.version: Версия в формате SemVer (MAJOR.MINOR.PATCH, например, 0.1.0). Указывает текущую стадию разработки.edition: Редакция Rust (2015, 2018, 2021). Определяет синтаксис и возможности языка. Рекомендуется 2021 для новых проектов.Дополнительные поля:
authors: Список авторов (например, ["Иван Иванов <ivan@example.com>"]). Опционально, но полезно для документации.description: Краткое описание проекта. Важно для crates.io.license: Лицензия (например, MIT, Apache-2.0). Обязательно для публикации.homepage: URL домашней страницы.repository: URL репозитория (например, GitHub).readme: Путь к файлу README (например, README.md).keywords: Список до 5 ключевых слов для поиска на crates.io.categories: Категории crates.io (например, ["utilities"]).
Пример минимального 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-сообществе.
Комментарии и практические советы:
name на crates.io перед публикацией.edition = "2021", чтобы получить доступ к новым возможностям, например, улучшенным замыканиям.readme и repository для удобства пользователей.name = "my_app" # Имя проекта, используется как имя бинарника
version = "0.1.0" # Начальная версия
[dependencies]
Секция [dependencies] перечисляет внешние библиотеки (crates), необходимые для работы вашего проекта. Это ядро функциональности, которое вы добавляете к своему коду.
Формат записи:
имя = "версия".имя = { version = "версия", features = [...], optional = true/false, path = "путь", git = "URL" }.Пример с пояснениями:
[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
Объяснение параметров:
serde = "1.0.152": Указывает точную версию библиотеки для сериализации данных. Cargo загрузит именно её.rand = { version = "0.8.5", features = ["small_rng"] }: Версия 0.8.5 с включённой фичей small_rng, которая добавляет компактный генератор случайных чисел.tokio = { version = "1.20", optional = true }: Зависимость включается только при активации соответствующей фичи через [features].path = "../my_local_crate": Указывает на локальный crate в соседней директории. Полезно для разработки.git = "https://github.com/user/experimental", branch = "dev": Загружает crate из git-репозитория, используя ветку dev. Можно указать tag или rev вместо branch.
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
edition, чтобы избежать проблем совместимости.serde = "1.0" # Сериализация и десериализация данных
optional и [features] для гибкости.cargo check.path.
Управление версиями зависимостей — это одна из ключевых задач Cargo, которая обеспечивает стабильность и совместимость вашего проекта. В Rust используется стандарт Semantic Versioning (SemVer), который позволяет разработчикам точно указывать, какие версии библиотек нужны, и как они могут обновляться. В этом разделе мы разберём, как работает SemVer, как задавать версии в Cargo.toml, зачем нужен Cargo.lock, как обновлять зависимости и какие инструменты помогут анализировать их состояние. Мы углубимся в детали, чтобы вы могли уверенно управлять зависимостями в проектах любого масштаба.
MAJOR.MINOR.PATCH)
SemVer — это стандарт версионирования, принятый в Rust и многих других экосистемах. Он состоит из трёх чисел, разделённых точками: MAJOR.MINOR.PATCH. Например, версия 1.2.3 означает:
MAJOR (1): Основная версия. Увеличивается при несовместимых изменениях в API, которые требуют адаптации кода (например, удаление функции).MINOR (2): Малая версия. Увеличивается при добавлении новой функциональности, совместимой с предыдущими версиями (например, новая функция).PATCH (3): Исправление. Увеличивается при исправлении ошибок без изменения API.
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": Точная версия. Cargo загрузит именно 1.0.0, без обновлений."^1.0.0": Совместимые обновления. Означает >=1.0.0, <2.0.0. Это поведение по умолчанию в Cargo. Например, 1.0.1 или 1.1.0 подойдут, но 2.0.0 — нет."~1.0.0": Только патчи. Означает >=1.0.0, <1.1.0. Подходят 1.0.1, 1.0.2, но не 1.1.0.">=1.0, <2.0": Диапазон версий. Указывает явные границы (например, от 1.0.0 до 1.999.999)."1.*": Любая версия в пределах 1.x.x. Не рекомендуется из-за риска несовместимости.
Каждый спецификатор подходит для разных случаев. Например, "^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) с её зависимостями и контрольной суммой для проверки целостности.
Правила использования:
Cargo.lock в git. Это обеспечивает одинаковую сборку на всех машинах.Cargo.lock из git (добавьте в .gitignore). Это позволяет пользователям вашей библиотеки выбирать совместимые версии зависимостей.Чтобы проверить содержимое:
cat Cargo.lock | grep rand
cargo update, -p)
Cargo позволяет обновлять зависимости в пределах указанных в Cargo.toml ограничений.
cargo update: Обновляет все зависимости до последних совместимых версий, переписывая Cargo.lock.cargo update -p имя: Обновляет только указанный crate.Пример:
$ 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 предоставляет дополнительные утилиты для анализа зависимостей.
cargo tree: Показывает дерево зависимостей.cargo install cargo-tree.cargo tree -p serde
Выводит зависимости serde, включая транзитивные.
cargo outdated: Показывает устаревшие зависимости.cargo install cargo-outdated.cargo outdated
Выводит список crates с доступными новыми версиями.
Эти инструменты помогают выявлять конфликты версий и поддерживать проект в актуальном состоянии.
cargo update, но проверяйте изменения в Cargo.lock перед коммитом."^1.0" для большинства зависимостей, чтобы получать обновления."1.0.0") для критически важных библиотек."*" — это может привести к несовместимости.cargo tree перед релизом, чтобы избежать дубликатов.cargo outdated для планирования обновлений.release, debug
Cargo поддерживает профили сборки, которые определяют, как компилятор rustc обрабатывает ваш код. Профили — это способ настроить баланс между скоростью компиляции, производительностью бинарника и удобством отладки. По умолчанию Cargo предлагает два основных профиля: debug для разработки и release для продакшена. В этом разделе мы разберём их особенности, настройки и управление итоговыми файлами, чтобы вы могли собирать бинарники так, как вам нужно — вплоть до изоляции их в чистую папку для деплоя. Погрузимся в детали!
debug: особенности, пример
Профиль debug используется по умолчанию, когда вы запускаете cargo build или cargo run без дополнительных флагов. Его главная цель — обеспечить быструю компиляцию и удобство отладки, жертвуя производительностью.
Особенности:
opt-level = 0: Минимальная оптимизация. Код компилируется быстро, но работает медленно.debug = true: Включает отладочную информацию (например, символы для отладчиков вроде gdb).debug-assertions = true: Включает проверки assert! в коде.target/debug/.Пример:
$ 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). Его цель — создать максимально оптимизированный бинарник для продакшена, жертвуя скоростью компиляции.
Особенности:
opt-level = 3: Максимальная оптимизация. Код работает быстро, но компиляция занимает больше времени.debug = false: Отладочная информация отключена по умолчанию.debug-assertions = false: Проверки assert! удаляются.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 меньше и быстрее, чем в 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" # Удалить символы
Объяснение параметров:
opt-level: Уровень оптимизации.
0: Без оптимизаций (быстрая компиляция).1: Базовая оптимизация.2: Умеренная оптимизация.3: Максимальная оптимизация."s": Оптимизация для размера."z": Минимальный размер.lto: Link-Time Optimization (оптимизация на этапе линковки).
false: Отключено."thin": Быстрая, но менее агрессивная оптимизация."fat": Максимальная оптимизация, но медленная.codegen-units: Количество единиц компиляции. Меньше значение — лучше оптимизация, но дольше сборка. По умолчанию 16.strip: Удаление данных из бинарника.
"none": Ничего не убирать."debuginfo": Убрать отладочную информацию."symbols": Убрать все символы.
С этими настройками release создаёт компактный и быстрый бинарник, а debug становится чуть быстрее благодаря opt-level = 1.
Одной из частых задач при сборке является управление итоговыми файлами. По умолчанию Cargo помещает бинарники в target/debug/ или target/release/, но эти папки содержат не только исполняемый файл, но и промежуточные артефакты, что неудобно для деплоя. Давайте разберём, как это исправить.
target/release/
После cargo build --release в target/release/ оказываются:
my_app: сам бинарник.my_app.d: файл зависимостей для отладки.deps/: папка с промежуточными файлами зависимостей.incremental/: кеш инкрементальной компиляции.
Если вы хотите скопировать бинарник в другое место (например, для деплоя), приходится вручную выбирать только 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.
debug для разработки, release для продакшена.opt-level = 1 в [profile.dev], если проект большой и тормозит в debug.lto = "thin" для баланса скорости и размера в release.--out-dir dist для финальных сборок, чтобы упростить деплой.build.rs, если вам нужен единый процесс для команды.du -h после изменений настроек.build, run, test
Cargo предоставляет набор команд, которые делают разработку на Rust удобной и эффективной. Эти команды покрывают всё: от компиляции кода до запуска тестов и генерации документации. В этом разделе мы разберём основные команды — cargo build, cargo run, cargo test и cargo doc — с примерами, флагами и нюансами. Мы также коснёмся дополнительных команд и дадим практические советы, чтобы вы могли использовать Cargo на полную мощность. Погружаемся в детали!
cargo build
Описание: Команда cargo build компилирует проект, создавая исполняемые файлы или библиотеки. Это основа работы с Cargo, которая переводит ваш код из исходников в бинарники.
Флаги:
--release: Использует профиль release для оптимизированной сборки.--out-dir путь: Копирует финальный бинарник в указанную папку (доступно с Rust 1.58).--target имя: Указывает целевой таргет для кросс-компиляции (например, x86_64-unknown-linux-musl).--verbose: Показывает подробный лог компиляции.
По умолчанию сборка происходит в профиле 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:
--release: Запуск в профиле release.--out-dir путь: Копирует бинарник в указанную папку перед запуском.--verbose: Подробный вывод.
Пример: Для проекта с 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 запускает все тесты в проекте: юнит-тесты, интеграционные и документационные. Это мощный инструмент для проверки корректности кода, который компилирует тесты в специальном режиме и выполняет их параллельно.
Типы тестов:
#[cfg(test)] внутри файла (обычно src/lib.rs или src/main.rs). Тестируют внутреннюю логику.tests/. Проверяют публичный API.```rust и ```), который автоматически тестируется.Примеры кода для каждого типа:
Юнит-тесты в 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
}
Флаги:
--nocapture: Показывает вывод println! из тестов.--ignored: Запускает тесты с #[ignore].--jobs N: Устанавливает количество параллельных тестов.--test имя: Запускает тесты из конкретного файла.Пример запуска:
$ 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
Флаги:
--no-deps: Генерирует документацию только для вашего проекта, игнорируя зависимости.--open: Открывает документацию в браузере.--document-private-items: Включает приватные элементы в документацию.Пример с приватными элементами:
$ cargo doc --document-private-items
pub будут задокументированы.pub (доступные только внутри модуля или крейта) также попадут в документацию.Допустим, у вас есть такой код в lib.rs:
/// Это публичная функция
pub fn public_function() {
println!("Я публичный!");
}
/// Это приватная функция
fn private_function() {
println!("Я приватный!");
}
--document-private-items: После выполнения cargo doc в документации будет только public_function.--document-private-items: После выполнения cargo doc --document-private-items в документации появятся оба метода: public_function и private_function.Вы можете комбинировать --document-private-items с другими опциями cargo doc:
--open: Открывает сгенерированную документацию в браузере сразу после создания:
cargo doc --document-private-items --open--no-deps: Исключает документацию зависимостей, фокусируясь только на вашем коде:
cargo doc --document-private-items --no-depscargo check: Проверяет код на ошибки без генерации бинарника. Быстрее, чем cargo build.
$ cargo check
cargo clean: Удаляет папку target/, очищая все артефакты.
$ cargo clean
cargo check для быстрой проверки синтаксиса.--release для финальных тестов производительности.cargo test.--nocapture для отладки тестов с выводом.--no-deps для ускорения в больших проектах.--out-dir в скриптах деплоя.
До сих пор мы разбирали 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"]
Объяснение:
[package]: Метаданные проекта. Имя data_processor — это общий идентификатор, но бинарники будут иметь свои имена.[lib]: Определяет библиотеку с именем processor_lib, исходники которой находятся в src/lib.rs.[[bin]]: Две секции для бинарников server и client. Каждый указывает путь к своему файлу в src/bin/.[dependencies]: serde для сериализации, tokio как опциональная зависимость для асинхронности.[features]: Фича async включает tokio, а default активирует её по умолчанию.
Этот Cargo.toml задаёт проект с общей библиотекой и двумя бинарниками, которые могут использовать её функциональность.
Структура проекта должна соответствовать настройкам в Cargo.toml. Вот как будет выглядеть дерево файлов:
data_processor/
├── Cargo.toml
├── src/
│ ├── lib.rs # Общая библиотека
│ ├── bin/
│ │ ├── server.rs # Бинарник сервера
│ │ └── client.rs # Бинарник клиента
Объяснение:
Cargo.toml: В корне проекта, как всегда.src/lib.rs: Файл библиотеки, содержащий общую логику.src/bin/: Папка для бинарников. Каждый файл здесь становится отдельным исполняемым файлом, если указан в [[bin]].
Такая структура типична для проектов с несколькими бинарниками в 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":"Клиентские данные"}
Теперь бинарники можно копировать куда угодно без лишних файлов.
После сборки проекта в Rust итоговые файлы — бинарники — часто нужно подготовить для деплоя или запуска на других системах. Однако стандартный процесс оставляет их в папке target/ вместе с промежуточными артефактами, а динамическая линковка может создавать зависимости от системных библиотек. В этом разделе мы разберём два ключевых аспекта: как изолировать итоговые бинарники в чистую папку и как настроить статическую линковку для создания полностью независимых исполняемых файлов. Мы рассмотрим проблемы, решения, примеры и автоматизацию, чтобы вы могли гибко управлять результатами сборки. Погружаемся в детали!
По умолчанию Cargo помещает бинарники в target/debug/ или target/release/, но эти папки содержат не только исполняемые файлы, что усложняет их перенос. Давайте разберём, как это исправить.
target/release/
После выполнения cargo build --release в target/release/ вы увидите:
my_app: Основной бинарник.my_app.d: Файл зависимостей для отладки.deps/: Папка с промежуточными файлами зависимостей.incremental/: Кеш инкрементальной компиляции.Пример:
$ 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-библиотеке.
Шаги:
$ rustup target add x86_64-unknown-linux-musl
На Linux может потребоваться musl-tools: sudo apt install musl-tools.
$ cargo build --release --target x86_64-unknown-linux-musl
Finished release [optimized] target(s) in 2.12s
$ ls target/x86_64-unknown-linux-musl/release/
my_app ...
Бинарник теперь полностью статический. Проверим:
$ ldd target/x86_64-unknown-linux-musl/release/my_app
not a dynamic executable
Это значит, что он не требует внешних библиотек и будет работать на любой совместимой системе.
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
--out-dir для финальных сборок, чтобы не копировать вручную.build.rs в проекты с несколькими бинарниками для автоматизации.musl для контейнеров или старых систем.ldd перед деплоем.strip.--out-dir и --target в скриптах деплоя:
cargo build --release --target x86_64-unknown-linux-musl --out-dir dist
Теория — это важно, но настоящие навыки приходят с практикой. В этом разделе мы предлагаем вам небольшое упражнение, которое поможет закрепить знания о работе с зависимостями в Cargo. Вы добавите внешнюю библиотеку в проект, подключите её в коде и проверите результат. Это простая задача, но она охватывает ключевые шаги: настройку Cargo.toml, импорт зависимостей и их использование в Rust. Давайте применим всё, что мы изучили, на практике — и заодно немного повеселимся с генерацией случайных чисел!
rand и сгенерировать числоВаша цель — создать простое приложение, которое генерирует случайное число в заданном диапазоне и выводит его на экран. Для этого нужно:
rand в проект как зависимость.src/main.rs, который использует rand для генерации числа от 1 до 100.
Библиотека 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);
}
Сохраните файл. Этот код:
Rng из rand для работы с генератором.thread_rng — потокобезопасный генератор случайных чисел.gen_range для генерации числа в диапазоне 1..=100 (включительно).println!.Шаг 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
Давайте разберём, что мы сделали, и как это связано с изученным материалом.
Добавление зависимости:
rand = "0.8.5" в [dependencies], как описано в разделе 2. Cargo автоматически загрузил crate с crates.io и записал точную версию в Cargo.lock.Cargo.lock:
$ cat Cargo.lock | grep rand
name = "rand"
version = "0.8.5"
Это обеспечивает воспроизводимость сборки (раздел 3).
Использование в коде:
use rand::Rng подключает трейт, необходимый для метода gen_range. Без этого компилятор выдаст ошибку, так как Rust требует явный импорт.thread_rng — это удобный способ получить генератор, привязанный к текущему потоку. Он инициализируется автоматически с использованием системного источника случайности.gen_range(1..=100) использует включающий диапазон (..=), что означает числа от 1 до 100 включительно. Если написать 1..100, верхняя граница будет 99.Сборка и запуск:
cargo run (раздел 5) сначала вызывает cargo build в профиле debug, затем запускает бинарник. Вывод в target/debug/ можно изолировать с --out-dir, как в разделе 7.rand.Расширения (для любопытных):
release:
$ cargo run --release --out-dir dist
Случайное число: 73
$ ls dist/
random_app
Бинарник будет быстрее и меньше.
small_rng в Cargo.toml:
rand = { version = "0.8.5", features = ["small_rng"] }
Это включит компактный генератор, хотя для простого примера разница незаметна.
Это упражнение — маленький, но важный шаг к освоению Cargo. Вы научились добавлять зависимости, подключать их в коде и видеть результат. Теперь вы можете экспериментировать с другими библиотеками, например, serde для JSON или tokio для асинхронности, применяя тот же подход.
Мы подошли к финалу главы, и это был насыщенный путь через одну из самых важных частей экосистемы Rust — Cargo. За это время мы разобрали инструмент до мельчайших деталей, превратив его из "чёрного ящика" в понятного и мощного помощника. Давайте подведём итоги того, что мы изучили, и посмотрим, как применить эти знания в ваших будущих проектах. А затем я приглашу вас не останавливаться на теории, а перейти к практике, чтобы закрепить всё, что вы узнали. Поехали!
Эта глава была настоящим глубоким погружением в Cargo, и мы охватили всё, что нужно для уверенной работы с проектами на Rust — от базовых настроек до продвинутых техник. Давайте вспомним ключевые моменты:
Cargo.toml: Мы изучили, как задавать метаданные в [package], добавлять зависимости ([dependencies], [dev-dependencies], [build-dependencies]) и управлять условной компиляцией через [features]. Вы теперь знаете, как сделать проект готовым к публикации на crates.io или совместной разработке."^1.0", "~1.0.0") и ролью Cargo.lock. Вы можете обновлять зависимости с cargo update и анализировать их с помощью cargo tree и cargo outdated.debug и release, научились настраивать их в Cargo.toml с параметрами вроде opt-level и lto, чтобы балансировать скорость компиляции и производительность.cargo build, run, test и doc, включая флаги вроде --out-dir и --nocapture. Теперь вы можете компилировать, тестировать и документировать проекты с лёгкостью.--out-dir и build.rs, а также собирать статические бинарники с x86_64-unknown-linux-musl и фичами вроде vendored для независимости от системы.rand дало вам опыт добавления зависимостей и их использования в коде, связав теорию с реальной задачей.
Каждый из этих разделов — это кирпичик в фундаменте вашего мастерства работы с Cargo. Мы не просто прошлись по поверхности, а углубились в детали: от синтаксиса TOML до проверки статической линковки с ldd. Теперь у вас есть полное представление о том, как Cargo управляет проектами, и вы можете настроить его под любые нужды — от маленьких скриптов до крупных приложений.
Cargo — это не просто инструмент, а ваш союзник в разработке. Он берёт на себя рутину — загрузку зависимостей, компиляцию, тестирование — и позволяет сосредоточиться на самом важном: создании качественного кода. Эта глава дала вам карту и компас для навигации по его возможностям, и теперь вы готовы использовать их в полную силу.
Знания без практики — как книга, которую вы прочитали, но не применили. Мы подробно разобрали Cargo, но настоящий прогресс начнётся, когда вы возьмёте эти идеи и начнёте экспериментировать. Вот несколько идей, чтобы вдохновить вас на дальнейшие шаги:
rand или утилиту для чтения файлов с std::fs. Добавьте зависимости, настройте профили и попробуйте статическую линковку.pretty_assertions для красивого вывода ошибок и проверьте документационные тесты с cargo test.Cargo.toml и работу с метаданными.build.rs для копирования бинарников или генерации кода. Попробуйте интегрировать это в CI/CD, например, GitHub Actions.x86_64-unknown-linux-musl или даже wasm32-unknown-unknown для веба. Проверьте бинарники на другой машине или в браузере.
Не бойтесь ошибаться — компилятор Rust и Cargo помогут вам найти и исправить проблемы. Каждый запуск cargo build, каждая строка в Cargo.toml — это шаг к тому, чтобы стать уверенным разработчиком. Попробуйте что-то своё: может быть, утилиту для командной строки или сервер на tokio. Главное — начните!
Эта глава — не конец, а начало. Cargo — это инструмент, который будет с вами на всём пути изучения Rust, и теперь у вас есть знания, чтобы использовать его эффективно. Откройте терминал, создайте новый проект с cargo new и дайте волю своему творчеству. Мы прошли теорию вместе, а практика — за вами. Удачи в ваших Rust-приключениях, и до встречи в следующей главе!