Инженерные практики Rust — за пределами cargo build
О проекте
- Курс основан на оригинальных материалах Rust Training. Лицензии: MIT (код) и CC-BY-4.0 (текст).
- Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus.
- Замечания и исправления присылайте через Issues и Pull Requests репозитория.
Практическое руководство по возможностям инструментария Rust, которые большинство команд открывают слишком поздно: build-скрипты, кросс-компиляция, бенчмаркинг, покрытие кода и проверка безопасности с помощью Miri и Valgrind. Каждая глава опирается на конкретные примеры из реального кода для диагностики оборудования — большого многокрейтового воркспейса, — поэтому каждый приём напрямую переносится в продакшн-код.
Как пользоваться этой книгой
Эта книга рассчитана на самостоятельное изучение или командные воркшопы. Каждая глава в значительной степени независима: читайте их по порядку или сразу переходите к нужной теме.
Обозначения сложности
| Символ | Уровень | Значение |
|---|---|---|
| 🟢 | Начальный | Простые инструменты с понятными шаблонами — полезны с первого дня |
| 🟡 | Средний | Требуют понимания внутреннего устройства инструментов или платформенных концепций |
| 🔴 | Продвинутый | Глубокое знание инструментов, nightly-функции или оркестрация нескольких инструментов |
План изучения
| Часть | Главы | Примерное время | Ключевой результат |
|---|---|---|---|
| I — Сборка и поставка | ch01–02 | 3–4 ч | Метаданные сборки, кросс-компиляция, статические бинарные файлы |
| II — Измерение и верификация | ch03–05 | 4–5 ч | Статистический бенчмаркинг, пороги покрытия, Miri и санитайзеры |
| III — Укрепление и оптимизация | ch06–10 | 6–8 ч | Безопасность цепочки поставок, профили релиза, инструменты времени компиляции, no_std, Windows |
| IV — Интеграция | ch11–13 | 3–4 ч | Продакшн-конвейер CI/CD, приёмы, итоговое упражнение |
| 16–21 ч | Полный инженерный конвейер для продакшна |
Работа с упражнениями
Каждая глава содержит 🏋️ упражнения с указанием сложности. Решения приведены в раскрывающихся блоках <details> — сначала попробуйте решить упражнение самостоятельно, а затем сверьтесь с решением.
- Упражнения 🟢 обычно занимают 10–15 минут
- Упражнения 🟡 требуют 20–40 минут и могут включать запуск инструментов локально
- Упражнения 🔴 требуют серьёзной настройки и экспериментов (от одного часа)
Предварительные требования
| Концепция | Где изучить |
|---|---|
| Структура воркспейса Cargo | Книга по Rust, гл. 14.3 |
| Флаги функций (feature flags) | Справочник Cargo — Features |
#[cfg(test)] и базовое тестирование | Паттерны Rust, гл. 12 |
Блоки unsafe и основы FFI | Паттерны Rust, гл. 10 |
Карта зависимостей между главами
┌──────────┐
│ ch00 │
│ Введение │
└────┬─────┘
┌─────┬───┬──┴──┬──────┬──────┐
▼ ▼ ▼ ▼ ▼ ▼
ch01 ch03 ch04 ch05 ch06 ch09
Сборка Бенч Покр Miri Зав. no_std
│ │ │ │ │ │
│ └────┴────┘ │ ▼
│ │ │ ch10
▼ ▼ ▼ Windows
ch02 ch07 ch07 │
Кросс Профиль Профиль │
│ │ │ │
│ ▼ │ │
│ ch08 │ │
│ Компил │ │
└──────────┴───────────┴─────┘
│
▼
ch11
CI/CD-конвейер
│
▼
ch12 ─── ch13
Приёмы Справка
Читать в любом порядке: ch01, ch03, ch04, ch05, ch06 и ch09 независимы друг от друга. Читать после необходимых предварительных глав: ch02 (нужна ch01), ch07–ch08 (выигрывают от ch03–ch06), ch10 (выигрывает от ch09). Читать в последнюю очередь: ch11 (связывает всё воедино), ch12 (приёмы), ch13 (справочник).
Аннотированное оглавление
Часть I — Сборка и поставка
| № | Глава | Сложность | Описание |
|---|---|---|---|
| 1 | Build-скрипты — build.rs в деталях | 🟢 | Константы времени компиляции, компиляция C-кода, генерация protobuf, линковка системных библиотек, антипаттерны |
| 2 | Кросс-компиляция — один исходник, много целей | 🟡 | Target triple, статические бинарные файлы на musl, кросс-компиляция для ARM, инструмент cross, cargo-zigbuild, GitHub Actions |
Часть II — Измерение и верификация
| № | Глава | Сложность | Описание |
|---|---|---|---|
| 3 | Бенчмаркинг — измеряем то, что важно | 🟡 | Criterion.rs, Divan, флеймграфы perf, PGO, непрерывный бенчмаркинг в CI |
| 4 | Покрытие кода — видим то, что пропускают тесты | 🟢 | cargo-llvm-cov, cargo-tarpaulin, grcov, интеграция с Codecov/Coveralls в CI |
| 5 | Miri, Valgrind и санитайзеры — проверка unsafe-кода | 🔴 | Интерпретатор MIR, Valgrind memcheck/Helgrind, ASan/MSan/TSan, cargo-fuzz, loom |
Часть III — Укрепление и оптимизация
| № | Глава | Сложность | Описание |
|---|---|---|---|
| 6 | Управление зависимостями и безопасность цепочки поставок | 🟢 | cargo-audit, cargo-deny, cargo-vet, cargo-outdated, cargo-semver-checks |
| 7 | Профили релиза и размер бинарного файла | 🟡 | Устройство профиля релиза, компромиссы LTO, cargo-bloat, cargo-udeps |
| 8 | Инструменты времени компиляции и разработки | 🟡 | sccache, mold, cargo-nextest, cargo-expand, cargo-geiger, линты воркспейса, MSRV |
| 9 | no_std и проверка фич | 🔴 | cargo-hack, слои core/alloc/std, пользовательские обработчики паники, тестирование кода no_std |
| 10 | Windows и условная компиляция | 🟡 | Паттерны #[cfg], крейты windows-sys/windows, cargo-xwin, абстракция платформы |
Часть IV — Интеграция
| № | Глава | Сложность | Описание |
|---|---|---|---|
| 11 | Собираем всё вместе — продакшн-конвейер CI/CD | 🟡 | Workflow GitHub Actions, cargo-make, pre-commit-хуки, cargo-dist, итоговый пример |
| 12 | Проверенные на практике приёмы | 🟡 | 10 проверенных паттернов: ловушка deny(warnings), настройка кэша, дедупликация зависимостей, RUSTFLAGS и другие |
| 13 | Краткая справочная карточка | — | Команды под рукой, более 60 записей таблицы решений, ссылки на дополнительную литературу |
Build-скрипты — build.rs в деталях 🟢
Чему вы научитесь:
- Как
build.rsвстраивается в конвейер сборки Cargo и когда он запускается- Пять продакшн-паттернов: константы времени компиляции, компиляция C/C++, кодогенерация protobuf, линковка через
pkg-configи определение фич- Антипаттерны, которые замедляют сборку или ломают кросс-компиляцию
- Как совмещать прослеживаемость с воспроизводимыми сборками
Перекрёстные ссылки: Кросс-компиляция использует build-скрипты для сборок с учётом целевой платформы ·
no_stdи фичи расширяетcfg-флаги, заданные здесь · CI/CD-конвейер оркестрирует build-скрипты в автоматизации
Каждый пакет Cargo может содержать файл с именем build.rs в корне крейта.
Cargo компилирует и выполняет этот файл до компиляции вашего крейта. Build-скрипт
сообщает результаты Cargo через инструкции println!, которые выводятся в stdout.
Что такое build.rs и когда он запускается
┌─────────────────────────────────────────────────────────┐
│ Конвейер сборки Cargo │
│ │
│ 1. Разрешение зависимостей │
│ 2. Загрузка крейтов │
│ 3. Компиляция build.rs ← обычный Rust, на HOST │
│ 4. Запуск build.rs ← stdout → инструкции для Cargo │
│ 5. Компиляция крейта (по инструкциям из шага 4) │
│ 6. Линковка │
└─────────────────────────────────────────────────────────┘
Ключевые факты:
build.rsвыполняется на хост-машине, а не на целевой. При кросс-компиляции build-скрипт запускается на вашей машине разработки, даже если итоговый бинарник предназначен для другой архитектуры.- Область действия build-скрипта ограничена его собственным пакетом. Он не может
повлиять на то, как компилируются другие крейты, — если только пакет не объявляет
ключ
linksвCargo.toml, который позволяет передавать метаданные зависимым крейтам черезcargo::metadata=KEY=VALUE. - Он запускается каждый раз, когда Cargo обнаруживает изменения, — если только вы
не выводите инструкции
cargo::rerun-if-changed, чтобы ограничить повторные запуски.
Примечание (Rust 1.71+): начиная с Rust 1.71 Cargo вычисляет «отпечаток» (fingerprint) скомпилированного бинарника
build.rs— если бинарник не изменился, он не будет перезапущен, даже если временные метки исходников изменились. Однакоcargo::rerun-if-changed=build.rsвсё равно полезна: без какой-либо инструкцииrerun-if-changedCargo перезапускаетbuild.rsпри изменении любого файла в пакете (а не только самогоbuild.rs). Если вывестиcargo::rerun-if-changed=build.rs, повторные запуски будут происходить только при изменении самогоbuild.rs— заметная экономия времени компиляции в крупных крейтах.
- Он может выводить cfg-флаги, переменные окружения, аргументы линкера и пути к файлам, которые использует основной крейт.
Минимальная запись в Cargo.toml:
[package]
name = "my-crate"
version = "0.1.0"
edition = "2021"
build = "build.rs" # значение по умолчанию — Cargo ищет build.rs автоматически
# build = "src/build.rs" # или разместите его в другом месте
Протокол инструкций Cargo
Build-скрипт взаимодействует с Cargo, выводя инструкции в stdout.
Начиная с Rust 1.77 предпочтительным префиксом является cargo:: (он заменил
старую форму с одним двоеточием cargo:).
| Инструкция | Назначение |
|---|---|
cargo::rerun-if-changed=PATH | Перезапускать build.rs только при изменении PATH |
cargo::rerun-if-env-changed=VAR | Перезапускать только при изменении переменной окружения VAR |
cargo::rustc-link-lib=NAME | Линковаться с нативной библиотекой NAME |
cargo::rustc-link-search=PATH | Добавить PATH в путь поиска библиотек |
cargo::rustc-cfg=KEY | Задать флаг #[cfg(KEY)] для условной компиляции |
cargo::rustc-cfg=KEY="VALUE" | Задать флаг #[cfg(KEY = "VALUE")] |
cargo::rustc-env=KEY=VALUE | Задать переменную окружения, доступную через env!() |
cargo::rustc-cdylib-link-arg=FLAG | Передать FLAG линкеру для целей cdylib |
cargo::warning=MESSAGE | Вывести предупреждение во время компиляции |
cargo::metadata=KEY=VALUE | Сохранить метаданные, доступные зависимым крейтам |
// build.rs — минимальный пример
fn main() {
// Перезапускать, только если сам build.rs изменился
println!("cargo::rerun-if-changed=build.rs");
// Задать переменную окружения времени компиляции
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_else(|_| "0".into());
println!("cargo::rustc-env=BUILD_TIMESTAMP={timestamp}");
}
Паттерн 1: константы времени компиляции
Самый распространённый случай: встроить метаданные сборки в бинарник, чтобы выводить их во время выполнения (хеш git, дата сборки, ID задачи CI).
// build.rs
use std::process::Command;
fn main() {
println!("cargo::rerun-if-changed=.git/HEAD");
println!("cargo::rerun-if-changed=.git/refs");
// Хеш коммита git
let output = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.expect("git не найден");
let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
println!("cargo::rustc-env=GIT_HASH={git_hash}");
// Профиль сборки (debug или release)
let profile = std::env::var("PROFILE").unwrap_or_else(|_| "unknown".into());
println!("cargo::rustc-env=BUILD_PROFILE={profile}");
// Target triple
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
println!("cargo::rustc-env=BUILD_TARGET={target}");
}
#![allow(unused)]
fn main() {
// src/main.rs — использование значений, известных на этапе сборки
fn print_version() {
println!(
"{} {} (git:{} target:{} profile:{})",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("GIT_HASH"),
env!("BUILD_TARGET"),
env!("BUILD_PROFILE"),
);
}
}
Встроенные переменные окружения Cargo, которые доступны без build.rs:
CARGO_PKG_NAME,CARGO_PKG_VERSION,CARGO_PKG_AUTHORS,CARGO_PKG_DESCRIPTION,CARGO_MANIFEST_DIR. См. полный список.
Паттерн 2: компиляция C/C++ кода с помощью крейта cc
Когда ваш крейт на Rust оборачивает C-библиотеку или нуждается в небольшом C-помощнике
(типично для аппаратных интерфейсов), крейт cc упрощает
компиляцию внутри build.rs.
# Cargo.toml
[build-dependencies]
cc = "1.0"
// build.rs
fn main() {
println!("cargo::rerun-if-changed=csrc/");
cc::Build::new()
.file("csrc/ipmi_raw.c")
.file("csrc/smbios_parser.c")
.include("csrc/include")
.flag("-Wall")
.flag("-Wextra")
.opt_level(2)
.compile("diag_helpers");
// Это создаёт libdiag_helpers.a и выводит правильные инструкции
// cargo::rustc-link-lib и cargo::rustc-link-search.
}
#![allow(unused)]
fn main() {
// src/lib.rs — FFI-привязки к скомпилированному C-коду
extern "C" {
fn ipmi_raw_command(
netfn: u8,
cmd: u8,
data: *const u8,
data_len: usize,
response: *mut u8,
response_len: *mut usize,
) -> i32;
}
/// Безопасная обёртка над сырым интерфейсом команд IPMI.
/// Предполагается: enum IpmiError { CommandFailed(i32), ... }
pub fn send_ipmi_command(netfn: u8, cmd: u8, data: &[u8]) -> Result<Vec<u8>, IpmiError> {
let mut response = vec![0u8; 256];
let mut response_len: usize = response.len();
// SAFETY: буфер ответа достаточно велик, а response_len корректно инициализирован.
let rc = unsafe {
ipmi_raw_command(
netfn,
cmd,
data.as_ptr(),
data.len(),
response.as_mut_ptr(),
&mut response_len,
)
};
if rc != 0 {
return Err(IpmiError::CommandFailed(rc));
}
response.truncate(response_len);
Ok(response)
}
}
Для C++-кода используйте .cpp(true) и .flag("-std=c++17"):
// build.rs — вариант для C++
fn main() {
println!("cargo::rerun-if-changed=cppsrc/");
cc::Build::new()
.cpp(true)
.file("cppsrc/vendor_parser.cpp")
.flag("-std=c++17")
.flag("-fno-exceptions") // соответствует модели Rust без исключений
.compile("vendor_helpers");
}
Паттерн 3: Protocol Buffers и кодогенерация
Build-скрипты отлично подходят для кодогенерации — превращения файлов схем .proto,
.fbs или .json в исходный код Rust во время компиляции. Вот паттерн для protobuf
с использованием prost-build:
# Cargo.toml
[build-dependencies]
prost-build = "0.13"
// build.rs
fn main() {
println!("cargo::rerun-if-changed=proto/");
prost_build::compile_protos(
&["proto/diagnostics.proto", "proto/telemetry.proto"],
&["proto/"],
)
.expect("Не удалось скомпилировать определения protobuf");
}
#![allow(unused)]
fn main() {
// src/lib.rs — подключаем сгенерированный код
pub mod diagnostics {
include!(concat!(env!("OUT_DIR"), "/diagnostics.rs"));
}
pub mod telemetry {
include!(concat!(env!("OUT_DIR"), "/telemetry.rs"));
}
}
OUT_DIR— это директория, которую предоставляет Cargo и в которую build-скрипты должны помещать сгенерированные файлы. Каждый крейт получает собственныйOUT_DIRвнутриtarget/.
Паттерн 4: линковка системных библиотек через pkg-config
Для системных библиотек, которые предоставляют файлы .pc (systemd, OpenSSL, libpci),
крейт pkg-config проверяет систему и выводит
нужные инструкции для линковки:
# Cargo.toml
[build-dependencies]
pkg-config = "0.3"
// build.rs
fn main() {
// Проверяем наличие libpci (используется для перечисления устройств PCIe)
pkg_config::Config::new()
.atleast_version("3.6.0")
.probe("libpci")
.expect("libpci >= 3.6.0 не найдена — установите pciutils-dev");
// Проверяем наличие libsystemd (необязательно — для интеграции с sd_notify)
if pkg_config::probe_library("libsystemd").is_ok() {
println!("cargo::rustc-cfg=has_systemd");
}
}
#![allow(unused)]
fn main() {
// src/lib.rs — условная компиляция на основе проверки через pkg-config
#[cfg(has_systemd)]
mod systemd_notify {
extern "C" {
fn sd_notify(unset_environment: i32, state: *const std::ffi::c_char) -> i32;
}
pub fn notify_ready() {
let state = std::ffi::CString::new("READY=1").unwrap();
// SAFETY: state — корректная C-строка с нулевым завершителем.
unsafe { sd_notify(0, state.as_ptr()) };
}
}
#[cfg(not(has_systemd))]
mod systemd_notify {
pub fn notify_ready() {
// ничего не делаем на системах без systemd
}
}
}
Паттерн 5: определение фич и условная компиляция
Build-скрипты могут проверять окружение компиляции и задавать cfg-флаги, которые основной крейт использует для условных путей выполнения.
Определение архитектуры процессора и ОС (безопасно — это константы времени компиляции):
// build.rs — определяем возможности CPU и ОС
fn main() {
println!("cargo::rerun-if-changed=build.rs");
let target = std::env::var("TARGET").unwrap();
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
// Включаем оптимизированные пути AVX2 на x86_64
if target.starts_with("x86_64") {
println!("cargo::rustc-cfg=has_x86_64");
}
// Включаем пути ARM NEON на aarch64
if target.starts_with("aarch64") {
println!("cargo::rustc-cfg=has_aarch64");
}
// Проверяем, доступно ли /dev/ipmi0 (проверка во время сборки)
if target_os == "linux" && std::path::Path::new("/dev/ipmi0").exists() {
println!("cargo::rustc-cfg=has_ipmi_device");
}
}
⚠️ Демонстрация антипаттерна — код ниже показывает заманчивый, но проблемный подход. Не используйте его в продакшне.
// build.rs — ПЛОХО: определение аппаратуры во время выполнения, но на этапе сборки
fn main() {
// АНТИПАТТЕРН: бинарник «привязывается» к железу машины сборки.
// Если собрать его на машине с GPU, а развернуть на машине без GPU,
// бинарник молча будет считать, что GPU есть.
if std::process::Command::new("accel-query")
.arg("--query-gpu=name")
.arg("--format=csv,noheader")
.output()
.is_ok()
{
println!("cargo::rustc-cfg=has_accel_device");
}
}
#![allow(unused)]
fn main() {
// src/gpu.rs — код, который адаптируется на основе определения на этапе сборки
pub fn query_gpu_info() -> GpuResult {
#[cfg(has_accel_device)]
{
run_accel_query()
}
#[cfg(not(has_accel_device))]
{
GpuResult::NotAvailable("accel-query не найден на этапе сборки".into())
}
}
}
⚠️ Почему это неправильно: определение устройств во время выполнения почти всегда лучше, чем определение во время сборки, для необязательного оборудования. Бинарник, созданный выше, привязан к конфигурации оборудования машины сборки — на целевой машине он будет вести себя иначе. Определение на этапе сборки уместно только для возможностей, которые действительно фиксированы на этапе компиляции (архитектура, ОС, наличие библиотек). Для оборудования вроде GPU определяйте его во время выполнения, например проверкой
which accel-queryили черезaccel-mgmt.
Антипаттерны и ловушки
| Антипаттерн | Почему это плохо | Решение |
|---|---|---|
Нет rerun-if-changed | build.rs запускается при каждой сборке, замедляя итерации | Всегда выводите как минимум cargo::rerun-if-changed=build.rs |
| Сетевые вызовы в build.rs | Сборки падают без сети, результат невоспроизводим | Вендорьте файлы или используйте отдельный шаг загрузки |
Запись в src/ | Cargo не ожидает, что исходники меняются во время сборки | Пишите в OUT_DIR и используйте include!() |
| Тяжёлые вычисления | Замедляют каждый cargo build | Кешируйте результаты в OUT_DIR и ограничивайте через rerun-if-changed |
| Игнорирование кросс-компиляции | Command::new("gcc") без учёта $CC | Используйте крейт cc, который работает с кросс-компиляционными тулчейнами |
| Паника без контекста | unwrap() выдаёт непрозрачную ошибку «build script failed» | Используйте .expect("понятное сообщение") или выводите cargo::warning= |
Применение: встраивание метаданных сборки
Сейчас проект использует env!("CARGO_PKG_VERSION") для вывода версии. Build-скрипт
мог бы расширить это более богатыми метаданными:
// build.rs — предлагаемое дополнение
fn main() {
println!("cargo::rerun-if-changed=.git/HEAD");
println!("cargo::rerun-if-changed=.git/refs");
println!("cargo::rerun-if-changed=build.rs");
// Встраиваем хеш git для прослеживаемости в диагностических отчётах
if let Ok(output) = std::process::Command::new("git")
.args(["rev-parse", "--short=10", "HEAD"])
.output()
{
let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
println!("cargo::rustc-env=APP_GIT_HASH={hash}");
} else {
println!("cargo::rustc-env=APP_GIT_HASH=unknown");
}
// Встраиваем временную метку сборки для связи с отчётами
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_else(|_| "0".into());
println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}");
// Выводим target triple — полезно при развёртывании на нескольких архитектурах
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
println!("cargo::rustc-env=APP_TARGET={target}");
}
#![allow(unused)]
fn main() {
// src/version.rs — использование метаданных
pub struct BuildInfo {
pub version: &'static str,
pub git_hash: &'static str,
pub build_epoch: &'static str,
pub target: &'static str,
}
pub const BUILD_INFO: BuildInfo = BuildInfo {
version: env!("CARGO_PKG_VERSION"),
git_hash: env!("APP_GIT_HASH"),
build_epoch: env!("APP_BUILD_EPOCH"),
target: env!("APP_TARGET"),
};
impl BuildInfo {
/// Разбираем эпоху во время выполнения, когда это нужно (преобразовать
/// const &str в u64 на stable Rust нельзя — нет const fn для разбора строки в число).
pub fn build_epoch_secs(&self) -> u64 {
self.build_epoch.parse().unwrap_or(0)
}
}
impl std::fmt::Display for BuildInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"DiagTool v{} (git:{} target:{})",
self.version, self.git_hash, self.target
)
}
}
}
Ключевая мысль из проекта: в кодовой базе нет ни одного файла
build.rsни в одном из крейтов, потому что это чистый Rust без зависимостей от C, без кодогенерации и без линковки системных библиотек. Когда это понадобится,build.rs— правильный инструмент, но не добавляйте его «просто так». Отсутствие build-скриптов в большой кодовой базе — признак чистой архитектуры, а не пробел. См. Управление зависимостями, чтобы узнать, как проект управляет цепочкой поставок без собственной логики сборки.
Попробуйте сами
-
Встройте метаданные git: создайте
build.rs, который выводитAPP_GIT_HASHиAPP_BUILD_EPOCHкак переменные окружения. Прочитайте их черезenv!()вmain.rsи выведите информацию о сборке. Убедитесь, что хеш меняется после коммита. -
Проверьте системную библиотеку: напишите
build.rs, который используетpkg-config, чтобы найтиlibz(zlib). Если библиотека найдена, выведитеcargo::rustc-cfg=has_zlib. Вmain.rsвыведите «zlib доступен» или «zlib не найден» в зависимости от флага cfg. -
Понаблюдайте за лишними перезапусками: удалите строку
rerun-if-changedизbuild.rsи посмотрите, сколько раз он перезапускается во времяcargo buildиcargo test. Затем верните строку и сравните результаты.
Воспроизводимые сборки
Глава 1 учит встраивать временные метки и хеши git в бинарники. Это полезно для прослеживаемости, но противоречит воспроизводимым сборкам — свойству, при котором сборка одного и того же исходного кода всегда даёт один и тот же бинарник.
Противоречие:
| Цель | Достигается через | Цена |
|---|---|---|
| Прослеживаемость | APP_BUILD_EPOCH в бинарнике | Каждая сборка уникальна — нельзя проверить целостность |
| Воспроизводимость | cargo build --locked всегда даёт одинаковый результат | Нет метаданных времени сборки |
Практическое решение:
# 1. Всегда используйте --locked в CI (гарантирует, что Cargo.lock учитывается)
cargo build --release --locked
# Завершится с ошибкой, если Cargo.lock отсутствует или устарел — ловит «у меня работает»
# 2. Для сборок, критичных к воспроизводимости, задайте SOURCE_DATE_EPOCH
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) cargo build --release --locked
# Использует временную метку последнего коммита вместо «сейчас» — один коммит = один бинарник
#![allow(unused)]
fn main() {
// В build.rs: учитываем SOURCE_DATE_EPOCH для воспроизводимости
let timestamp = std::env::var("SOURCE_DATE_EPOCH")
.unwrap_or_else(|_| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_else(|_| "0".into())
});
println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}");
}
Лучшая практика: используйте
SOURCE_DATE_EPOCHв build-скриптах, чтобы релизные сборки были воспроизводимыми (хеш git + зафиксированные зависимости + детерминированная временная метка = один и тот же бинарник), а dev-сборки по-прежнему получали актуальные временные метки для удобства.
Диаграмма выбора в конвейере сборки
flowchart TD
START["Нужна работа на этапе компиляции?"] -->|Нет| SKIP["build.rs не нужен"]
START -->|Да| WHAT{"Какого типа?"}
WHAT -->|"Встроить метаданные"| P1["Паттерн 1<br/>Константы времени компиляции"]
WHAT -->|"Компилировать C/C++"| P2["Паттерн 2<br/>крейт cc"]
WHAT -->|"Кодогенерация"| P3["Паттерн 3<br/>prost-build / tonic-build"]
WHAT -->|"Линковать системную библиотеку"| P4["Паттерн 4<br/>pkg-config"]
WHAT -->|"Определить фичи"| P5["Паттерн 5<br/>cfg-флаги"]
P1 --> RERUN["Всегда выводите<br/>cargo::rerun-if-changed"]
P2 --> RERUN
P3 --> RERUN
P4 --> RERUN
P5 --> RERUN
style SKIP fill:#91e5a3,color:#000
style RERUN fill:#ffd43b,color:#000
style P1 fill:#e3f2fd,color:#000
style P2 fill:#e3f2fd,color:#000
style P3 fill:#e3f2fd,color:#000
style P4 fill:#e3f2fd,color:#000
style P5 fill:#e3f2fd,color:#000
🏋️ Упражнения
🟢 Упражнение 1: штамп версии
Создайте минимальный крейт с build.rs, который встраивает текущий хеш git и профиль
сборки в переменные окружения. Выведите их из main(). Убедитесь, что вывод меняется
между debug- и release-сборками.
Решение
// build.rs
fn main() {
println!("cargo::rerun-if-changed=.git/HEAD");
println!("cargo::rerun-if-changed=build.rs");
let hash = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "unknown".into());
println!("cargo::rustc-env=GIT_HASH={hash}");
println!("cargo::rustc-env=BUILD_PROFILE={}", std::env::var("PROFILE").unwrap_or_default());
}
// src/main.rs
fn main() {
println!("{} v{} (git:{} profile:{})",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("GIT_HASH"),
env!("BUILD_PROFILE"),
);
}
cargo run # покажет profile:debug
cargo run --release # покажет profile:release
🟡 Упражнение 2: условная системная библиотека
Напишите build.rs, который с помощью pkg-config проверяет наличие и libz, и libpci.
Выведите cfg-флаг для каждой найденной библиотеки. В main.rs выведите, какие библиотеки
были обнаружены во время сборки.
Решение
# Cargo.toml
[build-dependencies]
pkg-config = "0.3"
// build.rs
fn main() {
println!("cargo::rerun-if-changed=build.rs");
if pkg_config::probe_library("zlib").is_ok() {
println!("cargo::rustc-cfg=has_zlib");
}
if pkg_config::probe_library("libpci").is_ok() {
println!("cargo::rustc-cfg=has_libpci");
}
}
// src/main.rs
fn main() {
#[cfg(has_zlib)]
println!("✅ zlib обнаружен");
#[cfg(not(has_zlib))]
println!("❌ zlib не найден");
#[cfg(has_libpci)]
println!("✅ libpci обнаружен");
#[cfg(not(has_libpci))]
println!("❌ libpci не найден");
}
Ключевые выводы
build.rsвыполняется на хосте во время компиляции — всегда выводитеcargo::rerun-if-changed, чтобы избежать лишних пересборок- Используйте крейт
cc(а не прямые вызовыgcc) для компиляции C/C++ — он корректно работает с кросс-компиляционными тулчейнами - Записывайте сгенерированные файлы в
OUT_DIR, но никогда вsrc/— Cargo не ожидает, что исходники будут меняться во время сборки - Предпочитайте определение во время выполнения определению во время сборки для необязательного оборудования
- Используйте
SOURCE_DATE_EPOCH, чтобы сборки были воспроизводимыми при встраивании временных меток
Кросс-компиляция — один исходник, много целей 🟡
Чему вы научитесь:
- Как устроены target triple в Rust и как добавлять их через
rustup- Сборка статических бинарников musl для развёртывания в контейнерах и облаке
- Кросс-компиляция под ARM (aarch64) с нативными тулчейнами,
crossиcargo-zigbuild- Настройка матричных сборок GitHub Actions для мультиархитектурного CI
Перекрёстные ссылки: Build-скрипты — build.rs выполняется на HOST при кросс-компиляции · Профили релиза — настройки LTO и strip для релизных кросс-скомпилированных бинарников · Windows — кросс-компиляция под Windows и цели
no_std
Кросс-компиляция — это сборка исполняемого файла на одной машине (хост), который
будет работать на другой машине (целевая). Хостом может быть ваш ноутбук x86_64,
а целью — ARM-сервер, контейнер на базе musl или даже машина с Windows.
Rust делает это на удивление просто, потому что rustc уже является кросс-компилятором —
ему нужны только подходящие библиотеки для целевой платформы и совместимый линкер.
Устройство target triple
Каждая цель компиляции в Rust задаётся target triple (несмотря на название, у неё часто четыре части):
<arch>-<vendor>-<os>-<env>
Примеры:
x86_64 - unknown - linux - gnu ← стандартный Linux (glibc)
x86_64 - unknown - linux - musl ← статический Linux (musl libc)
aarch64 - unknown - linux - gnu ← 64-бит ARM Linux
x86_64 - pc - windows- msvc ← Windows с MSVC
aarch64 - apple - darwin ← macOS на Apple Silicon
x86_64 - unknown - none ← голое железо (без ОС)
Список всех доступных целей:
# Все цели, в которые может компилировать rustc (~250 целей)
rustc --print target-list | wc -l
# Установленные на вашей системе цели
rustup target list --installed
# Текущая цель по умолчанию
rustc -vV | grep host
Установка тулчейнов через rustup
# Добавляем библиотеки для целевых платформ (std для нужной цели)
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-gnu
# Теперь можно кросс-компилировать:
cargo build --target x86_64-unknown-linux-musl
cargo build --target aarch64-unknown-linux-gnu # нужен линкер — см. ниже
Что даёт rustup target add: предварительно скомпилированные библиотеки std, core
и alloc для этой цели. Он не предоставляет C-линкер или C-библиотеку. Для целей,
которым нужен C-тулчейн (большинство целей gnu), его нужно установить отдельно.
# Ubuntu/Debian — устанавливаем кросс-линкер для aarch64
sudo apt install gcc-aarch64-linux-gnu
# Ubuntu/Debian — устанавливаем тулчейн musl для статических сборок
sudo apt install musl-tools
# Fedora
sudo dnf install gcc-aarch64-linux-gnu
.cargo/config.toml — настройка по целям
Вместо того чтобы передавать --target в каждой команде, задайте значения по умолчанию в
.cargo/config.toml в корне проекта или в домашней директории:
# .cargo/config.toml
# Цель по умолчанию для этого проекта (необязательно — уберите, чтобы оставить нативную)
# [build]
# target = "x86_64-unknown-linux-musl"
# Линкер для кросс-компиляции под aarch64
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
rustflags = ["-C", "target-feature=+crc"]
# Линкер для статических сборок musl (обычно подходит и системный gcc)
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
rustflags = ["-C", "target-feature=+crc,+aes"]
# ARM 32-бит (Raspberry Pi, встраиваемые системы)
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
# Переменные окружения для всех целей
[env]
# Пример: задаём собственный sysroot
# SYSROOT = "/opt/cross/sysroot"
Порядок поиска файла конфигурации (побеждает первый найденный):
<project>/.cargo/config.toml<project>/../.cargo/config.toml(родительские директории, подъём вверх по дереву)$CARGO_HOME/config.toml(обычно~/.cargo/config.toml)
Статические бинарники с musl
Для развёртывания в минимальных контейнерах (Alpine, образы Docker scratch) или на
системах, где нельзя контролировать версию glibc, собирайте проект с musl:
# Устанавливаем musl-цель
rustup target add x86_64-unknown-linux-musl
sudo apt install musl-tools # предоставляет musl-gcc
# Собираем полностью статический бинарник
cargo build --release --target x86_64-unknown-linux-musl
# Проверяем, что он статический
file target/x86_64-unknown-linux-musl/release/diag_tool
# → ELF 64-bit LSB executable, x86-64, statically linked
ldd target/x86_64-unknown-linux-musl/release/diag_tool
# → not a dynamic executable
Компромиссы статической и динамической линковки:
| Аспект | glibc (динамическая) | musl (статическая) |
|---|---|---|
| Размер бинарника | Меньше (общие библиотеки) | Больше (примерно на 5–15 МБ) |
| Переносимость | Нужна совместимая версия glibc | Работает на любом Linux |
| Разрешение DNS | Полная поддержка nsswitch | Базовый резолвер (без mDNS) |
| Развёртывание | Нужны sysroot или контейнер | Один бинарник, без зависимостей |
| Производительность | Немного быстрее malloc | Немного медленнее malloc |
Поддержка dlopen() | Да | Нет |
Для проекта: статическая сборка musl идеально подходит для развёртывания на разнородном серверном железе, где нельзя гарантировать версию ОС на хосте. Модель развёртывания одним бинарником устраняет проблемы «у меня работает».
Кросс-компиляция под ARM (aarch64)
ARM-серверы (AWS Graviton, Ampere Altra, Grace) всё чаще встречаются в дата-центрах. Кросс-компиляция под aarch64 с хоста x86_64:
# Шаг 1: установите цель и кросс-линкер
rustup target add aarch64-unknown-linux-gnu
sudo apt install gcc-aarch64-linux-gnu
# Шаг 2: настройте линкер в .cargo/config.toml (см. выше)
# Шаг 3: сборка
cargo build --release --target aarch64-unknown-linux-gnu
# Шаг 4: проверяем бинарник
file target/aarch64-unknown-linux-gnu/release/diag_tool
# → ELF 64-bit LSB executable, ARM aarch64
Для запуска тестов под целевую архитектуру нужно одно из двух:
- Реальная ARM-машина
- Эмуляция QEMU в пользовательском режиме
# Устанавливаем QEMU в пользовательском режиме (запускает ARM-бинарники на x86_64)
sudo apt install qemu-user qemu-user-static binfmt-support
# Теперь cargo test может запускать кросс-скомпилированные тесты через QEMU
cargo test --target aarch64-unknown-linux-gnu
# (Медленно — каждый тестовый бинарник эмулируется. Используйте для проверки в CI, а не в ежедневной разработке.)
Настройте QEMU как тестовый раннер в .cargo/config.toml:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64-static -L /usr/aarch64-linux-gnu"
Инструмент cross — кросс-компиляция на основе Docker
Инструмент cross даёт кросс-компиляцию без
настройки, используя готовые Docker-образы с предварительно настроенными тулчейнами:
# Установка cross (с crates.io — стабильные релизы)
cargo install cross
# Или из git для новейших возможностей (менее стабильно):
# cargo install cross --git https://github.com/cross-rs/cross
# Кросс-компиляция — настраивать тулчейн не нужно!
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --target x86_64-unknown-linux-musl
cross build --release --target armv7-unknown-linux-gnueabihf
# Кросс-тестирование — QEMU входит в состав Docker-образа
cross test --target aarch64-unknown-linux-gnu
Как это работает: cross заменяет cargo и запускает сборку внутри Docker-контейнера,
в котором уже установлен нужный кросс-тулчейн. Ваш исходный код монтируется в контейнер,
а результат попадает в обычную директорию target/.
Настройка Docker-образа через Cross.toml:
# Cross.toml
[target.aarch64-unknown-linux-gnu]
# Используем собственный Docker-образ с дополнительными системными библиотеками
image = "my-registry/cross-aarch64:latest"
# Предустановка системных пакетов
pre-build = [
"dpkg --add-architecture arm64",
"apt-get update && apt-get install -y libpci-dev:arm64"
]
[target.aarch64-unknown-linux-gnu.env]
# Передаём переменные окружения в контейнер
passthrough = ["CI", "GITHUB_TOKEN"]
Для cross нужен Docker (или Podman), но он избавляет от ручной установки
кросс-компиляторов, sysroot и QEMU. Это рекомендуемый подход для CI.
Zig как кросс-линкер
Zig включает в один архив размером около 40 МБ C-компилятор и sysroot для кросс-компиляции примерно под 40 целей. Это делает его удобным кросс-линкером для Rust:
# Установка Zig (один бинарник, менеджер пакетов не нужен)
# Скачайте с https://ziglang.org/download/
# Или через менеджер пакетов:
sudo snap install zig --classic --beta # Ubuntu
brew install zig # macOS
# Установка cargo-zigbuild
cargo install cargo-zigbuild
Зачем Zig? Главное преимущество — выбор версии glibc. Zig позволяет указать точную версию glibc для линковки, гарантируя, что бинарник запустится на старых дистрибутивах Linux:
# Сборка под glibc 2.17 (совместимость с CentOS 7 / RHEL 7)
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17
# Сборка под aarch64 с glibc 2.28 (Ubuntu 18.04+)
cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28
# Сборка под musl (полностью статическая)
cargo zigbuild --release --target x86_64-unknown-linux-musl
Суффикс .2.17 — расширение Zig: он указывает линкеру использовать версии символов
glibc 2.17, благодаря чему полученный бинарник работает на CentOS 7 и более поздних
системах. Без Docker, без управления sysroot и без установки кросс-компиляторов.
Сравнение: cross, cargo-zigbuild и ручной способ:
| Возможность | Вручную | cross | cargo-zigbuild |
|---|---|---|---|
| Усилия на настройку | Высокие (тулчейн на каждую цель) | Низкие (нужен Docker) | Низкие (один бинарник) |
| Нужен Docker | Нет | Да | Нет |
| Выбор версии glibc | Нет (используется glibc хоста) | Нет (используется glibc контейнера) | Да (точная версия) |
| Запуск тестов | Нужен QEMU | Входит в состав | Нужен QEMU |
| macOS → Linux | Сложно | Легко | Легко |
| Linux → macOS | Очень сложно | Не поддерживается | Ограниченно |
| Накладные расходы на размер бинарника | Нет | Нет | Нет |
CI-конвейер: матрица GitHub Actions
Продакшн-уровень CI-workflow, который собирает проект под несколько целей:
# .github/workflows/cross-build.yml
name: Кросс-платформенная сборка
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always
jobs:
build:
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
name: linux-x86_64
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
name: linux-x86_64-static
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
name: linux-aarch64
use_cross: true
- target: x86_64-pc-windows-msvc
os: windows-latest
name: windows-x86_64
runs-on: ${{ matrix.os }}
name: Сборка (${{ matrix.name }})
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Установка инструментов musl
if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt-get install -y musl-tools
- name: Установка cross
if: matrix.use_cross
run: cargo install cross
- name: Сборка (нативная)
if: "!matrix.use_cross"
run: cargo build --release --target ${{ matrix.target }}
- name: Сборка (cross)
if: matrix.use_cross
run: cross build --release --target ${{ matrix.target }}
- name: Запуск тестов
if: "!matrix.use_cross"
run: cargo test --target ${{ matrix.target }}
- name: Загрузка артефакта
uses: actions/upload-artifact@v4
with:
name: diag_tool-${{ matrix.name }}
path: target/${{ matrix.target }}/release/diag_tool*
Применение: сборки для серверов с разной архитектурой
Сейчас у бинарника нет настройки кросс-компиляции. Для инструмента диагностики оборудования, который развёртывается на разнородном парке серверов, рекомендуется добавить:
my_workspace/
├── .cargo/
│ └── config.toml ← конфигурация линкеров для каждой цели
├── Cross.toml ← конфигурация инструмента cross
└── .github/workflows/
└── cross-build.yml ← матрица CI для 3 целей
Рекомендуемый .cargo/config.toml:
# .cargo/config.toml для проекта
# Оптимизации профиля релиза (уже есть в Cargo.toml, показано для справки)
# [profile.release]
# lto = true
# codegen-units = 1
# panic = "abort"
# strip = true
# aarch64 для ARM-серверов (Graviton, Ampere, Grace)
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
# musl для переносимых статических бинарников
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
Рекомендуемые цели сборки:
| Цель | Сценарий | Куда развёртывать |
|---|---|---|
x86_64-unknown-linux-gnu | Стандартная нативная сборка | Обычные x86-серверы |
x86_64-unknown-linux-musl | Статический бинарник, любой дистрибутив | Контейнеры, минимальные хосты |
aarch64-unknown-linux-gnu | ARM-серверы | Graviton, Ampere, Grace |
Ключевая мысль: секция
[profile.release]в корневомCargo.tomlворкспейса уже содержитlto = true,codegen-units = 1,panic = "abort"иstrip = true— это идеальный профиль релиза для кросс-скомпилированных бинарников, которые развёртываются (полную таблицу влияния см. в разделе Профили релиза). В сочетании с musl это даёт один статический бинарник размером около 10 МБ без зависимостей во время выполнения.
Устранение неполадок при кросс-компиляции
| Симптом | Причина | Решение |
|---|---|---|
linker 'aarch64-linux-gnu-gcc' not found | Не установлен тулчейн кросс-линкера | sudo apt install gcc-aarch64-linux-gnu |
cannot find -lssl (цель musl) | Системный OpenSSL слинкован с glibc | Используйте фичу vendored: openssl = { version = "0.10", features = ["vendored"] } |
build.rs запускает не тот бинарник | build.rs выполняется на HOST, а не на целевой платформе | Проверяйте CARGO_CFG_TARGET_OS в build.rs, а не cfg!(target_os) |
Тесты проходят локально, но падают в cross | В Docker-образе нет тестовых данных | Подключите данные через Cross.toml: [build.env] volumes = ["./TestArea:/TestArea"] |
undefined reference to __cxa_thread_atexit_impl | На целевой машине старая glibc | Используйте cargo-zigbuild с явной версией glibc: --target x86_64-unknown-linux-gnu.2.17 |
| Бинарник падает с segfault на ARM | Собран под другой вариант ARM | Убедитесь, что target triple соответствует железу: aarch64-unknown-linux-gnu для 64-битного ARM |
GLIBC_2.XX not found во время выполнения | На машине сборки более новая glibc | Используйте musl для статических сборок или cargo-zigbuild для фиксации версии glibc |
Дерево решений для кросс-компиляции
flowchart TD
START["Нужна кросс-компиляция?"] --> STATIC{"Статический бинарник?"}
STATIC -->|Да| MUSL["Цель musl<br/>--target x86_64-unknown-linux-musl"]
STATIC -->|Нет| GLIBC{"Нужна старая glibc?"}
GLIBC -->|Да| ZIG["cargo-zigbuild<br/>--target x86_64-unknown-linux-gnu.2.17"]
GLIBC -->|Нет| ARCH{"Архитектура цели?"}
ARCH -->|"Та же архитектура"| NATIVE["Нативный тулчейн<br/>rustup target add + линкер"]
ARCH -->|"ARM/другая"| DOCKER{"Доступен Docker?"}
DOCKER -->|Да| CROSS["cross build<br/>на базе Docker, без настройки"]
DOCKER -->|Нет| MANUAL["Ручной sysroot<br/>apt install gcc-aarch64-linux-gnu"]
style MUSL fill:#91e5a3,color:#000
style ZIG fill:#91e5a3,color:#000
style CROSS fill:#91e5a3,color:#000
style NATIVE fill:#e3f2fd,color:#000
style MANUAL fill:#ffd43b,color:#000
🏋️ Упражнения
🟢 Упражнение 1: статический бинарник musl
Соберите любой бинарник Rust для x86_64-unknown-linux-musl. Проверьте, что он
статически слинкован, с помощью file и ldd.
Решение
rustup target add x86_64-unknown-linux-musl
cargo new hello-static && cd hello-static
cargo build --release --target x86_64-unknown-linux-musl
# Проверка
file target/x86_64-unknown-linux-musl/release/hello-static
# Вывод: ... statically linked ...
ldd target/x86_64-unknown-linux-musl/release/hello-static
# Вывод: not a dynamic executable
🟡 Упражнение 2: матрица GitHub Actions для кросс-сборки
Напишите workflow GitHub Actions, который собирает проект на Rust для трёх целей:
x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl и aarch64-unknown-linux-gnu.
Используйте стратегию matrix.
Решение
name: Кросс-сборка
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Установка cross
run: cargo install cross --locked
- name: Сборка
run: cross build --release --target ${{ matrix.target }}
- uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: target/${{ matrix.target }}/release/my-binary
Ключевые выводы
rustcуже является кросс-компилятором — нужны только правильная цель и линкер- musl создаёт полностью статические бинарники без зависимостей во время выполнения — идеально для контейнеров
cargo-zigbuildрешает проблему «какую версию glibc выбрать» для корпоративных дистрибутивов Linuxcross— самый простой путь для ARM и других экзотических целей: Docker берёт на себя sysroot- Всегда проверяйте бинарник через
fileиldd, чтобы убедиться, что он соответствует целевой платформе развёртывания
Бенчмаркинг — измеряем то, что важно 🟡
Чему вы научитесь:
- Почему наивный замер через
Instant::now()даёт ненадёжные результаты- Статистический бенчмаркинг с Criterion.rs и более лёгкой альтернативой — Divan
- Профилирование горячих точек с помощью
perf, флеймграфов и PGO- Настройка непрерывного бенчмаркинга в CI для автоматического обнаружения регрессий
Перекрёстные ссылки: Профили релиза — когда горячая точка найдена, оптимизируйте бинарник · CI/CD-конвейер — джоба с бенчмарками в конвейере · Покрытие кода — покрытие показывает, что протестировано, а бенчмарки — что работает быстро
«Мы должны забыть о мелкой эффективности примерно в 97% случаев: преждевременная оптимизация — корень всех зол. Однако не стоит упускать возможности в тех критических 3%.» — Дональд Кнут
Самое трудное — не написать бенчмарки, а написать такие, которые дают осмысленные, воспроизводимые и пригодные для действий числа. Эта глава описывает инструменты и приёмы, которые переводят вас от фразы «кажется, быстро» к «у нас есть статистическое подтверждение, что PR #347 снизил пропускную способность парсинга на 4,2%».
Почему не std::time::Instant?
Соблазн:
// ❌ Наивный бенчмарк — ненадёжные результаты
use std::time::Instant;
fn main() {
let start = Instant::now();
let result = parse_device_query_output(&sample_data);
let elapsed = start.elapsed();
println!("Разбор занял {:?}", elapsed);
// Проблема 1: компилятор может оптимизировать `result` (исключение мёртвого кода)
// Проблема 2: одно измерение — нет статистической значимости
// Проблема 3: масштабирование частоты CPU, тепловое ограничение, другие процессы
// Проблема 4: холодный и горячий кеш не контролируются
}
Проблемы ручного замера:
- Исключение мёртвого кода — компилятор может вообще пропустить вычисление, если результат не используется.
- Нет прогрева — первый запуск включает промахи кеша, ошибки страниц ОС и ленивую инициализацию. (JIT-эффекты к Rust неприменимы, а вот ошибки страниц ОС — применимы.)
- Нет статистического анализа — одно измерение ничего не говорит о разбросе, выбросах или доверительных интервалах.
- Нет обнаружения регрессий — нельзя сравнить с предыдущими запусками.
Criterion.rs — статистический бенчмаркинг
Criterion.rs — де-факто стандарт для микробенчмарков в Rust. Он использует статистические методы, чтобы получать надёжные измерения, и автоматически обнаруживает регрессии производительности.
Настройка:
# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports", "cargo_bench_support"] }
[[bench]]
name = "parsing_bench"
harness = false # Используем harness Criterion, а не встроенный тестовый harness
Полный бенчмарк:
#![allow(unused)]
fn main() {
// benches/parsing_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
/// Тип данных для разобранной информации о GPU
#[derive(Debug, Clone)]
struct GpuInfo {
index: u32,
name: String,
temp_c: u32,
power_w: f64,
}
/// Тестируемая функция — имитирует разбор CSV-вывода device-query
fn parse_gpu_csv(input: &str) -> Vec<GpuInfo> {
input
.lines()
.filter(|line| !line.starts_with('#'))
.filter_map(|line| {
let fields: Vec<&str> = line.split(", ").collect();
if fields.len() >= 4 {
Some(GpuInfo {
index: fields[0].parse().ok()?,
name: fields[1].to_string(),
temp_c: fields[2].parse().ok()?,
power_w: fields[3].parse().ok()?,
})
} else {
None
}
})
.collect()
}
fn bench_parse_gpu_csv(c: &mut Criterion) {
// Репрезентативные тестовые данные
let small_input = "0, Acme Accel-V1-80GB, 32, 65.5\n\
1, Acme Accel-V1-80GB, 34, 67.2\n";
let large_input = (0..64)
.map(|i| format!("{i}, Acme Accel-X1-80GB, {}, {:.1}\n", 30 + i % 20, 60.0 + i as f64))
.collect::<String>();
c.bench_function("parse_2_gpus", |b| {
b.iter(|| parse_gpu_csv(black_box(small_input)))
});
c.bench_function("parse_64_gpus", |b| {
b.iter(|| parse_gpu_csv(black_box(&large_input)))
});
}
criterion_group!(benches, bench_parse_gpu_csv);
criterion_main!(benches);
}
Запуск и чтение результатов:
# Запустить все бенчмарки
cargo bench
# Запустить конкретный бенчмарк по имени
cargo bench -- parse_64
# Вывод:
# parse_2_gpus time: [1.2345 µs 1.2456 µs 1.2578 µs]
# ▲ ▲ ▲
# │ доверительный интервал
# нижняя 95% медиана верхняя 95%
#
# parse_64_gpus time: [38.123 µs 38.456 µs 38.812 µs]
# change: [-1.2345% -0.5678% +0.1234%] (p = 0.12 > 0.05)
# No change in performance detected.
Что делает black_box(): это подсказка компилятору, которая не даёт исключить мёртвый
код и слишком агрессивно сворачивать константы. Компилятор не может «заглянуть» сквозь
black_box, поэтому ему приходится действительно вычислить результат.
Параметризованные бенчмарки и группы бенчмарков
Сравнение нескольких реализаций или размеров входных данных:
#![allow(unused)]
fn main() {
// benches/comparison_bench.rs
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId, Throughput};
fn bench_parsing_strategies(c: &mut Criterion) {
let mut group = c.benchmark_group("csv_parsing");
// Тестируем на разных размерах входных данных
for num_gpus in [1, 8, 32, 64, 128] {
let input = generate_gpu_csv(num_gpus);
// Задаём пропускную способность для отчёта в байтах в секунду
group.throughput(Throughput::Bytes(input.len() as u64));
group.bench_with_input(
BenchmarkId::new("split_based", num_gpus),
&input,
|b, input| b.iter(|| parse_split(input)),
);
group.bench_with_input(
BenchmarkId::new("regex_based", num_gpus),
&input,
|b, input| b.iter(|| parse_regex(input)),
);
group.bench_with_input(
BenchmarkId::new("nom_based", num_gpus),
&input,
|b, input| b.iter(|| parse_nom(input)),
);
}
group.finish();
}
criterion_group!(benches, bench_parsing_strategies);
criterion_main!(benches);
}
Вывод: Criterion создаёт HTML-отчёт target/criterion/report/index.html с violin-графиками,
сравнительными диаграммами и анализом регрессий — откройте его в браузере.
Divan — более лёгкая альтернатива
Divan — более новый фреймворк для бенчмаркинга, который использует атрибутные макросы вместо DSL-макросов Criterion:
# Cargo.toml
[dev-dependencies]
divan = "0.1"
[[bench]]
name = "parsing_bench"
harness = false
// benches/parsing_bench.rs
use divan::black_box;
const SMALL_INPUT: &str = "0, Acme Accel-V1-80GB, 32, 65.5\n\
1, Acme Accel-V1-80GB, 34, 67.2\n";
fn generate_gpu_csv(n: usize) -> String {
(0..n)
.map(|i| format!("{i}, Acme Accel-X1-80GB, {}, {:.1}\n", 30 + i % 20, 60.0 + i as f64))
.collect()
}
fn main() {
divan::main();
}
#[divan::bench]
fn parse_2_gpus() -> Vec<GpuInfo> {
parse_gpu_csv(black_box(SMALL_INPUT))
}
#[divan::bench(args = [1, 8, 32, 64, 128])]
fn parse_n_gpus(n: usize) -> Vec<GpuInfo> {
let input = generate_gpu_csv(n);
parse_gpu_csv(black_box(&input))
}
// Вывод Divan — аккуратная таблица:
// ╰─ parse_2_gpus fastest │ slowest │ median │ mean │ samples │ iters
// 1.234 µs │ 1.567 µs │ 1.345 µs │ 1.350 µs │ 100 │ 1600
Когда выбирать Divan вместо Criterion:
- Проще API (атрибутные макросы, меньше шаблонного кода)
- Быстрее компиляция (меньше зависимостей)
- Хорошо подходит для быстрых проверок производительности во время разработки
Когда выбирать Criterion:
- Статистическое обнаружение регрессий между запусками
- HTML-отчёты с графиками
- Устоявшаяся экосистема, больше интеграций с CI
Профилирование с perf и флеймграфами
Бенчмарки показывают насколько быстро, а профилирование — куда уходит время.
# Шаг 1: сборка с отладочной информацией (скорость release, отладочные символы)
cargo build --release
# Убедитесь, что отладочная информация доступна:
# [profile.release]
# debug = true # Добавьте временно для профилирования
# Шаг 2: запись с помощью perf
perf record --call-graph=dwarf ./target/release/diag_tool --run-diagnostics
# Шаг 3: построение флеймграфа
# Установка: cargo install flamegraph
# Установка: cargo install addr2line --features=bin (необязательно, ускоряет cargo-flamegraph)
cargo flamegraph --root -- --run-diagnostics
# Откроется интерактивный SVG-флеймграф
# Альтернатива: perf + inferno
perf script | inferno-collapse-perf | inferno-flamegraph > flamegraph.svg
Как читать флеймграф:
- Ширина = время, проведённое в функции (шире = медленнее)
- Высота = глубина стека вызовов (выше ≠ медленнее, просто глубже)
- Низ = точка входа, верх = листовые функции, которые выполняют реальную работу
- Ищите широкие «плато» наверху — это и есть ваши горячие точки
Оптимизация по профилю (PGO)
Profile-Guided Optimization (PGO) — техника оптимизации компилятора, которая повышает производительность приложений, интенсивно использующих процессор. Основная идея PGO — собрать данные о типичном выполнении программы (например, какие ветвления она выбирает чаще всего), а затем использовать эти данные для таких оптимизаций, как инлайнинг, размещение машинного кода, распределение регистров и т. д.
Данные о выполнении программы можно собирать двумя способами. Первый — запустить программу
под профилировщиком (например, perf). Второй — собрать инструментированный бинарник, то есть
бинарник со встроенным сбором данных, и запустить его. Второй способ обычно даёт более точные
данные, и именно он поддерживается rustc.
Ниже приведён пример PGO на основе инструментирования:
# Шаг 1: сборка с инструментированием
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release
# Шаг 2: запуск репрезентативных рабочих нагрузок
./target/release/diag_tool --run-full # генерирует данные профилирования
# Шаг 3: объединение данных профилирования
# Используйте llvm-profdata, соответствующий версии LLVM в rustc:
# $(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata
# Или если установлен llvm-tools: rustup component add llvm-tools
llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data/
# Шаг 4: пересборка с обратной связью от профилирования
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" cargo build --release
# Типичный прирост: 5–20% для вычислительно нагруженного кода (парсинг, криптография, кодогенерация).
# Код, ограниченный I/O или системными вызовами (как большой проект), получит гораздо
# меньший выигрыш, потому что процессор в основном ждёт, а не выполняет горячие циклы.
Вместо прямого использования компилятора для PGO можно воспользоваться cargo-pgo — у него интуитивный интерфейс командной строки, и он избавляет от всей ручной работы.
С cargo-pgo процесс оптимизации из примера выше выглядит так:
# Шаг 1: сборка с инструментированием
cargo pgo build
# Шаг 2: запуск репрезентативных рабочих нагрузок
cargo pgo run -- --run-full
# Шаг 3: пересборка с обратной связью от профилирования
cargo pgo optimize
Sampling PGO (SPGO) — более сложный способ выполнить PGO, зато с меньшими накладными расходами во время выполнения по сравнению с PGO на основе инструментирования. Пока лучшее место, чтобы почитать о нём, — руководство по PGO для Clang.
Совет: прежде чем тратить время на PGO, убедитесь, что в вашем профиле релиза уже включён LTO — обычно это даёт больший эффект при меньших усилиях.
Дополнительная литература:
- Официальное руководство rustc по PGO.
- Awesome PGO — коллекция бенчмарков PGO для реальных приложений, включая руководства по PGO для разных компиляторов (в том числе Sampling PGO)
- LLVM BOLT — оптимизация после линковки (Post-Link Optimization, PLO). PLO можно применять для дополнительных оптимизаций даже после PGO, чтобы получить ещё лучшую производительность.
cargo-pgoтакже поддерживаетllvm-bolt.
hyperfine — быстрый сквозной замер
hyperfine замеряет целые команды, а не отдельные
функции. Он идеально подходит для измерения общей производительности бинарника:
# Установка
cargo install hyperfine
# Или: sudo apt install hyperfine (Ubuntu 23.04+)
# Базовый бенчмарк
hyperfine './target/release/diag_tool --run-diagnostics'
# Сравнение двух реализаций
hyperfine './target/release/diag_tool_v1 --run-diagnostics' \
'./target/release/diag_tool_v2 --run-diagnostics'
# Прогревочные запуски + минимальное количество итераций
hyperfine --warmup 3 --min-runs 10 './target/release/diag_tool --run-all'
# Экспорт результатов в JSON для сравнения в CI
hyperfine --export-json bench.json './target/release/diag_tool --run-all'
Когда использовать hyperfine, а когда Criterion:
hyperfine: замер всего бинарника, сравнение до и после рефакторинга, нагрузки, ограниченные I/O- Criterion: микробенчмарки отдельных функций, статистическое обнаружение регрессий
Непрерывный бенчмаркинг в CI
Обнаруживайте регрессии производительности до того, как они попадут в релиз:
# .github/workflows/bench.yml
name: Бенчмарки
on:
pull_request:
paths: ['**/*.rs', 'Cargo.toml', 'Cargo.lock']
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Запуск бенчмарков
# Требуется criterion = { features = ["cargo_bench_support"] } для --output-format
run: cargo bench -- --output-format bencher | tee bench_output.txt
- name: Сохранение результата бенчмарка
uses: benchmark-action/github-action-benchmark@v1
with:
tool: 'cargo'
output-file-path: bench_output.txt
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
alert-threshold: '120%' # Оповещать, если на 20% медленнее
comment-on-alert: true
fail-on-alert: true # Блокировать PR при обнаружении регрессии
Ключевые моменты для CI:
- Используйте выделенные раннеры для бенчмарков (а не общий CI), чтобы результаты были стабильными
- Закрепите раннер за конкретным типом машины, если используете облачный CI
- Храните исторические данные, чтобы обнаруживать постепенные регрессии
- Задавайте пороги исходя из допустимых отклонений вашей нагрузки (5% для горячих путей, 20% для холодных)
Применение: производительность парсинга
В проекте есть несколько чувствительных к производительности путей парсинга, которым пошли бы на пользу бенчмарки:
| Горячая точка парсинга | Крейт | Почему это важно |
|---|---|---|
| Вывод CSV/XML от accelerator-query | device_diag | Вызывается для каждого GPU, до 8× за запуск |
| Разбор событий сенсоров | event_log | Тысячи записей на загруженных серверах |
| JSON топологии PCIe | topology_lib | Сложные вложенные структуры, проверяются golden-файлами |
| Сериализация JSON-отчёта | diag_framework | Итоговый вывод отчёта, чувствителен к размеру |
| Загрузка JSON-конфигурации | config_loader | Задержка при запуске |
Рекомендуемый первый бенчмарк — парсер топологии, у которого уже есть тестовые данные golden-файлов:
#![allow(unused)]
fn main() {
// topology_lib/benches/parse_bench.rs (предложено)
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
use std::fs;
fn bench_topology_parse(c: &mut Criterion) {
let mut group = c.benchmark_group("topology_parse");
for golden_file in ["S2001", "S1015", "S1035", "S1080"] {
let path = format!("tests/test_data/{golden_file}.json");
let data = fs::read_to_string(&path).expect("golden-файл не найден");
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_function(golden_file, |b| {
b.iter(|| {
topology_lib::TopologyProfile::from_json_str(
criterion::black_box(&data)
)
});
});
}
group.finish();
}
criterion_group!(benches, bench_topology_parse);
criterion_main!(benches);
}
Попробуйте сами
-
Напишите бенчмарк Criterion: выберите любую функцию парсинга в своей кодовой базе. Создайте директорию
benches/, настройте бенчмарк Criterion, который измеряет пропускную способность в байтах в секунду. Запуститеcargo benchи изучите HTML-отчёт. -
Постройте флеймграф: соберите проект с
debug = trueв[profile.release], затем запуститеcargo flamegraph -- <ваши аргументы>. Определите три самых широких стека наверху флеймграфа — это и есть ваши горячие точки. -
Сравните с
hyperfine: установитеhyperfineи замерьте общее время выполнения бинарника с разными флагами. Сравните его с временами отдельных функций из Criterion. Какое время уходит там, что Criterion не видит? (Ответ: I/O, системные вызовы, запуск процесса.)
Выбор инструмента для бенчмарков
flowchart TD
START["Хотите измерить производительность?"] --> WHAT{"Какой уровень?"}
WHAT -->|"Отдельная функция"| CRITERION["Criterion.rs<br/>Статистика, обнаружение регрессий"]
WHAT -->|"Быстрая проверка функции"| DIVAN["Divan<br/>Легче, атрибутные макросы"]
WHAT -->|"Весь бинарник"| HYPERFINE["hyperfine<br/>Сквозной, реальное время"]
WHAT -->|"Поиск горячих точек"| PERF["perf + флеймграф<br/>Профилировщик с выборкой CPU"]
CRITERION --> CI_BENCH["Непрерывный бенчмаркинг<br/>в GitHub Actions"]
PERF --> OPTIMIZE["Оптимизация по профилю<br/>(PGO)"]
style CRITERION fill:#91e5a3,color:#000
style DIVAN fill:#91e5a3,color:#000
style HYPERFINE fill:#e3f2fd,color:#000
style PERF fill:#ffd43b,color:#000
style CI_BENCH fill:#e3f2fd,color:#000
style OPTIMIZE fill:#ffd43b,color:#000
🏋️ Упражнения
🟢 Упражнение 1: первый бенчмарк Criterion
Создайте крейт с функцией, которая сортирует Vec<u64> из 10 000 случайных элементов.
Напишите для неё бенчмарк Criterion, затем переключитесь на .sort_unstable() и посмотрите
разницу в производительности в HTML-отчёте.
Решение
# Cargo.toml
[[bench]]
name = "sort_bench"
harness = false
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
rand = "0.8"
#![allow(unused)]
fn main() {
// benches/sort_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::Rng;
fn generate_data(n: usize) -> Vec<u64> {
let mut rng = rand::thread_rng();
(0..n).map(|_| rng.gen()).collect()
}
fn bench_sort(c: &mut Criterion) {
let mut group = c.benchmark_group("sort-10k");
group.bench_function("stable", |b| {
b.iter_batched(
|| generate_data(10_000),
|mut data| { data.sort(); black_box(&data); },
criterion::BatchSize::SmallInput,
)
});
group.bench_function("unstable", |b| {
b.iter_batched(
|| generate_data(10_000),
|mut data| { data.sort_unstable(); black_box(&data); },
criterion::BatchSize::SmallInput,
)
});
group.finish();
}
criterion_group!(benches, bench_sort);
criterion_main!(benches);
}
cargo bench
open target/criterion/sort-10k/report/index.html
🟡 Упражнение 2: горячая точка на флеймграфе
Соберите проект с debug = true в [profile.release], затем постройте флеймграф.
Определите три самых широких стека.
Решение
# Cargo.toml
[profile.release]
debug = true # Сохраняем символы для флеймграфа
cargo install flamegraph
cargo flamegraph --release -- <ваши аргументы>
# Откроет flamegraph.svg в браузере
# Самые широкие стеки наверху — это и есть ваши горячие точки
Ключевые выводы
- Никогда не замеряйте бенчмарки через
Instant::now()— используйте Criterion.rs для статистической строгости и обнаружения регрессий black_box()не даёт компилятору оптимизировать цель бенчмарка «в ничто»hyperfineизмеряет реальное время всего бинарника, а Criterion — отдельных функций: используйте оба инструмента- Флеймграфы показывают, где тратится время; бенчмарки показывают, сколько времени тратится
- Непрерывный бенчмаркинг в CI ловит регрессии производительности до выпуска
Покрытие кода — видим то, что пропускают тесты 🟢
Чему вы научитесь:
- Покрытие на основе исходников с
cargo-llvm-cov(самый точный инструмент покрытия для Rust)- Быстрые проверки покрытия с
cargo-tarpaulinиgrcovот Mozilla- Настройка порогов покрытия в CI с помощью Codecov и Coveralls
- Стратегия тестирования, управляемая покрытием, которая в первую очередь закрывает опасные слепые зоны
Перекрёстные ссылки: Miri и санитайзеры — покрытие находит непротестированный код, Miri находит UB в протестированном · Бенчмаркинг — покрытие показывает что протестировано, бенчмарки — что быстро · CI/CD-конвейер — порог покрытия в конвейере
Покрытие кода показывает, какие строки, ветви или функции ваши тесты на самом деле выполняют. Оно не доказывает корректность (покрытая строка всё равно может содержать ошибку), но надёжно выявляет слепые зоны — пути в коде, которые не проверяет ни один тест.
В проекте 1 006 тестов в нескольких крейтах, то есть вложения в тестирование серьёзные. Анализ покрытия отвечает на вопрос: «Доходят ли эти вложения до кода, который важен?»
Покрытие на основе исходного кода с llvm-cov
Rust использует LLVM, который предоставляет инструментирование покрытия на основе
исходного кода — самый точный из доступных методов. Рекомендуемый инструмент —
cargo-llvm-cov:
# Установка
cargo install cargo-llvm-cov
# Или через компонент rustup (для «сырых» инструментов LLVM)
rustup component add llvm-tools-preview
Базовое использование:
# Запустить тесты и показать сводку покрытия по файлам
cargo llvm-cov
# Сгенерировать HTML-отчёт (просмотр с построчной подсветкой)
cargo llvm-cov --html
# Результат: target/llvm-cov/html/index.html
# Сгенерировать отчёт в формате LCOV (для интеграций с CI)
cargo llvm-cov --lcov --output-path lcov.info
# Покрытие всего воркспейса (все крейты)
cargo llvm-cov --workspace
# Включить только определённые пакеты
cargo llvm-cov --package accel_diag --package topology_lib
# Покрытие с учётом doc-тестов
cargo llvm-cov --doctests
Чтение HTML-отчёта:
target/llvm-cov/html/index.html
├── Файл │ Функции │ Строки │ Ветви │ Регионы
├─ accel_diag/src/lib.rs │ 78.5% │ 82.3% │ 61.2% │ 74.1%
├─ sel_mgr/src/parse.rs│ 95.2% │ 96.8% │ 88.0% │ 93.5%
├─ topology_lib/src/.. │ 91.0% │ 93.4% │ 79.5% │ 89.2%
└─ ...
Зелёный = покрыто Красный = не покрыто Жёлтый = покрыто частично (ветвление)
Типы покрытия:
| Тип | Что измеряет | Значение |
|---|---|---|
| Покрытие строк | Какие строки исходника выполнялись | Базовый вопрос: «до этого кода вообще дошло?» |
| Покрытие ветвлений | Какие ветки if/match выполнялись | Выявляет непроверенные условия |
| Покрытие функций | Какие функции вызывались | Находит мёртвый код |
| Покрытие регионов | Какие регионы кода (подвыражения) выполнялись | Самый детальный уровень |
cargo-tarpaulin — быстрый путь
cargo-tarpaulin — инструмент покрытия,
работающий только в Linux, который проще настроить (не нужны компоненты LLVM):
# Установка
cargo install cargo-tarpaulin
# Базовый отчёт о покрытии
cargo tarpaulin
# HTML-вывод
cargo tarpaulin --out Html
# С дополнительными опциями
cargo tarpaulin \
--workspace \
--timeout 120 \
--out Xml Html \
--output-dir coverage/ \
--exclude-files "*/tests/*" "*/benches/*" \
--ignore-panics
# Пропустить некоторые крейты
cargo tarpaulin --workspace --exclude diag_tool # исключить крейт-бинарник
Сравнение tarpaulin и llvm-cov:
| Возможность | cargo-llvm-cov | cargo-tarpaulin |
|---|---|---|
| Точность | На основе исходников (самый точный) | На основе ptrace (иногда завышает покрытие) |
| Платформа | Любая (на основе LLVM) | Только Linux |
| Покрытие ветвлений | Да | Ограниченно |
| Doc-тесты | Да | Нет |
| Настройка | Нужен llvm-tools-preview | Самодостаточный |
| Скорость | Быстрее (инструментирование на этапе компиляции) | Медленнее (накладные расходы ptrace) |
| Стабильность | Очень стабильный | Иногда ложные срабатывания |
Рекомендация: используйте cargo-llvm-cov ради точности. cargo-tarpaulin — когда
нужна быстрая проверка без установки инструментов LLVM.
grcov — инструмент покрытия от Mozilla
grcov — агрегатор покрытия от Mozilla.
Он обрабатывает сырые данные профилирования LLVM и формирует отчёты в нескольких форматах:
# Установка
cargo install grcov
# Шаг 1: сборка с инструментированием покрытия
export RUSTFLAGS="-Cinstrument-coverage"
export LLVM_PROFILE_FILE="target/coverage/%p-%m.profraw"
cargo build --tests
# Шаг 2: запуск тестов (генерирует файлы .profraw)
cargo test
# Шаг 3: агрегация с помощью grcov
grcov target/coverage/ \
--binary-path target/debug/ \
--source-dir . \
--output-types html,lcov \
--output-path target/coverage/report \
--branch \
--ignore-not-existing \
--ignore "*/tests/*" \
--ignore "*/.cargo/*"
# Шаг 4: просмотр отчёта
open target/coverage/report/html/index.html
Когда использовать grcov: он полезнее всего, когда нужно объединить покрытие из нескольких запусков тестов (например, модульных, интеграционных и фаззинг-тестов) в один отчёт.
Покрытие в CI: Codecov и Coveralls
Загружайте данные о покрытии в сервис мониторинга, чтобы видеть исторические тренды и аннотации в PR:
# .github/workflows/coverage.yml
name: Покрытие кода
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- name: Установка cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Генерация покрытия
run: cargo llvm-cov --workspace --lcov --output-path lcov.info
- name: Загрузка в Codecov
uses: codecov/codecov-action@v4
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
# Опционально: задать минимальный порог покрытия
- name: Проверка порога покрытия
run: |
cargo llvm-cov --workspace --fail-under-lines 80
# Сборка падает, если покрытие строк опускается ниже 80%
Пороги покрытия — задавайте минимумы для каждого крейта, читая JSON-вывод:
# Получить покрытие по крейтам в формате JSON
cargo llvm-cov --workspace --json | jq '.data[0].totals.lines.percent'
# Упасть, если покрытие ниже порога
cargo llvm-cov --workspace --fail-under-lines 80
cargo llvm-cov --workspace --fail-under-functions 70
cargo llvm-cov --workspace --fail-under-regions 60
Стратегия тестирования, управляемая покрытием
Сами по себе цифры покрытия бессмысленны без стратегии. Вот как эффективно использовать данные о покрытии:
Шаг 1: сортировка по риску
Высокое покрытие, высокий риск → ✅ Хорошо — поддерживайте
Высокое покрытие, низкий риск → 🔄 Возможно, избыточно протестировано — пропускайте, если медленно
Низкое покрытие, высокий риск → 🔴 Пишите тесты СЕЙЧАС — здесь прячутся баги
Низкое покрытие, низкий риск → 🟡 Отслеживайте, но не паникуйте
Шаг 2: ориентируйтесь на покрытие ветвлений, а не строк
#![allow(unused)]
fn main() {
// 100% покрытие строк, 50% покрытие ветвлений — всё ещё рискованно!
pub fn classify_temperature(temp_c: i32) -> ThermalState {
if temp_c > 105 { // ← проверено при temp=110 → Critical
ThermalState::Critical
} else if temp_c > 85 { // ← проверено при temp=90 → Warning
ThermalState::Warning
} else if temp_c < -10 { // ← НИКОГДА НЕ ПРОВЕРЯЛОСЬ → случай ошибки датчика пропущен
ThermalState::SensorError
} else {
ThermalState::Normal // ← проверено при temp=25 → Normal
}
}
}
Шаг 3: отсекайте шум
# Исключаем тестовый код из покрытия (он всегда «покрыт»)
cargo llvm-cov --workspace --ignore-filename-regex 'tests?\.rs$|benches/'
# Исключаем сгенерированный код
cargo llvm-cov --workspace --ignore-filename-regex 'target/'
В коде отмечайте участки, которые нельзя протестировать:
#![allow(unused)]
fn main() {
// Инструменты покрытия распознают этот паттерн
#[cfg(not(tarpaulin_include))] // tarpaulin
fn unreachable_hardware_path() {
// Этот путь требует реального GPU, чтобы сработать
}
// Для llvm-cov используйте более точный подход:
// просто примите, что некоторые пути нуждаются в интеграционных и аппаратных тестах,
// а не в модульных. Отслеживайте их в списке исключений покрытия.
}
Дополнительные инструменты тестирования
proptest — property-based тестирование находит крайние случаи, которые пропускают
написанные вручную тесты:
[dev-dependencies]
proptest = "1"
#![allow(unused)]
fn main() {
use proptest::prelude::*;
proptest! {
#[test]
fn parse_never_panics(input in "\\PC*") {
// proptest генерирует тысячи случайных строк
// Если parse_gpu_csv паникует на каком-либо входе, тест падает,
// и proptest минимизирует упавший пример за вас.
let _ = parse_gpu_csv(&input);
}
#[test]
fn temperature_roundtrip(raw in 0u16..4096) {
let temp = Temperature::from_raw(raw);
let md = temp.millidegrees_c();
// Свойство: значение в тысячных долях градуса всегда должно выводиться из raw
assert_eq!(md, (raw as i32) * 625 / 10);
}
}
}
insta — snapshot-тестирование для больших структурированных выводов (JSON, текстовые отчёты):
[dev-dependencies]
insta = { version = "1", features = ["json"] }
#![allow(unused)]
fn main() {
#[test]
fn test_der_report_format() {
let report = generate_der_report(&test_results);
// При первом запуске создаётся файл снимка. При последующих — сравнение с ним.
// Запустите `cargo insta review`, чтобы принять изменения в интерактивном режиме.
insta::assert_json_snapshot!(report);
}
}
Когда добавлять proptest и insta: если ваши модульные тесты — это только примеры «счастливого пути», proptest найдёт крайние случаи, которые вы упустили. Если вы тестируете большие форматы вывода (JSON-отчёты, записи DER), снимки insta писать и поддерживать быстрее, чем ручные assert-ы.
Применение: карта покрытия для более чем 1000 тестов
В проекте больше 1000 тестов, но отслеживания покрытия нет. Его добавление покажет, как распределены тестовые усилия. Непокрытые пути — первые кандидаты на проверку с помощью Miri и санитайзеров:
Рекомендуемая конфигурация покрытия:
# Быстрое покрытие всего воркспейса (предлагаемая команда для CI)
cargo llvm-cov --workspace \
--ignore-filename-regex 'tests?\.rs$' \
--fail-under-lines 75 \
--html
# Покрытие по крейтам для целенаправленного улучшения
for crate in accel_diag event_log topology_lib network_diag compute_diag fan_diag; do
echo "=== $crate ==="
cargo llvm-cov --package "$crate" --json 2>/dev/null | \
jq -r '.data[0].totals | "Строки: \(.lines.percent | round)% Ветви: \(.branches.percent | round)%"'
done
Крейты с ожидаемо высоким покрытием (исходя из плотности тестов):
topology_lib— набор тестов golden-файлов на 922 строкиevent_log— реестр с хелперамиcreate_test_record()cable_diag— паттерныmake_test_event()/make_test_context()
Ожидаемые пробелы в покрытии (по результатам просмотра кода):
- Ветки обработки ошибок в путях IPMI-коммуникации
- Ветки, специфичные для железа GPU (требуют реального GPU)
- Крайние случаи парсинга
dmesg(вывод зависит от платформы)
Правило 80/20 для покрытия: от 0% до 80% добраться несложно. От 80% до 95% требует всё более искусственных тестовых сценариев. От 95% до 100% требует исключений
#[cfg(not(...))]и редко стоит усилий. Ориентируйтесь на 80% покрытия строк и 70% покрытия ветвлений как на разумный практический минимум.
Устранение неполадок с покрытием
| Симптом | Причина | Решение |
|---|---|---|
llvm-cov показывает 0% для всех файлов | Инструментирование не применено | Запускайте cargo llvm-cov, а не cargo test и llvm-cov по отдельности |
Покрытие считает unreachable!() непокрытым | Эти ветки есть в скомпилированном коде | Используйте #[cfg(not(tarpaulin_include))] или добавьте в регулярное выражение исключений |
| Тестовый бинарник падает под покрытием | Конфликт инструментирования и санитайзера | Не комбинируйте cargo llvm-cov с -Zsanitizer=address; запускайте их отдельно |
Покрытие различается между llvm-cov и tarpaulin | Разные методы инструментирования | Считайте источником истины llvm-cov (встроен в компилятор); заводите задачи при крупных расхождениях |
error: profraw file is malformed | Тестовый бинарник упал во время выполнения | Сначала исправьте падающий тест; profraw-файлы повреждаются, если процесс завершился аварийно |
| Покрытие ветвлений кажется невероятно низким | Оптимизатор создаёт ветки для ветвей match, unwrap и т. д. | Для практических порогов ориентируйтесь на покрытие строк; покрытие ветвлений по природе ниже |
Попробуйте сами
-
Измерьте покрытие своего проекта: запустите
cargo llvm-cov --workspace --htmlи откройте отчёт. Найдите три файла с самым низким покрытием. Они просто не тестируются или им по природе трудно быть протестированными (код, зависящий от железа)? -
Задайте порог покрытия: добавьте
cargo llvm-cov --workspace --fail-under-lines 60в CI. Намеренно закомментируйте тест и убедитесь, что CI падает. Затем поднимите порог до фактического покрытия проекта минус 2%. -
Покрытие ветвлений и строк: напишите функцию с
matchиз трёх веток и протестируйте только две из них. Сравните покрытие строк (может показать 66%) и покрытие ветвлений (может показать 50%). Какая метрика полезнее для вашего проекта?
Выбор инструмента покрытия
flowchart TD
START["Нужно покрытие кода?"] --> ACCURACY{"Приоритет?"}
ACCURACY -->|"Максимальная точность"| LLVM["cargo-llvm-cov<br/>На основе исходников, нативно для компилятора"]
ACCURACY -->|"Быстрая проверка"| TARP["cargo-tarpaulin<br/>Только Linux, быстрый"]
ACCURACY -->|"Агрегация нескольких запусков"| GRCOV["grcov<br/>Mozilla, объединяет профили"]
LLVM --> CI_GATE["Порог покрытия в CI<br/>--fail-under-lines 80"]
TARP --> CI_GATE
CI_GATE --> UPLOAD{"Куда загружать?"}
UPLOAD -->|"Codecov"| CODECOV["codecov/codecov-action"]
UPLOAD -->|"Coveralls"| COVERALLS["coverallsapp/github-action"]
style LLVM fill:#91e5a3,color:#000
style TARP fill:#e3f2fd,color:#000
style GRCOV fill:#e3f2fd,color:#000
style CI_GATE fill:#ffd43b,color:#000
🏋️ Упражнения
🟢 Упражнение 1: первый отчёт о покрытии
Установите cargo-llvm-cov, запустите его на любом проекте на Rust и откройте HTML-отчёт.
Найдите три файла с самым низким покрытием строк.
Решение
cargo install cargo-llvm-cov
cargo llvm-cov --workspace --html --open
# Отчёт сортирует файлы по покрытию — самые низкие внизу
# Ищите файлы ниже 50% — это ваши слепые зоны
🟡 Упражнение 2: порог покрытия в CI
Добавьте порог покрытия в workflow GitHub Actions, который падает, если покрытие строк опускается ниже 60%. Проверьте работу, закомментировав один тест.
Решение
# .github/workflows/coverage.yml
name: Покрытие
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- run: cargo install cargo-llvm-cov
- run: cargo llvm-cov --workspace --fail-under-lines 60
Закомментируйте тест, отправьте изменения и посмотрите, как workflow падает.
Ключевые выводы
cargo-llvm-cov— самый точный инструмент покрытия для Rust: он использует собственное инструментирование компилятора- Покрытие не доказывает корректность, но нулевое покрытие доказывает отсутствие тестов — используйте его, чтобы находить слепые зоны
- Задайте порог покрытия в CI (например,
--fail-under-lines 80), чтобы предотвращать регрессии - Не гонитесь за 100% покрытием — сосредоточьтесь на путях с высоким риском (обработка ошибок, unsafe, парсинг)
- Никогда не комбинируйте инструментирование покрытия с санитайзерами в одном запуске
Miri, Valgrind и санитайзеры — проверка unsafe-кода 🔴
Чему вы научитесь:
- Miri как интерпретатор MIR — что он ловит (алиасинг, UB, утечки) и что не может (FFI, системные вызовы)
- Valgrind memcheck, Helgrind (гонки данных), Callgrind (профилирование) и Massif (куча)
- Санитайзеры LLVM: ASan, MSan, TSan, LSan с nightly-сборкой
-Zbuild-stdcargo-fuzzдля поиска падений иloomдля проверки моделей конкурентности- Дерево решений для выбора подходящего инструмента проверки
Перекрёстные ссылки: Покрытие кода — покрытие находит непротестированные пути, Miri проверяет протестированные ·
no_stdи фичи — кодno_stdчасто требуетunsafe, который может проверить Miri · CI/CD-конвейер — джоба Miri в конвейере
Безопасный Rust гарантирует безопасность памяти и отсутствие гонок данных на этапе компиляции.
Но как только вы пишете unsafe — для FFI, собственных структур данных или ради
производительности, — эти гарантии становятся вашей ответственностью. Эта глава описывает
инструменты, которые проверяют, действительно ли ваш unsafe-код выполняет заявленные
контракты безопасности.
Miri — интерпретатор для unsafe Rust
Miri — это интерпретатор промежуточного представления Rust (Mid-level Intermediate Representation, MIR). Вместо компиляции в машинный код Miri выполняет программу шаг за шагом, проверяя на каждой операции отсутствие неопределённого поведения (UB).
# Установка Miri (компонент только для nightly)
rustup +nightly component add miri
# Запуск тестового набора под Miri
cargo +nightly miri test
# Запуск конкретного бинарника под Miri
cargo +nightly miri run
# Запуск конкретного теста
cargo +nightly miri test -- test_name
Как работает Miri:
Исходник → rustc → MIR → Miri интерпретирует MIR
│
├─ Отслеживает происхождение (provenance) каждого указателя
├─ Проверяет каждое обращение к памяти
├─ Проверяет выравнивание при каждом разыменовании
├─ Обнаруживает use-after-free
├─ Обнаруживает гонки данных (в многопоточном коде)
└─ Применяет правила Stacked Borrows / Tree Borrows
Что ловит Miri (а что не может)
Miri обнаруживает:
| Категория | Пример | Упадёт ли во время выполнения? |
|---|---|---|
| Выход за границы | ptr.add(100).read() за пределами выделения | Иногда (зависит от раскладки страниц) |
| Use-after-free | Чтение Box после освобождения через сырой указатель | Иногда (зависит от аллокатора) |
| Double free | Двойной вызов drop_in_place | Обычно |
| Невыровненный доступ | (ptr as *const u32).read() по нечётному адресу | На некоторых архитектурах |
| Некорректные значения | transmute::<u8, bool>(2) | Молча даёт неверный результат |
| Висящие ссылки | &*ptr, где ptr уже освобождён | Нет (молчаливое повреждение) |
| Гонки данных | Два потока, один пишет, без синхронизации | Периодически, трудно воспроизвести |
| Нарушение Stacked Borrows | Алиасинг &mut-ссылок | Нет (молчаливое повреждение) |
Miri НЕ обнаруживает:
| Ограничение | Почему |
|---|---|
| Логические ошибки | Miri проверяет безопасность памяти, а не корректность |
| Взаимоблокировки | Miri проверяет гонки данных, а не livelock |
| Проблемы производительности | Интерпретация в 10–100× медленнее нативного выполнения |
| Взаимодействие с ОС и железом | Miri не может эмулировать системные вызовы и ввод-вывод устройств |
| Все вызовы FFI | Не может интерпретировать C-код (только Rust MIR) |
| Полное покрытие путей | Проверяет только те пути, до которых доходит ваш тестовый набор |
Конкретный пример — поиск некорректного кода, который «работает» на практике:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
#[test]
fn test_miri_catches_ub() {
// Это «работает» в release-сборках, но является неопределённым поведением
let mut v = vec![1, 2, 3];
let ptr = v.as_ptr();
// Push может перевыделить буфер, и тогда ptr станет недействительным
v.push(4);
// ❌ UB: ptr может висеть после перевыделения
// Miri поймает это, даже если аллокатор случайно не переместит буфер.
// let _val = unsafe { *ptr };
// Ошибка: Miri сообщит:
// "pointer to alloc1234 was dereferenced after this
// allocation got freed"
// ✅ Правильно: получаем новый указатель после изменения
let ptr = v.as_ptr();
let val = unsafe { *ptr };
assert_eq!(val, 1);
}
}
}
Запуск Miri на реальном крейте
Практический workflow Miri для крейта с unsafe:
# Шаг 1: запустите все тесты под Miri
cargo +nightly miri test 2>&1 | tee miri_output.txt
# Шаг 2: если Miri сообщил об ошибках, изолируйте их
cargo +nightly miri test -- failing_test_name
# Шаг 3: используйте бэктрейс Miri для диагностики
MIRIFLAGS="-Zmiri-backtrace=full" cargo +nightly miri test
# Шаг 4: выберите модель заимствований
# Stacked Borrows (по умолчанию, строже):
cargo +nightly miri test
# Tree Borrows (экспериментальная, менее строгая):
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test
Флаги Miri для типичных сценариев:
# Отключить изоляцию (разрешить доступ к файловой системе и переменным окружения)
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test
# Проверка утечек памяти включена в Miri по умолчанию.
# Чтобы подавить ошибки утечек (например, для намеренных утечек):
# MIRIFLAGS="-Zmiri-ignore-leaks" cargo +nightly miri test
# Задать seed для ГСЧ — воспроизводимые результаты в тестах с рандомизацией
MIRIFLAGS="-Zmiri-seed=42" cargo +nightly miri test
# Включить строгую проверку provenance
MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test
# Несколько флагов сразу
MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-backtrace=full -Zmiri-strict-provenance" \
cargo +nightly miri test
Miri в CI:
# .github/workflows/miri.yml
name: Miri
on: [push, pull_request]
jobs:
miri:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: miri
- name: Запуск Miri
run: cargo miri test --workspace
env:
MIRIFLAGS: "-Zmiri-backtrace=full"
# Проверка утечек включена по умолчанию.
# Пропускайте тесты, которые используют системные вызовы, с которыми Miri не справляется
# (файловый ввод-вывод, сеть и т. п.)
Замечание о производительности: Miri в 10–100× медленнее нативного выполнения. Тестовый набор, который нативно выполняется за 5 секунд, под Miri может идти 5 минут. В CI запускайте Miri на выбранном подмножестве: только крейты с
unsafe-кодом.
Valgrind и его интеграция с Rust
Valgrind — классический инструмент проверки памяти для C/C++. Он работает и со скомпилированными бинарниками Rust, проверяя ошибки памяти на уровне машинного кода.
# Установка Valgrind
sudo apt install valgrind # Debian/Ubuntu
sudo dnf install valgrind # Fedora
# Сборка с отладочной информацией (Valgrind нужны символы)
cargo build --tests
# или для release с отладочной информацией:
# cargo build --release
# [profile.release]
# debug = true
# Запуск конкретного тестового бинарника под Valgrind
valgrind --tool=memcheck \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
./target/debug/deps/my_crate-abc123 --test-threads=1
# Запуск основного бинарника
valgrind --tool=memcheck \
--leak-check=full \
--error-exitcode=1 \
./target/debug/diag_tool --run-diagnostics
Инструменты Valgrind помимо memcheck:
| Инструмент | Команда | Что обнаруживает |
|---|---|---|
| Memcheck | --tool=memcheck | Утечки памяти, use-after-free, переполнение буфера |
| Helgrind | --tool=helgrind | Гонки данных и нарушения порядка захвата блокировок |
| DRD | --tool=drd | Гонки данных (другой алгоритм обнаружения) |
| Callgrind | --tool=callgrind | Профилирование количества инструкций CPU (на уровне путей) |
| Massif | --tool=massif | Профилирование кучи во времени |
| Cachegrind | --tool=cachegrind | Анализ промахов кеша |
Callgrind для профилирования на уровне инструкций:
# Подсчёт инструкций (стабильнее, чем измерение реального времени)
valgrind --tool=callgrind \
--callgrind-out-file=callgrind.out \
./target/release/diag_tool --run-diagnostics
# Визуализация в KCachegrind
kcachegrind callgrind.out
# или текстовый вариант:
callgrind_annotate callgrind.out | head -100
Miri или Valgrind — когда что использовать:
| Аспект | Miri | Valgrind |
|---|---|---|
| Проверяет UB, специфичный для Rust | ✅ Stacked/Tree Borrows | ❌ Не знает правил Rust |
| Проверяет C FFI-код | ❌ Не может интерпретировать C | ✅ Проверяет весь машинный код |
| Нужен nightly | ✅ Да | ❌ Нет |
| Скорость | В 10–100× медленнее | В 10–50× медленнее |
| Платформа | Любая (интерпретирует MIR) | Linux, macOS (выполняет нативный код) |
| Обнаружение гонок данных | ✅ Да | ✅ Да (Helgrind/DRD) |
| Обнаружение утечек | ✅ Да | ✅ Да (более тщательно) |
| Ложные срабатывания | Очень редко | Иногда (особенно с аллокаторами) |
Используйте оба:
- Miri — для чистого Rust
unsafe(Stacked Borrows, provenance) - Valgrind — для кода с большим количеством FFI и для анализа утечек всей программы
AddressSanitizer, MemorySanitizer, ThreadSanitizer
Санитайзеры LLVM — это проходы инструментирования на этапе компиляции, которые добавляют проверки времени выполнения. Они быстрее Valgrind (накладные расходы 2–5× против 10–50×) и ловят другие классы ошибок.
# Нужно: установите исходники Rust для пересборки std с инструментированием санитайзера
rustup component add rust-src --toolchain nightly
# AddressSanitizer (ASan) — переполнение буфера, use-after-free, переполнение стека
RUSTFLAGS="-Zsanitizer=address" \
cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu
# MemorySanitizer (MSan) — чтение неинициализированной памяти
RUSTFLAGS="-Zsanitizer=memory" \
cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu
# ThreadSanitizer (TSan) — гонки данных
RUSTFLAGS="-Zsanitizer=thread" \
cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu
# LeakSanitizer (LSan) — утечки памяти (по умолчанию входит в ASan)
RUSTFLAGS="-Zsanitizer=leak" \
cargo +nightly test --target x86_64-unknown-linux-gnu
Примечание: ASan, MSan и TSan требуют
-Zbuild-std, чтобы пересобрать стандартную библиотеку с инструментированием санитайзера. LSan — не требует.
Сравнение санитайзеров:
| Санитайзер | Накладные расходы | Что ловит | Nightly? | -Zbuild-std? |
|---|---|---|---|---|
| ASan | 2× память, 2× CPU | Переполнение буфера, use-after-free, переполнение стека | Да | Да |
| MSan | 3× память, 3× CPU | Чтение неинициализированных данных | Да | Да |
| TSan | 5–10× память, 5× CPU | Гонки данных | Да | Да |
| LSan | Минимальные | Утечки памяти | Да | Нет |
Практический пример — ловим гонку данных с помощью TSan:
#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;
fn racy_counter() -> u64 {
// ❌ UB: неслинхронизированное разделяемое изменяемое состояние
let data = Arc::new(std::cell::UnsafeCell::new(0u64));
let mut handles = vec![];
for _ in 0..4 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// SAFETY: НЕКОРРЕКТНО — гонка данных!
unsafe {
*data.get() += 1;
}
}
}));
}
for h in handles {
h.join().unwrap();
}
// Значение должно быть 4000, но из-за гонки может оказаться любым
unsafe { *data.get() }
}
// И Miri, и TSan находят это:
// Miri: "Data race detected between (1) write and (2) write"
// TSan: "WARNING: ThreadSanitizer: data race"
//
// Исправление: используйте AtomicU64 или Mutex<u64>
}
Смежные инструменты: фаззинг и проверка конкурентности
cargo-fuzz — фаззинг с обратной связью по покрытию (находит падения в парсерах и декодерах):
# Установка
cargo install cargo-fuzz
# Инициализация цели фаззинга
cargo fuzz init
cargo fuzz add parse_gpu_csv
#![allow(unused)]
fn main() {
// fuzz/fuzz_targets/parse_gpu_csv.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
// Фаззер генерирует миллионы входных данных в поисках паник и падений.
let _ = diag_tool::parse_gpu_csv(s);
}
});
}
# Запуск фаззера (работает до прерывания или до обнаружения падения)
cargo +nightly fuzz run parse_gpu_csv -- -max_total_time=300 # 5 минут
# Минимизация падения
cargo +nightly fuzz tmin parse_gpu_csv artifacts/parse_gpu_csv/crash-...
Когда фаззить: любая функция, которая разбирает недоверенные или полудоверенные входные данные (вывод датчиков, конфигурационные файлы, сетевые данные, JSON/CSV). Фаззинг находил реальные ошибки во всех крупных крейтах-парсерах Rust (serde, regex, image).
loom — проверка моделей конкурентности (исчерпывающе перебирает порядки атомарных операций):
[dev-dependencies]
loom = "0.7"
#![allow(unused)]
fn main() {
#[cfg(loom)]
mod tests {
use loom::sync::atomic::{AtomicUsize, Ordering};
use loom::thread;
#[test]
fn test_counter_is_atomic() {
loom::model(|| {
let counter = loom::sync::Arc::new(AtomicUsize::new(0));
let c1 = counter.clone();
let c2 = counter.clone();
let t1 = thread::spawn(move || { c1.fetch_add(1, Ordering::SeqCst); });
let t2 = thread::spawn(move || { c2.fetch_add(1, Ordering::SeqCst); });
t1.join().unwrap();
t2.join().unwrap();
// loom перебирает ВСЕ возможные чередования потоков
assert_eq!(counter.load(Ordering::SeqCst), 2);
});
}
}
}
Когда использовать
loom: когда у вас есть структуры данных без блокировок или собственные примитивы синхронизации. Loom исчерпывающе перебирает чередования потоков — это проверка модели, а не стресс-тест. Для кода на основеMutex/RwLockон не нужен.
Когда какой инструмент использовать
Дерево решений для проверки unsafe-кода:
Код написан на чистом Rust (без FFI)?
├─ Да → Используйте Miri (ловит UB, специфичный для Rust, Stacked Borrows)
│ В CI также запускайте ASan для защиты в глубину
└─ Нет (вызывает C/C++ код через FFI)
├─ Есть опасения по безопасности памяти?
│ └─ Да → Используйте Valgrind memcheck И ASan
├─ Есть опасения по конкурентности?
│ └─ Да → Используйте TSan (быстрее) или Helgrind (тщательнее)
└─ Есть опасения по утечкам памяти?
└─ Да → Используйте Valgrind --leak-check=full
Рекомендуемая матрица CI:
# Запускаем все инструменты параллельно для быстрой обратной связи
jobs:
miri:
runs-on: ubuntu-latest
steps:
- uses: dtolnay/rust-toolchain@nightly
with: { components: miri }
- run: cargo miri test --workspace
asan:
runs-on: ubuntu-latest
steps:
- uses: dtolnay/rust-toolchain@nightly
- run: |
RUSTFLAGS="-Zsanitizer=address" \
cargo test -Zbuild-std --target x86_64-unknown-linux-gnu
valgrind:
runs-on: ubuntu-latest
steps:
- run: sudo apt-get install -y valgrind
- uses: dtolnay/rust-toolchain@stable
- run: cargo build --tests
- run: |
for test_bin in $(find target/debug/deps -maxdepth 1 -executable -type f ! -name '*.d'); do
valgrind --error-exitcode=1 --leak-check=full "$test_bin" --test-threads=1
done
Применение: ноль unsafe — и когда он понадобится
В проекте ни одного блока unsafe на более чем 90 тысячах строк Rust. Для инструмента
диагностики системного уровня это замечательное достижение, которое показывает, что безопасного
Rust достаточно для:
- Связи с IPMI (через
std::process::Commandкipmitool) - Запросов к GPU (через
std::process::Commandкaccel-query) - Разбора топологии PCIe (чистый разбор JSON и текста)
- Управления записями SEL (чистые структуры данных)
- Генерации DER-отчётов (сериализация JSON)
Когда проекту понадобится unsafe?
Вероятные причины появления unsafe:
| Сценарий | Почему нужен unsafe | Рекомендуемая проверка |
|---|---|---|
| Прямой IPMI через ioctl | libc::ioctl() обходит подпроцесс ipmitool | Miri + Valgrind |
| Прямые запросы к драйверу GPU | FFI к accel-mgmt вместо разбора вывода accel-query | Valgrind (C-библиотека) |
| Memory-mapped конфигурация PCIe | mmap для прямого чтения конфигурационного пространства | ASan + Valgrind |
| Буфер SEL без блокировок | AtomicPtr для параллельного сбора событий | Miri + TSan |
Вариант для встраиваемых систем / no_std | Манипуляции с сырыми указателями для bare-metal | Miri |
Подготовка: прежде чем вводить unsafe, добавьте инструменты проверки в CI:
# Cargo.toml — добавляем флаги фич для unsafe-оптимизаций
[features]
default = []
direct-ipmi = [] # Включить прямой ioctl-доступ к IPMI вместо подпроцесса ipmitool
direct-accel-api = [] # Включить FFI accel-mgmt вместо разбора вывода accel-query
#![allow(unused)]
fn main() {
// src/ipmi.rs — за флагом фичи
#[cfg(feature = "direct-ipmi")]
mod direct {
//! Прямой доступ к IPMI-устройству через ioctl на /dev/ipmi0.
//!
//! # Safety
//! Этот модуль использует `unsafe` для системных вызовов ioctl.
//! Проверено: Miri (где возможно), Valgrind memcheck, ASan.
use std::os::unix::io::RawFd;
// ... реализация unsafe ioctl ...
}
#[cfg(not(feature = "direct-ipmi"))]
mod subprocess {
//! IPMI через подпроцесс ipmitool (по умолчанию, полностью безопасно).
// ... текущая реализация ...
}
}
Ключевая мысль: держите
unsafeза флагами фич, чтобы его можно было проверять независимо. Запускайтеcargo +nightly miri test --features direct-ipmiв CI, чтобы непрерывно проверять unsafe-пути, не затрагивая безопасную сборку по умолчанию.
cargo-careful — дополнительные проверки UB без Miri
cargo-careful запускает ваш код с включёнными
дополнительными проверками стандартной библиотеки. Он ловит часть неопределённого поведения,
которое обычные сборки игнорируют, не требуя nightly или замедления Miri в 10–100×:
# Установка (требуется nightly, но код выполняется почти с нативной скоростью)
cargo install cargo-careful
# Запуск тестов с дополнительными проверками UB (ловит неинициализированную память и некорректные значения)
cargo +nightly careful test
# Запуск бинарника с дополнительными проверками
cargo +nightly careful run -- --run-diagnostics
Что cargo-careful ловит, чего не ловят обычные сборки:
- Чтение неинициализированной памяти в
MaybeUninitиzeroed() - Создание некорректных значений
bool,charили перечислений через transmute - Невыровненное чтение и запись через указатели
copy_nonoverlappingс перекрывающимися диапазонами
Место в лестнице проверок:
Наименьшие накладные расходы Наибольшая тщательность
├─ cargo test ──► cargo careful test ──► Miri ──► ASan ──► Valgrind ─┤
│ (0× накладные) (~1.5× накладные) (10-100×) (2×) (10-50×) │
│ Только safe Rust Ловит часть UB Чистый Rust FFI+Rust FFI+Rust │
Рекомендация: добавьте
cargo +nightly careful testв CI как быструю проверку безопасности. Он работает почти с нативной скоростью (в отличие от Miri) и ловит реальные ошибки, которые маскируют безопасные абстракции Rust.
Устранение неполадок с Miri и санитайзерами
| Симптом | Причина | Решение |
|---|---|---|
Miri does not support FFI | Miri — интерпретатор Rust, он не может выполнять C-код | Для FFI-кода используйте Valgrind или ASan |
error: unsupported operation: can't call foreign function | Miri встретил вызов extern "C" | Замокайте границу FFI или ограничьте код через #[cfg(miri)] |
Stacked Borrows violation | Нарушение правила алиасинга — даже если код «работает» | Miri прав; перепишите код, чтобы не алиасить &mut с & |
Санитайзер сообщает DEADLYSIGNAL | ASan обнаружил переполнение буфера | Проверьте индексацию массивов, операции со срезами и арифметику указателей |
LeakSanitizer: detected memory leaks | Box::leak(), forget() или пропущенный drop() | Намеренно: подавите через __lsan_disable(); непреднамеренно: исправьте утечку |
| Miri невероятно медленный | Miri интерпретирует, а не компилирует — в 10–100× медленнее | Запускайте только тесты --lib или помечайте медленные тесты через #[cfg_attr(miri, ignore)] |
TSan: false positive с атомарными операциями | TSan не до конца понимает модель порядка атомарных операций Rust | Добавьте TSAN_OPTIONS=suppressions=tsan.supp с конкретными подавлениями |
Попробуйте сами
-
Вызовите обнаружение UB в Miri: напишите
unsafe-функцию, которая создаёт две ссылки&mutна один и тот жеi32(нарушение алиасинга). Запуститеcargo +nightly miri testи посмотрите на ошибку «Stacked Borrows». Исправьте её черезUnsafeCellили отдельные выделения. -
Запустите ASan на намеренной ошибке: создайте тест с
unsafe-доступом к массиву за пределами границ. Соберите сRUSTFLAGS="-Zsanitizer=address"и посмотрите отчёт ASan. Обратите внимание, как он указывает точную строку. -
Измерьте накладные расходы Miri: замерьте
cargo test --libиcargo +nightly miri test --libна одном и том же наборе тестов. Рассчитайте коэффициент замедления. По результатам решите, какие тесты запускать под Miri в CI, а какие пропускать через#[cfg_attr(miri, ignore)].
Дерево решений для проверки безопасности
flowchart TD
START["Есть unsafe-код?"] -->|Нет| SAFE["Безопасный Rust — проверка<br/>не нужна"]
START -->|Да| KIND{"Какой именно?"}
KIND -->|"Чистый Rust unsafe"| MIRI["Miri<br/>интерпретатор MIR<br/>ловит алиасинг, UB, утечки"]
KIND -->|"FFI / взаимодействие с C"| VALGRIND["Valgrind memcheck<br/>или ASan"]
KIND -->|"Конкурентный unsafe"| CONC{"Без блокировок?"}
CONC -->|"Атомарные операции / без блокировок"| LOOM["loom<br/>Проверка моделей для атомарных операций"]
CONC -->|"Mutex / разделяемое состояние"| TSAN["TSan или<br/>Miri -Zmiri-check-number-validity"]
MIRI --> CI_MIRI["CI: cargo +nightly miri test"]
VALGRIND --> CI_VALGRIND["CI: valgrind --leak-check=full"]
style SAFE fill:#91e5a3,color:#000
style MIRI fill:#e3f2fd,color:#000
style VALGRIND fill:#ffd43b,color:#000
style LOOM fill:#ff6b6b,color:#000
style TSAN fill:#ffd43b,color:#000
🏋️ Упражнения
🟡 Упражнение 1: вызовите обнаружение UB в Miri
Напишите unsafe-функцию, которая создаёт две ссылки &mut на один и тот же i32
(нарушение алиасинга). Запустите cargo +nightly miri test, найдите ошибку Stacked Borrows
и исправьте её.
Решение
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
#[test]
fn aliasing_ub() {
let mut x: i32 = 42;
let ptr = &mut x as *mut i32;
unsafe {
// ОШИБКА: две ссылки &mut на одно и то же место
let _a = &mut *ptr;
let _b = &mut *ptr; // Miri: нарушение Stacked Borrows!
}
}
}
}
Исправление: используйте отдельные выделения или UnsafeCell:
#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;
#[test]
fn no_aliasing_ub() {
let x = UnsafeCell::new(42);
unsafe {
let a = &mut *x.get();
*a = 100;
}
}
}
🔴 Упражнение 2: обнаружение выхода за границы с помощью ASan
Создайте тест с unsafe-доступом к массиву за пределами границ. Соберите на nightly с
RUSTFLAGS="-Zsanitizer=address" и посмотрите отчёт ASan.
Решение
#![allow(unused)]
fn main() {
#[test]
fn oob_access() {
let arr = [1u8, 2, 3, 4, 5];
let ptr = arr.as_ptr();
unsafe {
let _val = *ptr.add(10); // Выход за границы!
}
}
}
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std \
--target x86_64-unknown-linux-gnu -- oob_access
# Отчёт ASan: stack-buffer-overflow по адресу <точный адрес>
Ключевые выводы
- Miri — инструмент для чистого Rust
unsafe: он ловит нарушения алиасинга, use-after-free и утечки, которые компилируются и проходят тесты - Valgrind — инструмент для FFI и взаимодействия с C: он работает с готовым бинарником без перекомпиляции
- Санитайзеры (ASan, TSan, MSan) требуют nightly, но работают почти с нативной скоростью — идеально для больших тестовых наборов
loomсоздан специально для проверки структур данных без блокировок- Запускайте Miri в CI на каждый push, а санитайзеры — по ночному расписанию, чтобы не замедлять основной конвейер
Управление зависимостями и безопасность цепочки поставок 🟢
Чему вы научитесь:
- Поиск известных уязвимостей с помощью
cargo-audit- Соблюдение политик по лицензиям, рекомендациям и источникам с помощью
cargo-deny- Проверка доверия к цепочке поставок с помощью
cargo-vetот Mozilla- Отслеживание устаревших зависимостей и обнаружение несовместимых изменений API
- Визуализация и дедупликация дерева зависимостей
Перекрёстные ссылки: Профили релиза —
cargo-udepsубирает неиспользуемые зависимости, найденные здесь · CI/CD-конвейер — джобы audit и deny в конвейере · Build-скрипты —build-dependenciesтоже входят в вашу цепочку поставок
Бинарник на Rust содержит не только ваш код — он содержит все транзитивные зависимости
из Cargo.lock. Уязвимость, нарушение лицензии или вредоносный крейт в любой точке этого
дерева становятся вашей проблемой. Эта глава описывает инструменты, которые делают
управление зависимостями поддающимся аудиту и автоматизированным.
cargo-audit — поиск известных уязвимостей
cargo-audit сверяет ваш
Cargo.lock с базой рекомендаций RustSec, в которой отслеживаются
известные уязвимости опубликованных крейтов.
# Установка
cargo install cargo-audit
# Поиск известных уязвимостей
cargo audit
# Вывод:
# Crate: chrono
# Version: 0.4.19
# Title: Potential segfault in localtime_r invocations
# Date: 2020-11-10
# ID: RUSTSEC-2020-0159
# URL: https://rustsec.org/advisories/RUSTSEC-2020-0159
# Solution: Upgrade to >= 0.4.20
# Проверка с падением CI, если есть уязвимости
cargo audit --deny warnings
# Генерация JSON-вывода для автоматической обработки
cargo audit --json
# Исправление уязвимостей обновлением Cargo.lock
cargo audit fix
Интеграция с CI:
# .github/workflows/audit.yml
name: Проверка безопасности
on:
schedule:
- cron: '0 0 * * *' # Ежедневная проверка — рекомендации появляются постоянно
push:
paths: ['Cargo.lock']
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
cargo-deny — комплексное применение политик
cargo-deny выходит далеко за рамки поиска
уязвимостей. Он применяет политики по четырём направлениям:
- Рекомендации (advisories) — известные уязвимости (как и в cargo-audit)
- Лицензии — список разрешённых и запрещённых лицензий
- Ограничения (bans) — запрещённые крейты или дублирующиеся версии
- Источники (sources) — разрешённые реестры и git-источники
# Установка
cargo install cargo-deny
# Инициализация конфигурации
cargo deny init
# Создаёт deny.toml с документированными значениями по умолчанию
# Запуск всех проверок
cargo deny check
# Запуск отдельных проверок
cargo deny check advisories
cargo deny check licenses
cargo deny check bans
cargo deny check sources
Пример deny.toml:
# deny.toml
[advisories]
vulnerability = "deny" # Падать при известных уязвимостях
unmaintained = "warn" # Предупреждать о неподдерживаемых крейтах
yanked = "deny" # Падать на отозванных (yanked) крейтах
notice = "warn" # Предупреждать об информационных рекомендациях
[licenses]
unlicensed = "deny" # У всех крейтов должна быть лицензия
allow = [
"MIT",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Unicode-DFS-2016",
]
copyleft = "deny" # Никаких GPL/LGPL/AGPL в этом проекте
default = "deny" # Запрещать всё, что не разрешено явно
[bans]
multiple-versions = "warn" # Предупреждать, если один крейт присутствует в 2 версиях
wildcards = "deny" # Никаких path = "*" в зависимостях
highlight = "all" # Показывать все дубликаты, а не только первый
# Запрещаем отдельные проблемные крейты
deny = [
# openssl-sys подтягивает C-библиотеку OpenSSL — предпочитайте rustls
{ name = "openssl-sys", wrappers = ["native-tls"] },
]
# Разрешаем отдельные дублирующиеся версии (когда их не избежать)
[[bans.skip]]
name = "syn"
version = "1.0" # syn 1.x и 2.x часто сосуществуют
[sources]
unknown-registry = "deny" # Разрешён только crates.io
unknown-git = "deny" # Никаких случайных git-зависимостей
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
Контроль лицензий особенно ценен для коммерческих проектов:
# Проверить, какие лицензии есть в дереве зависимостей
cargo deny list
# Вывод:
# MIT — 127 crates
# Apache-2.0 — 89 crates
# BSD-3-Clause — 12 crates
# MPL-2.0 — 3 crates ← может потребоваться юридическая проверка
# Unicode-DFS — 1 crate
cargo-vet — проверка доверия к цепочке поставок
cargo-vet (от Mozilla) отвечает на другой вопрос:
не «есть ли у этого крейта известные баги?», а «проверил ли этот код доверенный человек?»
# Установка
cargo install cargo-vet
# Инициализация (создаёт директорию supply-chain/)
cargo vet init
# Проверить, какие крейты нужно проверить
cargo vet
# После проверки крейта подтвердите это:
cargo vet certify serde 1.0.203
# Фиксирует, что вы проверили serde 1.0.203 по своим критериям
# Импорт аудитов от доверенных организаций
cargo vet import mozilla
cargo vet import google
cargo vet import bytecode-alliance
Как это работает:
supply-chain/
├── audits.toml ← Сертификаты аудита вашей команды
├── config.toml ← Конфигурация доверия и критерии
└── imports.lock ← Зафиксированные импорты от других организаций
cargo-vet наиболее полезен организациям со строгими требованиями к цепочке поставок
(госсектор, финансы, инфраструктура). Для большинства команд cargo-deny даёт достаточную защиту.
cargo-outdated и cargo-semver-checks
cargo-outdated — поиск зависимостей, для которых есть более новые версии:
cargo install cargo-outdated
cargo outdated --workspace
# Вывод:
# Name Project Compat Latest Kind
# serde 1.0.193 1.0.203 1.0.203 Normal
# regex 1.9.6 1.10.4 1.10.4 Normal
# thiserror 1.0.50 1.0.61 2.0.3 Normal ← доступна мажорная версия
cargo-semver-checks — обнаружение несовместимых изменений API до публикации.
Незаменим для библиотечных крейтов:
cargo install cargo-semver-checks
# Проверить, совместимы ли ваши изменения с semver
cargo semver-checks
# Вывод:
# ✗ Function `parse_gpu_csv` is now private (was public)
# → Несовместимое изменение (BREAKING). Повысьте MAJOR-версию.
#
# ✗ Struct `GpuInfo` has a new required field `power_limit_w`
# → Несовместимое изменение (BREAKING). Повысьте MAJOR-версию.
#
# ✓ Function `parse_gpu_csv_v2` was added (non-breaking)
cargo-tree — визуализация и дедупликация зависимостей
cargo tree встроен в Cargo (ничего устанавливать не нужно) и незаменим для понимания
графа зависимостей:
# Полное дерево зависимостей
cargo tree
# Понять, почему включён конкретный крейт
cargo tree --invert --package openssl-sys
# Показывает все пути от вашего крейта к openssl-sys
# Найти дублирующиеся версии
cargo tree --duplicates
# Вывод:
# syn v1.0.109
# └── serde_derive v1.0.193
#
# syn v2.0.48
# ├── thiserror-impl v1.0.56
# └── tokio-macros v2.2.0
# Показать только прямые зависимости
cargo tree --depth 1
# Показать фичи зависимостей
cargo tree --format "{p} {f}"
# Подсчитать общее количество зависимостей
cargo tree | wc -l
Стратегия дедупликации: если cargo tree --duplicates показывает один и тот же крейт
в двух мажорных версиях, проверьте, можно ли обновить цепочку зависимостей, чтобы объединить
их. Каждый дубликат увеличивает время компиляции и размер бинарника.
Применение: гигиена зависимостей в мультикрейтовом проекте
Воркспейс использует [workspace.dependencies] для централизованного управления версиями —
отличная практика. В сочетании с cargo tree --duplicates
для анализа размера это предотвращает расхождение версий и уменьшает раздувание бинарника:
# Корневой Cargo.toml — все версии зафиксированы в одном месте
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
regex = "1.10"
thiserror = "1.0"
anyhow = "1.0"
rayon = "1.8"
Рекомендуемые дополнения для проекта:
# Добавить в CI-конвейер:
cargo deny init # Однократная настройка
cargo deny check # Каждый PR — лицензии, рекомендации, ограничения
cargo audit --deny warnings # Каждый push — поиск уязвимостей
cargo outdated --workspace # Раз в неделю — отслеживание доступных обновлений
Рекомендуемый deny.toml для проекта:
[advisories]
vulnerability = "deny"
yanked = "deny"
[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-DFS-2016"]
copyleft = "deny" # Инструмент диагностики железа — никакого copyleft
[bans]
multiple-versions = "warn" # Отслеживаем дубликаты, пока не блокируем
wildcards = "deny"
[sources]
unknown-registry = "deny"
unknown-git = "deny"
Конвейер аудита цепочки поставок
flowchart LR
PR["Pull Request"] --> AUDIT["cargo audit<br/>Известные CVE"]
AUDIT --> DENY["cargo deny check<br/>Лицензии + Ограничения + Источники"]
DENY --> OUTDATED["cargo outdated<br/>По расписанию, раз в неделю"]
OUTDATED --> SEMVER["cargo semver-checks<br/>Только библиотечные крейты"]
AUDIT -->|"Ошибка"| BLOCK["❌ Блокировать слияние"]
DENY -->|"Ошибка"| BLOCK
SEMVER -->|"Несовместимое изменение"| BUMP["Поднять мажорную версию"]
style BLOCK fill:#ff6b6b,color:#000
style BUMP fill:#ffd43b,color:#000
style PR fill:#e3f2fd,color:#000
🏋️ Упражнения
🟢 Упражнение 1: проверьте свои зависимости
Запустите cargo audit и cargo deny init && cargo deny check на любом проекте на Rust.
Сколько рекомендаций найдено? Сколько категорий лицензий в вашем дереве?
Решение
cargo audit
# Обратите внимание на рекомендации — чаще всего это chrono, time или старые крейты
cargo deny init
cargo deny list
# Показывает разбивку по лицензиям: MIT (N), Apache-2.0 (N) и т. д.
cargo deny check
# Показывает полную проверку по всем четырём направлениям
🟡 Упражнение 2: найдите и устраните дублирующиеся зависимости
Запустите cargo tree --duplicates на воркспейсе. Найдите крейт, который присутствует в двух
версиях. Можно ли обновить Cargo.toml, чтобы их объединить? Измерьте влияние на время
компиляции и размер бинарника.
Решение
cargo tree --duplicates
# Типично: syn 1.x и syn 2.x
# Выясняем, кто подтягивает старую версию:
cargo tree --invert --package syn@1.0.109
# Вывод: serde_derive 1.0.xxx -> syn 1.0.109
# Проверяем, использует ли новый serde_derive syn 2.x:
cargo update -p serde_derive
cargo tree --duplicates
# Если syn 1.x исчез, вы устранили дубликат
# Измеряем влияние:
time cargo build --release # До и после
cargo bloat --release --crates | head -20
Ключевые выводы
cargo auditловит известные CVE — запускайте его на каждый push и по ежедневному расписаниюcargo denyприменяет политики по четырём направлениям: рекомендации, лицензии, ограничения и источники- Используйте
[workspace.dependencies], чтобы централизовать управление версиями в мультикрейтовом воркспейсе cargo tree --duplicatesвыявляет раздувание: каждый дубликат увеличивает время компиляции и размер бинарникаcargo-vetпредназначен для сред с высокими требованиями к безопасности; для большинства команд достаточноcargo-deny
Профили релиза и размер бинарного файла 🟡
Чему вы научитесь:
- Устройство профиля релиза: LTO, codegen-units, стратегия паники, strip, opt-level
- Компромиссы thin LTO, fat LTO и кросс-языкового LTO
- Анализ размера бинарника с помощью
cargo-bloat- Сокращение зависимостей с помощью
cargo-udeps,cargo-macheteиcargo-shearПерекрёстные ссылки: Инструменты времени компиляции — вторая половина оптимизации · Бенчмаркинг — измеряйте время выполнения до оптимизации · Зависимости — сокращение зависимостей уменьшает и размер, и время компиляции
Стандартный cargo build --release уже хорош. Но для продакшн-развёртывания — особенно
инструментов из одного бинарника, которые ставятся на тысячи серверов, — между «хорошо»
и «оптимизировано» есть существенная разница. Эта глава описывает ручки настройки профиля
и инструменты для измерения размера бинарника.
Устройство профиля релиза
Профили Cargo определяют, как rustc компилирует ваш код. Значения по умолчанию
консервативны — они рассчитаны на широкую совместимость, а не на максимальную производительность:
# Cargo.toml — встроенные значения Cargo по умолчанию (то, что получается, если ничего не указывать)
[profile.release]
opt-level = 3 # Уровень оптимизации (0=нет, 1=базовый, 2=хороший, 3=агрессивный)
lto = false # Оптимизация на этапе линковки ВЫКЛЮЧЕНА
codegen-units = 16 # Параллельные единицы генерации кода (быстрее компиляция, меньше оптимизации)
panic = "unwind" # Раскрутка стека при панике (бинарник больше, catch_unwind работает)
strip = "none" # Сохранять все символы и отладочную информацию
overflow-checks = false # Проверки переполнения целых чисел в release отключены
debug = false # Отладочной информации в release нет
Оптимизированный профиль для продакшна (то, что уже использует проект):
[profile.release]
lto = true # Полная оптимизация между крейтами
codegen-units = 1 # Одна единица генерации кода — максимум возможностей для оптимизации
panic = "abort" # Без накладных расходов на раскрутку — меньше и быстрее
strip = true # Удалить все символы — меньше бинарник
Влияние каждой настройки:
| Настройка | По умолчанию → оптимизировано | Размер бинарника | Скорость выполнения | Время компиляции |
|---|---|---|---|---|
lto = false → true | — | -10 до -20% | +5 до +20% | В 2–5× медленнее |
codegen-units = 16 → 1 | — | -5 до -10% | +5 до +10% | В 1,5–2× медленнее |
panic = "unwind" → "abort" | — | -5 до -10% | Незначительно | Незначительно |
strip = "none" → true | — | -50 до -70% | Нет | Нет |
opt-level = 3 → "s" | — | -10 до -30% | -5 до -10% | Примерно так же |
opt-level = 3 → "z" | — | -15 до -40% | -10 до -20% | Примерно так же |
Дополнительные тонкие настройки профиля:
[profile.release]
# Всё перечисленное выше, плюс:
overflow-checks = true # Оставить проверки переполнения даже в release (безопасность важнее скорости)
debug = "line-tables-only" # Минимальная отладочная информация для бэктрейсов без полного DWARF
rpath = false # Не встраивать пути к runtime-библиотекам
incremental = false # Отключить инкрементальную компиляцию (чище сборки)
# Для сборок, оптимизированных по размеру (встраиваемые системы, WASM):
# opt-level = "z" # Агрессивная оптимизация по размеру
# strip = "symbols" # Удалить символы, но оставить секции отладки
Переопределения профиля для отдельных крейтов — оптимизируйте горячие крейты, остальные не трогайте:
# Dev-сборки: оптимизируем зависимости, но не свой код (быстрая пересборка)
[profile.dev.package."*"]
opt-level = 2 # Оптимизировать все зависимости в dev-режиме
# Release-сборки: переопределяем оптимизацию для конкретного крейта
[profile.release.package.serde_json]
opt-level = 3 # Максимальная оптимизация для разбора JSON
codegen-units = 1
# Профиль тестов: совпадает с release для точных интеграционных тестов
[profile.test]
opt-level = 1 # Небольшая оптимизация, чтобы медленные тесты не упирались в таймаут
LTO подробно — thin, fat и кросс-языковой
Link-Time Optimization позволяет LLVM оптимизировать код через границы крейтов — встраивать
функции из serde_json в ваш код разбора, удалять мёртвый код из regex и т. д. Без LTO
каждый крейт — это отдельный «островок» оптимизации.
[profile.release]
# Вариант 1: Fat LTO (по умолчанию при lto = true)
lto = true
# Весь код объединяется в один модуль LLVM → максимальная оптимизация
# Самая долгая компиляция, самый маленький и быстрый бинарник
# Вариант 2: Thin LTO
lto = "thin"
# Каждый крейт остаётся отдельным, но LLVM делает межмодульную оптимизацию
# Компиляция быстрее, чем у fat LTO, оптимизация почти такая же
# Лучший компромисс для большинства проектов
# Вариант 3: без LTO
lto = false
# Только оптимизация внутри крейта
# Самая быстрая компиляция, бинарник больше
# Вариант 4: выключено (явно)
lto = "off"
# То же, что false
Fat LTO и Thin LTO:
| Аспект | Fat LTO (true) | Thin LTO ("thin") |
|---|---|---|
| Качество оптимизации | Лучшее | ~95% от fat |
| Время компиляции | Долго (весь код в одном модуле) | Умеренно (параллельные модули) |
| Потребление памяти | Высокое (весь LLVM IR в памяти) | Ниже (потоковая обработка) |
| Параллелизм | Нет (один модуль) | Хороший (по модулям) |
| Рекомендуется для | Финальных релизных сборок | Сборок CI и разработки |
Кросс-языковой LTO — оптимизация через границы Rust и C:
[profile.release]
lto = true
# Cargo.toml — для крейтов, использующих крейт cc
[build-dependencies]
cc = "1.0"
// build.rs — включаем кросс-языковой LTO (через линкер-плагин)
fn main() {
// Крейт cc учитывает CFLAGS из окружения.
// Для кросс-языкового LTO компилируйте C-код с флагами:
// -flto=thin -O2
cc::Build::new()
.file("csrc/fast_parser.c")
.flag("-flto=thin")
.opt_level(2)
.compile("fast_parser");
}
# Включаем LTO через линкер-плагин (требуется совместимый линкер LLD или gold)
RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
cargo build --release
Кросс-языковой LTO позволяет LLVM встраивать C-функции в вызывающий их Rust-код и наоборот. Наибольший эффект — в коде с большим количеством FFI, где часто вызываются маленькие C-функции (например, обёртки над ioctl для IPMI).
Анализ размера бинарника с cargo-bloat
cargo-bloat отвечает на вопрос:
«Какие функции и крейты занимают больше всего места в моём бинарнике?»
# Установка
cargo install cargo-bloat
# Показать самые большие функции
cargo bloat --release -n 20
# Вывод:
# File .text Size Crate Name
# 2.8% 5.1% 78.5KiB serde_json serde_json::de::Deserializer::parse_...
# 2.1% 3.8% 58.2KiB regex_syntax regex_syntax::ast::parse::ParserI::p...
# 1.5% 2.7% 42.1KiB accel_diag accel_diag::vendor::parse_smi_output
# ...
# Показать по крейтам (какие зависимости самые большие)
cargo bloat --release --crates
# Вывод:
# File .text Size Crate
# 12.3% 22.1% 340KiB serde_json
# 8.7% 15.6% 240KiB regex
# 6.2% 11.1% 170KiB std
# 5.1% 9.2% 141KiB accel_diag
# ...
# Сравнить две сборки (до и после оптимизации)
cargo bloat --release --crates > before.txt
# ... вносим изменения ...
cargo bloat --release --crates > after.txt
diff before.txt after.txt
Типичные источники раздувания и способы борьбы с ними:
| Источник раздувания | Типичный размер | Решение |
|---|---|---|
regex (полный движок) | 200–400 КБ | Используйте regex-lite, если Unicode не нужен |
serde_json (полный) | 200–350 КБ | Рассмотрите simd-json или sonic-rs, если важна производительность |
| Мономорфизация обобщений | Зависит от случая | Используйте dyn Trait на границах API |
Механизмы форматирования (Display, Debug) | 50–150 КБ | #[derive(Debug)] на больших перечислениях накапливается |
| Строки сообщений о панике | 20–80 КБ | panic = "abort" убирает раскрутку, strip убирает строки |
| Неиспользуемые фичи | Зависит от случая | Отключите фичи по умолчанию: serde = { version = "1", default-features = false } |
Сокращение зависимостей с помощью cargo-udeps
cargo-udeps находит зависимости, объявленные в
Cargo.toml, которые ваш код на самом деле не использует:
# Установка (требуется nightly)
cargo install cargo-udeps
# Поиск неиспользуемых зависимостей
cargo +nightly udeps --workspace
# Вывод:
# unused dependencies:
# `diag_tool v0.1.0`
# └── "tempfile" (dev-dependency)
#
# `accel_diag v0.1.0`
# └── "once_cell" ← нужна была до LazyLock, теперь мёртвая
Каждая неиспользуемая зависимость:
- Увеличивает время компиляции
- Увеличивает размер бинарника
- Добавляет риски цепочки поставок
- Добавляет возможные проблемы с лицензиями
Альтернатива: cargo-machete — быстрый эвристический подход:
cargo install cargo-machete
cargo machete
# Быстрее, но возможны ложные срабатывания (эвристика, а не анализ на основе компиляции)
Альтернатива: cargo-shear — золотая середина между cargo-udeps и cargo-machete:
cargo install cargo-shear
cargo shear --fix
# Медленнее, чем cargo-machete, но намного быстрее cargo-udeps
# Значительно меньше ложных срабатываний, чем у cargo-machete
Дерево решений по оптимизации размера
flowchart TD
START["Бинарник слишком большой?"] --> STRIP{"strip = true?"}
STRIP -->|"Нет"| DO_STRIP["Добавьте strip = true<br/>-50…-70% размера"]
STRIP -->|"Да"| LTO{"LTO включён?"}
LTO -->|"Нет"| DO_LTO["Добавьте lto = true<br/>codegen-units = 1"]
LTO -->|"Да"| BLOAT["Запустите cargo-bloat<br/>--crates"]
BLOAT --> BIG_DEP{"Крупная зависимость?"}
BIG_DEP -->|"Да"| REPLACE["Замените на более лёгкую<br/>альтернативу или отключите<br/>фичи по умолчанию"]
BIG_DEP -->|"Нет"| UDEPS["cargo-udeps<br/>Удалите неиспользуемые зависимости"]
UDEPS --> OPT_LEVEL{"Нужно меньше?"}
OPT_LEVEL -->|"Да"| SIZE_OPT["opt-level = 's' или 'z'"]
style DO_STRIP fill:#91e5a3,color:#000
style DO_LTO fill:#e3f2fd,color:#000
style REPLACE fill:#ffd43b,color:#000
style SIZE_OPT fill:#ff6b6b,color:#000
🏋️ Упражнения
🟢 Упражнение 1: измерьте эффект LTO
Соберите проект с настройками release по умолчанию, затем с lto = true +
codegen-units = 1 + strip = true. Сравните размер бинарника и время компиляции.
Решение
# Release по умолчанию
cargo build --release
ls -lh target/release/my-binary
time cargo build --release # Запомните время
# Оптимизированный release — добавьте в Cargo.toml:
# [profile.release]
# lto = true
# codegen-units = 1
# strip = true
# panic = "abort"
cargo clean
cargo build --release
ls -lh target/release/my-binary # Обычно на 30–50% меньше
time cargo build --release # Обычно компиляция в 2–3× медленнее
🟡 Упражнение 2: найдите самый большой крейт
Запустите cargo bloat --release --crates на проекте. Определите самую большую зависимость.
Можно ли её уменьшить, отключив фичи по умолчанию или заменив на более лёгкую альтернативу?
Решение
cargo install cargo-bloat
cargo bloat --release --crates
# Вывод:
# File .text Size Crate
# 12.3% 22.1% 340KiB serde_json
# 8.7% 15.6% 240KiB regex
# Для regex — попробуйте regex-lite, если Unicode не нужен:
# regex-lite = "0.1" # ~10× меньше полного regex
# Для serde — отключите фичи по умолчанию, если std не нужен:
# serde = { version = "1", default-features = false, features = ["derive"] }
cargo bloat --release --crates # Сравните результаты после изменений
Ключевые выводы
lto = true+codegen-units = 1+strip = true+panic = "abort"— это профиль релиза для продакшна- Thin LTO (
lto = "thin") даёт около 80% выигрыша fat LTO при малой части затрат на компиляцию cargo-bloat --cratesточно показывает, какие зависимости занимают место в бинарникеcargo-udeps,cargo-macheteиcargo-shearнаходят мёртвые зависимости, которые тратят время компиляции и размер бинарника- Переопределения профиля для отдельных крейтов позволяют оптимизировать горячие крейты, не замедляя всю сборку
Инструменты времени компиляции и разработки 🟡
Чему вы научитесь:
- Кеширование компиляции с помощью
sccacheдля локальных сборок и CI- Более быстрая линковка с
mold(в 3–10× быстрее линкера по умолчанию)cargo-nextest: более быстрый и информативный запускатель тестов- Инструменты наблюдения за разработкой:
cargo-expand,cargo-geiger,cargo-watch- Линты воркспейса, политика MSRV и документация как часть CI
Перекрёстные ссылки: Профили релиза — LTO и оптимизация размера бинарника · CI/CD-конвейер — эти инструменты встраиваются в ваш конвейер · Зависимости — меньше зависимостей = быстрее компиляция
Оптимизация времени компиляции: sccache, mold, cargo-nextest
Долгая компиляция — главная боль разработчиков на Rust. Эти инструменты вместе могут сократить время итерации на 50–80%:
sccache — общий кеш компиляции:
# Установка
cargo install sccache
# Настраиваем как обёртку rustc
export RUSTC_WRAPPER=sccache
# Или задаём постоянно в .cargo/config.toml:
# [build]
# rustc-wrapper = "sccache"
# Первая сборка: обычная скорость (заполняет кеш)
cargo build --release # 3 минуты
# Очистка и пересборка: попадания в кеш для неизменившихся крейтов
cargo clean && cargo build --release # 45 секунд
# Проверка статистики кеша
sccache --show-stats
# Compile requests 1,234
# Cache hits 987 (80%)
# Cache misses 247
sccache поддерживает общие кеши (S3, GCS, Azure Blob) для обмена кешем в команде и в CI.
mold — более быстрый линкер:
Линковка часто оказывается самой медленной фазой. mold в 3–5× быстрее lld и в 10–20×
быстрее стандартного GNU ld:
# Установка
sudo apt install mold # Ubuntu 22.04+
# Примечание: mold предназначен для целей ELF (Linux). macOS использует Mach-O, а не ELF.
# Линкер macOS (ld64) и так довольно быстрый; если нужен более быстрый:
# brew install sold # sold = mold для Mach-O (экспериментально, менее зрелый)
# На практике время линковки на macOS редко становится узким местом.
# Используем mold для линковки
# .cargo/config.toml
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# См. https://github.com/rui314/mold/blob/main/docs/mold.md#environment-variables
export MOLD_JOBS=1
# Проверяем, что используется mold
cargo build -v 2>&1 | grep mold
cargo-nextest — более быстрый запускатель тестов:
# Установка
cargo install cargo-nextest
# Запуск тестов (по умолчанию параллельно, с таймаутом на тест и повторами)
cargo nextest run
# Ключевые преимущества перед cargo test:
# - Каждый тест выполняется в своём процессе → лучше изоляция
# - Параллельное выполнение с умным планированием
# - Таймауты на тест (больше никаких зависших CI)
# - Вывод в JUnit XML для CI
# - Повтор упавших тестов
# Конфигурация
cargo nextest run --retries 2 --fail-fast
# Архивируем тестовые бинарники (полезно для CI: собрать один раз, тестировать на нескольких машинах)
cargo nextest archive --archive-file tests.tar.zst
cargo nextest run --archive-file tests.tar.zst
# .config/nextest.toml
[profile.default]
retries = 0
slow-timeout = { period = "60s", terminate-after = 3 }
fail-fast = true
[profile.ci]
retries = 2
fail-fast = false
junit = { path = "test-results.xml" }
Общая конфигурация для разработки:
# .cargo/config.toml — оптимизируем внутренний цикл разработки
[build]
rustc-wrapper = "sccache" # Кешировать артефакты компиляции
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"] # Более быстрая линковка
# Dev-профиль: оптимизируем зависимости, но не свой код
# (поместить в Cargo.toml)
# [profile.dev.package."*"]
# opt-level = 2
cargo-expand и cargo-geiger — инструменты наблюдения
cargo-expand — посмотреть, что генерируют макросы:
cargo install cargo-expand
# Раскрыть все макросы в конкретном модуле
cargo expand --lib accel_diag::vendor
# Раскрыть конкретный derive
# Дано: #[derive(Debug, Serialize, Deserialize)]
# cargo expand покажет сгенерированные блоки impl
cargo expand --lib --tests
Незаменим для отладки вывода макросов #[derive], раскрытий macro_rules! и для понимания
того, что генерирует serde для ваших типов.
Помимо cargo-expand, раскрыть макросы можно и с помощью rust-analyzer:
- Установите курсор на макрос, который хотите проверить.
- Откройте палитру команд (например,
F1в VSCode). - Найдите
rust-analyzer: Expand macro recursively at caret.
cargo-geiger — подсчёт использования unsafe во всём дереве зависимостей:
cargo install cargo-geiger
cargo geiger
# Output:
# Metric output format: x/y
# x = небезопасный код, используемый при сборке
# y = весь небезопасный код, найденный в крейте
#
# Functions Expressions Impls Traits Methods
# 0/0 0/0 0/0 0/0 0/0 ✅ my_crate
# 0/5 0/23 0/2 0/0 0/3 ✅ serde
# 3/3 14/14 0/0 0/0 2/2 ❗ libc
# 15/15 142/142 4/4 0/0 12/12 ☢️ ring
# Обозначения:
# ✅ = unsafe не используется
# ❗ = используется некоторое количество unsafe
# ☢️ = сильно использует unsafe
Для политики проекта «без unsafe» cargo geiger проверяет, что ни одна зависимость не
вносит в граф вызовов, который реально выполняет ваш код, unsafe-код.
Линты воркспейса — [workspace.lints]
Начиная с Rust 1.74, Clippy и линты компилятора можно настраивать централизованно в
Cargo.toml — больше не нужны #![deny(...)] в начале каждого крейта:
# Корневой Cargo.toml — конфигурация линтов для всех крейтов
[workspace.lints.clippy]
unwrap_used = "warn" # Предпочитайте ? или expect("причина")
dbg_macro = "deny" # Не допускаем dbg!() в закоммиченном коде
todo = "warn" # Отслеживаем незавершённые реализации
large_enum_variant = "warn" # Ловим случайное раздувание размера
[workspace.lints.rust]
unsafe_code = "deny" # Обеспечиваем политику «без unsafe»
missing_docs = "warn" # Поощряем документирование
# Cargo.toml каждого крейта — подключаем линты воркспейса
[lints]
workspace = true
Это заменяет разрозненные атрибуты #![deny(clippy::unwrap_used)] и обеспечивает единую
политику во всём воркспейсе.
Автоисправление предупреждений Clippy:
# Пусть Clippy автоматически применит предложения, которые можно применить механически
cargo clippy --fix --workspace --all-targets --allow-dirty
# Исправляем и применяем предложения, которые могут изменить поведение (проверяйте внимательно!)
cargo clippy --fix --workspace --all-targets --allow-dirty -- -W clippy::pedantic
Совет: запускайте
cargo clippy --fixперед коммитом. Он обрабатывает тривиальные проблемы (неиспользуемые импорты, избыточные клоны, упрощения типов), которые вручную исправлять скучно.
Политика MSRV и rust-version
Minimum Supported Rust Version (MSRV) гарантирует, что ваш крейт компилируется на старых тулчейнах. Это важно при развёртывании на системах с замороженной версией Rust.
# Cargo.toml
[package]
name = "diag_tool"
version = "0.1.0"
rust-version = "1.75" # Минимальная требуемая версия Rust
# Проверка соответствия MSRV
cargo +1.75.0 check --workspace
# Автоматический поиск MSRV
cargo install cargo-msrv
cargo msrv find
# Output: Minimum Supported Rust Version is 1.75.0
# Проверка в CI
cargo msrv verify
MSRV в CI:
jobs:
msrv:
name: Проверка MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.75.0" # Совпадает с rust-version в Cargo.toml
- run: cargo check --workspace
Стратегия MSRV:
- Бинарные приложения (как большой проект): используйте последний stable. MSRV не нужен.
- Библиотечные крейты (публикуемые на crates.io): задайте MSRV как самую старую версию
Rust, которая поддерживает все используемые фичи. Обычно это
N-2(на две версии позади текущей). - Корпоративные развёртывания: задайте MSRV по самой старой версии Rust, установленной на вашем парке.
Применение: профиль продакшн-бинарника
В проекте уже есть отличный профиль релиза:
# Текущий Cargo.toml воркспейса
[profile.release]
lto = true # ✅ Полная оптимизация между крейтами
codegen-units = 1 # ✅ Максимальная оптимизация
panic = "abort" # ✅ Нет накладных расходов на раскрутку
strip = true # ✅ Удаляем символы для развёртывания
[profile.dev]
opt-level = 0 # ✅ Быстрая компиляция
debug = true # ✅ Полная отладочная информация
Рекомендуемые дополнения:
# Оптимизируем зависимости в dev-режиме (быстрее выполнение тестов)
[profile.dev.package."*"]
opt-level = 2
# Профиль тестов: немного оптимизации, чтобы медленные тесты не упирались в таймаут
[profile.test]
opt-level = 1
# Оставляем проверки переполнения в release (безопасность)
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
overflow-checks = true # ← добавьте: ловим переполнения целых чисел
debug = "line-tables-only" # ← добавьте: бэктрейсы без полного DWARF
Рекомендуемые инструменты для разработчика:
# .cargo/config.toml (предлагаемый)
[build]
rustc-wrapper = "sccache" # 80%+ попаданий в кеш после первой сборки
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"] # В 3–5× быстрее линковка
Ожидаемый эффект на проект:
| Метрика | Сейчас | С дополнениями |
|---|---|---|
| Release-бинарник | ~10 МБ (stripped, LTO) | То же |
| Время dev-сборки | ~45 с | ~25 с (sccache + mold) |
| Пересборка (изменение одного файла) | ~15 с | ~5 с (sccache + mold) |
| Запуск тестов | cargo test | cargo nextest — в 2× быстрее |
| Сканирование уязвимостей зависимостей | Нет | cargo audit в CI |
| Соответствие лицензиям | Вручную | cargo deny автоматически |
| Поиск неиспользуемых зависимостей | Вручную | cargo udeps в CI |
cargo-watch — автоматическая пересборка при изменении файлов
cargo-watch перезапускает команду при каждом
изменении исходного файла — незаменим для быстрого цикла обратной связи:
# Установка
cargo install cargo-watch
# Повторная проверка при каждом сохранении (мгновенная обратная связь)
cargo watch -x check
# Запуск clippy и тестов при изменении
cargo watch -x 'clippy --workspace --all-targets' -x 'test --workspace --lib'
# Следим только за конкретными крейтами (быстрее для больших воркспейсов)
cargo watch -w accel_diag/src -x 'test -p accel_diag'
# Очищать экран между запусками
cargo watch -c -x check
Совет: сочетайте с
moldиsccacheиз примеров выше, чтобы инкрементальная проверка занимала меньше секунды.
cargo doc и документация воркспейса
Для большого воркспейса сгенерированная документация необходима, чтобы API было легко
найти. cargo doc использует rustdoc, чтобы создать HTML-документацию из doc-комментариев
и сигнатур типов:
# Генерация документации для всех крейтов воркспейса (откроется в браузере)
cargo doc --workspace --no-deps --open
# Включить приватные элементы (полезно при разработке)
cargo doc --workspace --no-deps --document-private-items
# Проверить ссылки в документации без генерации HTML (быстрая проверка в CI)
cargo doc --workspace --no-deps 2>&1 | grep -E 'warning|error'
Внутридокументные ссылки — ссылки между типами разных крейтов без URL:
#![allow(unused)]
fn main() {
/// Запускает диагностику GPU с настройками из [`GpuConfig`].
///
/// Реализацию см. в [`crate::accel_diag::run_diagnostics`].
/// Возвращает [`DiagResult`], который можно сериализовать
/// в формат [`DerReport`](crate::core_lib::DerReport).
pub fn run_accel_diag(config: &GpuConfig) -> DiagResult {
// ...
}
}
Показываем платформенно-специфичные API в документации:
#![allow(unused)]
fn main() {
// Cargo.toml: [package.metadata.docs.rs]
// all-features = true
// rustdoc-args = ["--cfg", "docsrs"]
/// Только для Windows: читает состояние батареи через Win32 API.
///
/// Доступно только в сборках `cfg(windows)`.
#[cfg(windows)]
#[doc(cfg(windows))] // Показывает бейдж «Доступно только в Windows» в документации
pub fn get_battery_status() -> Option<u8> {
// ...
}
}
Проверка документации в CI:
# Добавьте в workflow CI
- name: Проверка документации
run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
# Считает битые внутридокументные ссылки ошибками
Для проекта: при большом количестве крейтов
cargo doc --workspace— лучший способ для новых участников команды увидеть поверхность API. ДобавьтеRUSTDOCFLAGS="-D warnings"в CI, чтобы ловить битые ссылки до слияния.
Дерево решений по времени компиляции
flowchart TD
START["Компиляция слишком медленная?"] --> WHERE{"Где тратится время?"}
WHERE -->|"Перекомпиляция<br/>неизменённых крейтов"| SCCACHE["sccache<br/>Общий кеш компиляции"]
WHERE -->|"Фаза линковки"| MOLD["Линкер mold<br/>линковка в 3–10× быстрее"]
WHERE -->|"Запуск тестов"| NEXTEST["cargo-nextest<br/>Параллельный запускатель тестов"]
WHERE -->|"Всё сразу"| COMBO["Всё перечисленное +<br/>cargo-udeps для сокращения зависимостей"]
SCCACHE --> CI_CACHE{"CI или локально?"}
CI_CACHE -->|"CI"| S3["Общий кеш S3/GCS"]
CI_CACHE -->|"Локально"| LOCAL["Локальный дисковый кеш<br/>настраивается автоматически"]
style SCCACHE fill:#91e5a3,color:#000
style MOLD fill:#e3f2fd,color:#000
style NEXTEST fill:#ffd43b,color:#000
style COMBO fill:#b39ddb,color:#000
🏋️ Упражнения
🟢 Упражнение 1: настройте sccache и mold
Установите sccache и mold, настройте их в .cargo/config.toml, затем измерьте выигрыш
по времени компиляции при чистой пересборке.
Решение
# Установка
cargo install sccache
sudo apt install mold # Ubuntu 22.04+
# Настраиваем .cargo/config.toml:
cat > .cargo/config.toml << 'EOF'
[build]
rustc-wrapper = "sccache"
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
EOF
# Первая сборка (заполняет кеш)
time cargo build --release # например, 180 с
# Очистка и пересборка (попадания в кеш)
cargo clean
time cargo build --release # например, 45 с
sccache --show-stats
# Попаданий в кеш должно быть 60–80%+
🟡 Упражнение 2: переходим на cargo-nextest
Установите cargo-nextest и запустите набор тестов. Сравните реальное время с cargo test.
Каков прирост скорости?
Решение
cargo install cargo-nextest
# Стандартный запускатель тестов
time cargo test --workspace 2>&1 | tail -5
# nextest (параллельное выполнение по тестовым бинарникам)
time cargo nextest run --workspace 2>&1 | tail -5
# Типичный прирост: в 2–5× для больших воркспейсов
# nextest также предоставляет:
# - время выполнения каждого теста
# - повторы для нестабильных тестов
# - вывод JUnit XML для CI
cargo nextest run --workspace --retries 2
Ключевые выводы
sccacheс бэкендом S3/GCS разделяет кеш компиляции между командой и CImold— самый быстрый линкер для ELF: время линковки падает с секунд до миллисекундcargo-nextestзапускает тесты параллельно по бинарникам, с лучшим выводом и поддержкой повторовcargo-geigerсчитает использованиеunsafe— запускайте его перед тем, как принимать новые зависимости[workspace.lints]централизует настройку линтов Clippy и rustc во всём многокрейтовом воркспейсе
no_std и проверка фич 🔴
Чему вы научитесь:
- Систематическая проверка комбинаций фич с помощью
cargo-hack- Три уровня Rust:
core,allocиstd— когда что использовать- Сборка крейтов
no_stdс собственными обработчиками паники и аллокаторами- Тестирование кода
no_stdна хосте и с помощью QEMUПерекрёстные ссылки: Windows и условная компиляция — платформенная половина этой темы · Кросс-компиляция — кросс-компиляция под ARM и встраиваемые цели · Miri и санитайзеры — проверка
unsafe-кода в окруженияхno_std· Build-скрипты —cfg-флаги, которые выводитbuild.rs
Rust работает везде — от 8-битных микроконтроллеров до облачных серверов. Эта глава описывает
фундамент: как отказаться от стандартной библиотеки через #![no_std] и проверить, что
ваши комбинации фич действительно компилируются.
Проверка комбинаций фич с помощью cargo-hack
cargo-hack систематически проверяет все комбинации
фич — это необходимо для крейтов с кодом под #[cfg(...)]:
# Установка
cargo install cargo-hack
# Проверяем, что каждая фича компилируется по отдельности
cargo hack check --each-feature --workspace
# Ядерный вариант: проверяем ВСЕ комбинации фич (экспоненциально!)
# Годится только для крейтов с менее чем 8 фичами.
cargo hack check --feature-powerset --workspace
# Практичный компромисс: каждая фича отдельно + все фичи + ни одной фичи
cargo hack check --each-feature --workspace --no-dev-deps
cargo check --workspace --all-features
cargo check --workspace --no-default-features
Почему это важно для проекта:
Если вы добавляете платформенные фичи (linux, windows, direct-ipmi, direct-accel-api),
cargo-hack поймает комбинации, которые ломаются:
# Пример: фичи, которые закрывают платформенный код
[features]
default = ["linux"]
linux = [] # Доступ к железу, специфичный для Linux
windows = ["dep:windows-sys"] # API, специфичные для Windows
direct-ipmi = [] # небезопасный ioctl IPMI (гл. 5)
direct-accel-api = [] # небезопасный FFI accel-mgmt (гл. 5)
# Проверяем, что все фичи компилируются по отдельности И вместе
cargo hack check --each-feature -p diag_tool
# Ловит: «feature 'windows' не компилируется без 'direct-ipmi'»
# Ловит: опечатку в #[cfg(feature = \"linux\")] — там написано 'lnux'
Интеграция с CI:
# Добавьте в конвейер CI (быстро — только проверки компиляции)
- name: Проверка матрицы фич
run: cargo hack check --each-feature --workspace --no-dev-deps
Эмпирическое правило: запускайте
cargo hack check --each-featureв CI для любого крейта с двумя и более фичами.--feature-powerset— только для базовых библиотечных крейтов с менее чем 8 фичами: это экспоненциально ($2^n$ комбинаций).
no_std — когда и зачем
#![no_std] говорит компилятору: «не линкуй стандартную библиотеку». Крейт может использовать
только core (и, при необходимости, alloc). Зачем это нужно?
| Сценарий | Зачем no_std |
|---|---|
| Встраиваемая прошивка (ARM Cortex-M, RISC-V) | Нет ОС, нет кучи, нет файловой системы |
| UEFI-диагностический инструмент | Среда до загрузки ОС, нет API ОС |
| Модули ядра | Пространство ядра не может использовать пользовательский std |
| WebAssembly (WASM) | Минимизация размера бинарника, нет зависимостей от ОС |
| Загрузчики | Работают до появления какой-либо ОС |
| Разделяемая библиотека с C-интерфейсом | Не тянуть рантайм Rust в вызывающий код |
Для диагностики железа no_std становится актуален при создании:
- UEFI-диагностических инструментов, которые работают до загрузки ОС
- Диагностики прошивки BMC (ограниченные по ресурсам ARM SoC)
- Диагностики PCIe на уровне ядра (модуль ядра или eBPF-зонд)
core, alloc и std — три уровня
┌─────────────────────────────────────────────────────────────┐
│ std │
│ Всё из core и alloc, ПЛЮС: │
│ • Файловый ввод-вывод (std::fs, std::io) │
│ • Сеть (std::net) │
│ • Потоки (std::thread) │
│ • Время (std::time) │
│ • Окружение (std::env) │
│ • Процессы (std::process) │
│ • Специфичное для ОС (std::os::unix, std::os::windows) │
├─────────────────────────────────────────────────────────────┤
│ alloc (доступен с #![no_std] + extern crate │
│ alloc, если есть глобальный аллокатор) │
│ • String, Vec, Box, Rc, Arc │
│ • BTreeMap, BTreeSet │
│ • Макрос format!() │
│ • Коллекции и умные указатели, которым нужна куча │
├─────────────────────────────────────────────────────────────┤
│ core (всегда доступен, даже в #![no_std]) │
│ • Примитивные типы (u8, bool, char и др.) │
│ • Option, Result │
│ • Iterator, slice, array, str (срезы, а не String) │
│ • Трейты: Clone, Copy, Debug, Display, From, Into │
│ • Атомарные типы (core::sync::atomic) │
│ • Cell, RefCell (core::cell) — Pin (core::pin) │
│ • core::fmt (форматирование без выделения памяти) │
│ • core::mem, core::ptr (низкоуровневые операции с памятью) │
│ • Математика: core::num, базовая арифметика │
└─────────────────────────────────────────────────────────────┘
Что вы теряете без std:
- Нет
HashMap(нужен хешер — используйтеBTreeMapизallocилиhashbrown) - Нет
println!()(нужен stdout — используйтеcore::fmt::Writeв буфер) - Нет
std::error::Error(стабилизирован вcoreначиная с Rust 1.81, но многие экосистемы ещё не перешли) - Нет файлового ввода-вывода, сети и потоков (если их не предоставляет платформенный HAL)
- Нет
Mutex(используйтеspin::Mutexили платформенные блокировки)
Сборка крейта no_std
#![allow(unused)]
fn main() {
// src/lib.rs — библиотечный крейт no_std
#![no_std]
// При желании используем выделение памяти в куче
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
/// Показание температуры с термодатчика.
/// Работает в любой среде — от голого железа до Linux.
#[derive(Clone, Copy, Debug)]
pub struct Temperature {
/// Сырое значение датчика (0.0625°C на LSB для типичных I2C-датчиков)
raw: u16,
}
impl Temperature {
pub const fn from_raw(raw: u16) -> Self {
Self { raw }
}
/// Преобразование в градусы Цельсия (fixed-point, FPU не нужен)
pub const fn millidegrees_c(&self) -> i32 {
(self.raw as i32) * 625 / 10 // разрешение 0.0625°C
}
pub fn degrees_c(&self) -> f32 {
self.raw as f32 * 0.0625
}
}
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let md = self.millidegrees_c();
// Обрабатываем знак корректно для значений между -0.999°C и -0.001°C,
// где md / 1000 == 0, но значение отрицательное.
if md < 0 && md > -1000 {
write!(f, "-0.{:03}°C", (-md) % 1000)
} else {
write!(f, "{}.{:03}°C", md / 1000, (md % 1000).abs())
}
}
}
/// Разбор значений температуры, разделённых пробелами.
/// Использует alloc — требуется глобальный аллокатор.
pub fn parse_temperatures(input: &str) -> Vec<Temperature> {
input
.split_whitespace()
.filter_map(|s| s.parse::<u16>().ok())
.map(Temperature::from_raw)
.collect()
}
/// Форматирование без выделения памяти — пишем прямо в буфер.
/// Работает в окружениях только с `core` (без alloc и кучи).
pub fn format_temp_into(temp: &Temperature, buf: &mut [u8]) -> usize {
use core::fmt::Write;
struct SliceWriter<'a> {
buf: &'a mut [u8],
pos: usize,
}
impl<'a> Write for SliceWriter<'a> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let bytes = s.as_bytes();
let remaining = self.buf.len() - self.pos;
if bytes.len() > remaining {
// Буфер полон — сообщаем об ошибке вместо тихого обрезания.
// Вызывающий код может проверить возвращённый pos на частичную запись.
return Err(fmt::Error);
}
self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
self.pos += bytes.len();
Ok(())
}
}
let mut w = SliceWriter { buf, pos: 0 };
let _ = write!(w, "{}", temp);
w.pos
}
}
# Cargo.toml для крейта no_std
[package]
name = "thermal-sensor"
version = "0.1.0"
edition = "2021"
[features]
default = ["alloc"]
alloc = [] # Включает Vec, String и т. д.
std = [] # Включает полный std (подразумевает alloc)
[dependencies]
# Используем совместимые с no_std крейты
serde = { version = "1.0", default-features = false, features = ["derive"] }
# ↑ default-features = false убирает зависимость от std!
Ключевой паттерн крейтов: многие популярные крейты (serde, log, rand, embedded-hal) поддерживают
no_stdчерезdefault-features = false. Всегда проверяйте, требует ли зависимостьstd, прежде чем использовать её в контекстеno_std. Учтите, что некоторым крейтам (например,regex) нужен как минимумalloc, и в окружениях только сcoreони не работают.
Собственные обработчики паники и аллокаторы
В бинарниках #![no_std] (а не в библиотеках) нужно предоставить обработчик паники и,
при необходимости, глобальный аллокатор:
// src/main.rs — бинарник no_std (например, UEFI-диагностика)
#![no_std]
#![no_main]
extern crate alloc;
use core::panic::PanicInfo;
// Обязательно: что делать при панике (раскрутки стека нет)
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
// В embedded: мигнуть светодиодом, писать в UART, зависнуть
// В UEFI: вывести сообщение в консоль, остановиться
// Минимально: просто бесконечный цикл
loop {
core::hint::spin_loop();
}
}
// Обязательно при использовании alloc: предоставить глобальный аллокатор
use alloc::alloc::{GlobalAlloc, Layout};
struct BumpAllocator {
// Простой bump-аллокатор для embedded/UEFI
// В реальности используйте крейт вроде `linked_list_allocator` или `embedded-alloc`
}
// ПРЕДУПРЕЖДЕНИЕ: это нерабочая заглушка! Вызов alloc() вернёт null,
// что сразу приведёт к UB (контракт глобального аллокатора требует
// ненулевых результатов для выделений ненулевого размера). В реальном коде
// используйте проверенный крейт-аллокатор:
// - embedded-alloc (embedded-цели)
// - linked_list_allocator (UEFI / ядра ОС)
// - talc (универсальный no_std)
unsafe impl GlobalAlloc for BumpAllocator {
/// # Safety
/// Layout должен иметь ненулевой размер. Возвращает null (заглушка — упадёт).
unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
// ЗАГЛУШКА — упадёт! Замените на реальную логику выделения.
core::ptr::null_mut()
}
/// # Safety
/// `_ptr` должен быть возвращён `alloc` с совместимым layout.
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
// Ничего не делаем для bump-аллокатора
}
}
#[global_allocator]
static ALLOCATOR: BumpAllocator = BumpAllocator {};
// Точка входа (зависит от платформы, а не fn main)
// Для UEFI: #[entry] или efi_main
// Для embedded: #[cortex_m_rt::entry]
Тестирование кода no_std
Тесты выполняются на хост-машине, где есть std. Хитрость: ваша библиотека — no_std,
но тестовый харнес использует std:
#![allow(unused)]
fn main() {
// Ваш крейт: #![no_std] в src/lib.rs
// Но тесты автоматически работают под std:
#[cfg(test)]
mod tests {
use super::*;
// std здесь доступен — println!, assert!, Vec работают
#[test]
fn test_temperature_conversion() {
let temp = Temperature::from_raw(800); // 50.0°C
assert_eq!(temp.millidegrees_c(), 50000);
assert!((temp.degrees_c() - 50.0).abs() < 0.01);
}
#[test]
fn test_format_into_buffer() {
let temp = Temperature::from_raw(800);
let mut buf = [0u8; 32];
let len = format_temp_into(&temp, &mut buf);
let s = core::str::from_utf8(&buf[..len]).unwrap();
assert_eq!(s, "50.000°C");
}
}
}
Тестирование на реальной цели (когда std вообще недоступен):
# Для тестов на устройстве (встраиваемый ARM) используйте defmt-test
# Для целей UEFI используйте uefi-test-runner
# Для кросс-архитектурных тестов без железа используйте QEMU
# Запуск тестов библиотеки no_std на хосте (всегда работает):
cargo test --lib
# Проверка компиляции no_std под целевую платформу no_std:
cargo check --target thumbv7em-none-eabihf # ARM Cortex-M
cargo check --target riscv32imac-unknown-none-elf # RISC-V
Дерево решений по no_std
flowchart TD
START["Нужна ли вашему коду<br/>стандартная библиотека?"] --> NEED_FS{"Файловая система,<br/>сеть, потоки?"}
NEED_FS -->|"Да"| USE_STD["Используем std<br/>Обычное приложение"]
NEED_FS -->|"Нет"| NEED_HEAP{"Нужно выделение в куче?<br/>Vec, String, Box"}
NEED_HEAP -->|"Да"| USE_ALLOC["#![no_std]<br/>extern crate alloc"]
NEED_HEAP -->|"Нет"| USE_CORE["#![no_std]<br/>только core"]
USE_ALLOC --> VERIFY["cargo-hack<br/>--each-feature"]
USE_CORE --> VERIFY
USE_STD --> VERIFY
VERIFY --> TARGET{"На цели есть ОС?"}
TARGET -->|"Да"| HOST_TEST["cargo test --lib<br/>Обычное тестирование"]
TARGET -->|"Нет"| CROSS_TEST["QEMU / defmt-test<br/>Тестирование на устройстве"]
style USE_STD fill:#91e5a3,color:#000
style USE_ALLOC fill:#ffd43b,color:#000
style USE_CORE fill:#ff6b6b,color:#000
🏋️ Упражнения
🟡 Упражнение 1: проверка комбинаций фич
Установите cargo-hack и запустите cargo hack check --each-feature --workspace на проекте
с несколькими фичами. Находит ли он нерабочие комбинации?
Решение
cargo install cargo-hack
# Проверяем каждую фичу по отдельности
cargo hack check --each-feature --workspace --no-dev-deps
# Если какая-то комбинация фич не компилируется:
# error[E0433]: failed to resolve: use of undeclared crate or module `std`
# → Значит, в фича-гейте не хватает защиты #[cfg]
# Проверяем все фичи, отсутствие фич и каждую по отдельности:
cargo hack check --each-feature --workspace
cargo check --workspace --all-features
cargo check --workspace --no-default-features
🔴 Упражнение 2: сборка библиотеки no_std
Создайте библиотечный крейт, который компилируется с #![no_std]. Реализуйте простой кольцевой
буфер на стеке. Проверьте компиляцию под thumbv7em-none-eabihf (ARM Cortex-M).
Решение
#![allow(unused)]
fn main() {
// lib.rs
#![no_std]
pub struct RingBuffer<const N: usize> {
data: [u8; N],
head: usize,
len: usize,
}
impl<const N: usize> RingBuffer<N> {
pub const fn new() -> Self {
Self { data: [0; N], head: 0, len: 0 }
}
pub fn push(&mut self, byte: u8) -> bool {
if self.len == N { return false; }
let idx = (self.head + self.len) % N;
self.data[idx] = byte;
self.len += 1;
true
}
pub fn pop(&mut self) -> Option<u8> {
if self.len == 0 { return None; }
let byte = self.data[self.head];
self.head = (self.head + 1) % N;
self.len -= 1;
Some(byte)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_pop() {
let mut rb = RingBuffer::<4>::new();
assert!(rb.push(1));
assert!(rb.push(2));
assert_eq!(rb.pop(), Some(1));
assert_eq!(rb.pop(), Some(2));
assert_eq!(rb.pop(), None);
}
}
}
rustup target add thumbv7em-none-eabihf
cargo check --target thumbv7em-none-eabihf
# ✅ Компилируется для bare-metal ARM
Ключевые выводы
cargo-hack --each-featureобязателен для любого крейта с условной компиляцией — запускайте его в CIcore→alloc→std— это слои: каждый добавляет возможности, но требует больше поддержки рантайма- Собственные обработчики паники и аллокаторы обязательны для бинарников
no_stdна голом железе - Тестируйте библиотеки
no_stdна хосте черезcargo test --lib— железо не нужно - Запускайте
--feature-powersetтолько для базовых библиотек с менее чем 8 фичами — это $2^n$ комбинаций
Windows и условная компиляция 🟡
Чему вы научитесь:
- Паттерны поддержки Windows: крейты
windows-sys/windows,cargo-xwin- Условная компиляция через
#[cfg]— её проверяет компилятор, а не препроцессор- Архитектура платформенных абстракций: когда хватает блоков
#[cfg], а когда нужны трейты- Кросс-компиляция под Windows из Linux
Перекрёстные ссылки:
no_stdи фичи —cargo-hackи проверка фич · Кросс-компиляция — общая настройка кросс-сборки · Build-скрипты —cfg-флаги, которые выводитbuild.rs
Поддержка Windows — платформенные абстракции
Атрибуты #[cfg()] в Rust и фичи Cargo позволяют одной кодовой базе аккуратно работать
и на Linux, и на Windows. В проекте этот паттерн уже есть в platform::run_command:
#![allow(unused)]
fn main() {
// Реальный паттерн из проекта — запуск оболочки в зависимости от платформы
pub fn exec_cmd(cmd: &str, timeout_secs: Option<u64>) -> Result<CommandResult, CommandError> {
#[cfg(windows)]
let mut child = Command::new("cmd")
.args(["/C", cmd])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
#[cfg(not(windows))]
let mut child = Command::new("sh")
.args(["-c", cmd])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
// ... остальной код не зависит от платформы ...
}
}
Доступные предикаты cfg:
#![allow(unused)]
fn main() {
// Операционная система
#[cfg(target_os = "linux")] // Именно Linux
#[cfg(target_os = "windows")] // Windows
#[cfg(target_os = "macos")] // macOS
#[cfg(unix)] // Linux, macOS, BSD и т. д.
#[cfg(windows)] // Windows (сокращённая запись)
// Архитектура
#[cfg(target_arch = "x86_64")] // x86 64-бит
#[cfg(target_arch = "aarch64")] // ARM 64-бит
#[cfg(target_arch = "x86")] // x86 32-бит
// Ширина указателя (переносимая альтернатива архитектуре)
#[cfg(target_pointer_width = "64")] // Любая 64-битная платформа
#[cfg(target_pointer_width = "32")] // Любая 32-битная платформа
// Окружение / C-библиотека
#[cfg(target_env = "gnu")] // glibc
#[cfg(target_env = "musl")] // musl libc
#[cfg(target_env = "msvc")] // MSVC в Windows
// Порядок байтов
#[cfg(target_endian = "little")]
#[cfg(target_endian = "big")]
// Комбинации через any(), all(), not()
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[cfg(not(windows))]
}
Крейты windows-sys и windows
Для прямых вызовов API Windows:
# Cargo.toml — windows-sys для сырого FFI (легче, без абстракций)
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_System_Services",
"Win32_System_Registry",
"Win32_System_Power",
] }
# ПРИМЕЧАНИЕ: windows-sys выпускает несовместимые по semver релизы (0.48 → 0.52 → 0.59).
# Зафиксируйте одну минорную версию — в каждом релизе могут удаляться или переименовываться привязки API.
# Перед началом нового проекта проверьте актуальную версию на https://github.com/microsoft/windows-rs
# Или используйте крейт windows для безопасных обёрток (тяжелее, зато удобнее)
# windows = { version = "0.59", features = [...] }
#![allow(unused)]
fn main() {
// src/platform/windows.rs
#[cfg(windows)]
mod win {
use windows_sys::Win32::System::Power::{
GetSystemPowerStatus, SYSTEM_POWER_STATUS,
};
pub fn get_battery_status() -> Option<u8> {
let mut status = SYSTEM_POWER_STATUS::default();
// SAFETY: GetSystemPowerStatus записывает в переданный буфер.
// Буфер корректного размера и выравнивания.
let ok = unsafe { GetSystemPowerStatus(&mut status) };
if ok != 0 {
Some(status.BatteryLifePercent)
} else {
None
}
}
}
}
windows-sys и крейт windows:
| Аспект | windows-sys | windows |
|---|---|---|
| Стиль API | Сырой FFI (вызовы через unsafe) | Безопасные обёртки на Rust |
| Размер бинарника | Минимальный (только объявления extern) | Больше (код обёрток) |
| Время компиляции | Быстрое | Медленнее |
| Эргономика | В стиле C, безопасность вручную | Идиоматично для Rust |
| Обработка ошибок | Сырые BOOL / HRESULT | Result<T, windows::core::Error> |
| Когда использовать | Критичная к производительности тонкая обёртка | Прикладной код, удобство |
Кросс-компиляция под Windows из Linux
# Вариант 1: MinGW (GNU ABI)
rustup target add x86_64-pc-windows-gnu
sudo apt install gcc-mingw-w64-x86-64
cargo build --target x86_64-pc-windows-gnu
# Получаем .exe — запускается на Windows, линкуется с msvcrt
# Вариант 2: ABI MSVC через xwin (для полной совместимости с MSVC)
cargo install cargo-xwin
cargo xwin build --target x86_64-pc-windows-msvc
# Использует CRT и заголовки SDK от Microsoft, которые скачиваются автоматически
# Вариант 3: кросс-компиляция на основе Zig
cargo zigbuild --target x86_64-pc-windows-gnu
GNU и MSVC ABI в Windows:
| Аспект | x86_64-pc-windows-gnu | x86_64-pc-windows-msvc |
|---|---|---|
| Линкер | MinGW ld | MSVC link.exe или lld-link |
| C-рантайм | msvcrt.dll (универсальный) | ucrtbase.dll (современный) |
| Совместимость с C++ | ABI GCC | ABI MSVC |
| Кросс-компиляция из Linux | Просто (MinGW) | Возможна (cargo-xwin) |
| Поддержка Windows API | Полная | Полная |
| Формат отладочной информации | DWARF | PDB |
| Рекомендуется для | Простых инструментов, сборок CI | Полной интеграции с Windows |
Паттерны условной компиляции
Паттерн 1: выбор платформенного модуля
#![allow(unused)]
fn main() {
// src/platform/mod.rs — компилируем разные модули для разных ОС
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::*;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
pub use windows::*;
// Оба модуля реализуют один и тот же публичный API:
// pub fn get_cpu_temperature() -> Result<f64, PlatformError>
// pub fn list_pci_devices() -> Result<Vec<PciDevice>, PlatformError>
}
Паттерн 2: платформенная поддержка за фичами
# Cargo.toml
[features]
default = ["linux"]
linux = [] # Доступ к железу, специфичный для Linux
windows = ["dep:windows-sys"] # API, специфичные для Windows
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [...], optional = true }
#![allow(unused)]
fn main() {
// Ошибка компиляции, если кто-то пытается собрать под Windows без фичи:
#[cfg(all(target_os = "windows", not(feature = "windows")))]
compile_error!("Включите фичу 'windows', чтобы собрать проект для Windows");
}
Паттерн 3: платформенная абстракция на трейтах
#![allow(unused)]
fn main() {
/// Не зависящий от платформы интерфейс доступа к железу.
pub trait HardwareAccess {
type Error: std::error::Error;
fn read_cpu_temperature(&self) -> Result<f64, Self::Error>;
fn read_gpu_temperature(&self, gpu_index: u32) -> Result<f64, Self::Error>;
fn list_pci_devices(&self) -> Result<Vec<PciDevice>, Self::Error>;
fn send_ipmi_command(&self, cmd: &IpmiCmd) -> Result<IpmiResponse, Self::Error>;
}
#[cfg(target_os = "linux")]
pub struct LinuxHardware;
#[cfg(target_os = "linux")]
impl HardwareAccess for LinuxHardware {
type Error = LinuxHwError;
fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
// Читаем из /sys/class/thermal/thermal_zone0/temp
let raw = std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")?;
Ok(raw.trim().parse::<f64>()? / 1000.0)
}
// ...
}
#[cfg(target_os = "windows")]
pub struct WindowsHardware;
#[cfg(target_os = "windows")]
impl HardwareAccess for WindowsHardware {
type Error = WindowsHwError;
fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
// Читаем через WMI (Win32_TemperatureProbe) или Open Hardware Monitor
todo!("запрос температуры через WMI")
}
// ...
}
/// Создаём реализацию, подходящую для текущей платформы
pub fn create_hardware() -> impl HardwareAccess {
#[cfg(target_os = "linux")]
{ LinuxHardware }
#[cfg(target_os = "windows")]
{ WindowsHardware }
}
}
Архитектура платформенной абстракции
Для проекта, который работает на нескольких платформах, код стоит разложить по трём слоям:
┌──────────────────────────────────────────────────┐
│ Прикладная логика (не зависит от платформы) │
│ diag_tool, accel_diag, network_diag, event_log │
│ Использует только трейт абстракции платформы │
├──────────────────────────────────────────────────┤
│ Слой абстракции платформы (определения трейтов) │
│ trait HardwareAccess { ... } │
│ trait CommandRunner { ... } │
│ trait FileSystem { ... } │
├──────────────────────────────────────────────────┤
│ Платформенные реализации (через cfg) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Реализация │ │ Реализация │ │
│ │ для Linux │ │ для Windows │ │
│ │ /sys, /proc │ │ WMI, Registry│ │
│ │ ipmitool │ │ ipmiutil │ │
│ │ lspci │ │ devcon │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────┘
Тестирование абстракции: мокайте платформенный трейт в модульных тестах:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
struct MockHardware {
cpu_temp: f64,
gpu_temps: Vec<f64>,
}
impl HardwareAccess for MockHardware {
type Error = std::io::Error;
fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
Ok(self.cpu_temp)
}
fn read_gpu_temperature(&self, index: u32) -> Result<f64, Self::Error> {
self.gpu_temps.get(index as usize)
.copied()
.ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("GPU {index} не найден")
))
}
fn list_pci_devices(&self) -> Result<Vec<PciDevice>, Self::Error> {
Ok(vec![]) // Мок возвращает пустой результат
}
fn send_ipmi_command(&self, _cmd: &IpmiCmd) -> Result<IpmiResponse, Self::Error> {
Ok(IpmiResponse::default())
}
}
#[test]
fn test_thermal_check_with_mock() {
let hw = MockHardware {
cpu_temp: 75.0,
gpu_temps: vec![82.0, 84.0],
};
let result = run_thermal_diagnostic(&hw);
assert!(result.is_ok());
}
}
}
Приложение: сначала Linux, готовность к Windows
Проект уже частично готов к Windows. Используйте
cargo-hack, чтобы проверить все комбинации фич,
и кросс-компиляцию, чтобы тестировать
под Windows из Linux:
Уже сделано:
platform::run_commandиспользует#[cfg(windows)]для выбора оболочки- Тесты используют
#[cfg(windows)]/#[cfg(not(windows))], чтобы подбирать команды, подходящие для платформы
Рекомендуемый путь развития поддержки Windows:
Фаза 1: выделяем трейт платформенной абстракции (текущий этап → 2 недели)
├─ определяем трейт HardwareAccess в core_lib
├─ оборачиваем текущий код для Linux в реализацию LinuxHardware
└─ все диагностические модули зависят от трейта, а не от особенностей Linux
Фаза 2: добавляем заглушки для Windows (2 недели)
├─ реализуем WindowsHardware с заглушками TODO
├─ CI собирает проект для x86_64-pc-windows-msvc (только проверка компиляции)
└─ тесты проходят с MockHardware на всех платформах
Фаза 3: реализация для Windows (постоянная работа)
├─ IPMI через ipmiutil.exe или драйвер OpenIPMI для Windows
├─ GPU через accel-mgmt (accel-api.dll) — тот же API, что и в Linux
├─ PCIe через Windows Setup API (SetupDiEnumDeviceInfo)
└─ NIC через WMI (Win32_NetworkAdapter)
Дополнение к кросс-платформенному CI:
# Добавьте в матрицу CI
- target: x86_64-pc-windows-msvc
os: windows-latest
name: windows-x86_64
Это гарантирует, что кодовая база компилируется под Windows ещё до завершения полной
реализации, а ошибки в cfg выявляются рано.
Ключевая мысль: абстракция не обязана быть идеальной с первого дня. Начинайте с блоков
#[cfg]в листовых функциях (как уже делаетexec_cmd), а переходите к трейтам, когда появятся две и более платформенные реализации. Преждевременная абстракция хуже, чем блоки#[cfg].
Дерево решений по условной компиляции
flowchart TD
START["Платформенно-специфичный код?"] --> HOW_MANY{"Сколько платформ?"}
HOW_MANY -->|"2 (Linux + Windows)"| CFG_BLOCKS["Блоки #[cfg]<br/>в листовых функциях"]
HOW_MANY -->|"3+"| TRAIT_APPROACH["Трейт платформы<br/>+ реализация для каждой"]
CFG_BLOCKS --> WINAPI{"Нужны API Windows?"}
WINAPI -->|"Минимально"| WIN_SYS["windows-sys<br/>Привязки сырого FFI"]
WINAPI -->|"Развёрнуто (COM и др.)"| WIN_RS["Крейт windows<br/>Безопасные идиоматичные обёртки"]
WINAPI -->|"Не нужны<br/>(только #[cfg])"| NATIVE["cfg(windows)<br/>cfg(unix)"]
TRAIT_APPROACH --> CI_CHECK["cargo-hack<br/>--each-feature"]
CFG_BLOCKS --> CI_CHECK
CI_CHECK --> XCOMPILE["Кросс-компиляция в CI<br/>cargo-xwin или<br/>нативные раннеры"]
style CFG_BLOCKS fill:#91e5a3,color:#000
style TRAIT_APPROACH fill:#ffd43b,color:#000
style WIN_SYS fill:#e3f2fd,color:#000
style WIN_RS fill:#e3f2fd,color:#000
🏋️ Упражнения
🟢 Упражнение 1: платформенно-условный модуль
Создайте модуль с реализациями get_hostname() под #[cfg(unix)] и #[cfg(windows)].
Проверьте, что обе компилируются через cargo check и cargo check --target x86_64-pc-windows-msvc.
Решение
#![allow(unused)]
fn main() {
// src/hostname.rs
#[cfg(unix)]
pub fn get_hostname() -> String {
use std::fs;
fs::read_to_string("/etc/hostname")
.unwrap_or_else(|_| "unknown".to_string())
.trim()
.to_string()
}
#[cfg(windows)]
pub fn get_hostname() -> String {
use std::env;
env::var("COMPUTERNAME").unwrap_or_else(|_| "unknown".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hostname_is_not_empty() {
let name = get_hostname();
assert!(!name.is_empty());
}
}
}
# Проверка компиляции под Linux
cargo check
# Проверка компиляции под Windows (кросс-проверка)
rustup target add x86_64-pc-windows-msvc
cargo check --target x86_64-pc-windows-msvc
🟡 Упражнение 2: кросс-компиляция под Windows с cargo-xwin
Установите cargo-xwin и соберите простой бинарник для x86_64-pc-windows-msvc из Linux.
Убедитесь, что результат — .exe.
Решение
cargo install cargo-xwin
rustup target add x86_64-pc-windows-msvc
cargo xwin build --release --target x86_64-pc-windows-msvc
# Заголовки и библиотеки Windows SDK скачиваются автоматически
file target/x86_64-pc-windows-msvc/release/my-binary.exe
# Вывод: PE32+ executable (console) x86-64, for MS Windows
# Можно также проверить через Wine:
wine target/x86_64-pc-windows-msvc/release/my-binary.exe
Ключевые выводы
- Начинайте с блоков
#[cfg]в листовых функциях; переходите к трейтам, когда три и более платформ начинают расходиться windows-sys— для сырого FFI; крейтwindowsпредоставляет безопасные идиоматичные обёрткиcargo-xwinкросс-компилирует под ABI MSVC Windows из Linux — машина с Windows не нужна- Всегда проверяйте
--target x86_64-pc-windows-msvcв CI, даже если выпускаете только под Linux - Комбинируйте
#[cfg]с фичами Cargo для необязательной поддержки платформ (например,feature = "windows")
Собираем всё вместе — продакшн-конвейер CI/CD 🟡
Чему вы научитесь:
- Структура многоэтапного workflow CI в GitHub Actions (check → test → coverage → security → cross → release)
- Стратегии кеширования с
rust-cacheи тонкая настройкаsave-if- Запуск Miri и санитайзеров по ночному расписанию
- Автоматизация задач с помощью
Makefile.tomlи pre-commit-хуков- Автоматические релизы с
cargo-distПерекрёстные ссылки: Build-скрипты · Кросс-компиляция · Бенчмаркинг · Покрытие · Miri и санитайзеры · Зависимости · Профили релиза · Инструменты времени компиляции ·
no_std· Windows
Отдельные инструменты полезны. Конвейер, который автоматически запускает их на каждый push, меняет всё. Эта глава собирает инструменты из глав 1–10 в единый CI/CD-workflow.
Полный workflow GitHub Actions
Один файл workflow, который запускает все этапы проверки параллельно:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_ENCODED_RUSTFLAGS: "-Dwarnings" # Считать предупреждения ошибками (только для верхнеуровневого крейта)
# ПРИМЕЧАНИЕ: в отличие от RUSTFLAGS, CARGO_ENCODED_RUSTFLAGS не влияет на build-скрипты
# и процедурные макросы, что избавляет от ложных падений из-за предупреждений сторонних крейтов.
# Если хотите применять правило и к build-скриптам, используйте RUSTFLAGS="-Dwarnings".
jobs:
# ─── Этап 1: быстрая обратная связь (< 2 мин) ───
check:
name: Проверка + Clippy + форматирование
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2 # Кешируем зависимости
- name: Проверка Cargo.lock
run: cargo fetch --locked
- name: Проверка документации
run: RUSTDOCFLAGS='-Dwarnings' cargo doc --workspace --all-features --no-deps
- name: Проверка компиляции
run: cargo check --workspace --all-targets --all-features
- name: Линты Clippy
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: Форматирование
run: cargo fmt --all -- --check
# ─── Этап 2: тесты (< 5 мин) ───
test:
name: Тесты (${{ matrix.os }})
needs: check
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Запуск тестов
run: cargo test --workspace
- name: Запуск doc-тестов
run: cargo test --workspace --doc
# ─── Этап 3: кросс-компиляция (< 10 мин) ───
cross:
name: Кросс-сборка (${{ matrix.target }})
needs: check
strategy:
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
use_cross: true
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Установка musl-tools
if: contains(matrix.target, 'musl')
run: sudo apt-get install -y musl-tools
- name: Установка cross
if: matrix.use_cross
uses: taiki-e/install-action@cross
- name: Сборка (нативная)
if: "!matrix.use_cross"
run: cargo build --release --target ${{ matrix.target }}
- name: Сборка (cross)
if: matrix.use_cross
run: cross build --release --target ${{ matrix.target }}
- name: Загрузка артефакта
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: target/${{ matrix.target }}/release/diag_tool
# ─── Этап 4: покрытие (< 10 мин) ───
coverage:
name: Покрытие кода
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Генерация покрытия
run: cargo llvm-cov --workspace --lcov --output-path lcov.info
- name: Проверка минимального покрытия
run: cargo llvm-cov --workspace --fail-under-lines 75
- name: Загрузка в Codecov
uses: codecov/codecov-action@v4
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
# ─── Этап 5: проверка безопасности (< 15 мин) ───
miri:
name: Miri
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: miri
- name: Запуск Miri
run: cargo miri test --workspace
env:
MIRIFLAGS: "-Zmiri-backtrace=full"
# ─── Этап 6: бенчмарки (только для PR, < 10 мин) ───
bench:
name: Бенчмарки
if: github.event_name == 'pull_request'
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Запуск бенчмарков
run: cargo bench -- --output-format bencher | tee bench.txt
- name: Сравнение с базовой линией
uses: benchmark-action/github-action-benchmark@v1
with:
tool: 'cargo'
output-file-path: bench.txt
github-token: ${{ secrets.GITHUB_TOKEN }}
alert-threshold: '115%'
comment-on-alert: true
Схема выполнения конвейера:
┌─────────┐
│ check │ ← clippy + fmt + cargo check (2 мин)
└────┬────┘
┌─────────┬──┴──┬──────────┬──────────┐
▼ ▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌────────┐ ┌──────┐ ┌──────┐
│ test │ │cross │ │coverage│ │ miri │ │bench │
│ (2×) │ │ (2×) │ │ │ │ │ │(PR) │
└──────┘ └──────┘ └────────┘ └──────┘ └──────┘
3 мин 8 мин 8 мин 12 мин 5 мин
Общее время: ~14 мин (параллельно после шлюза check)
Стратегии кеширования в CI
Swatinem/rust-cache@v2 — стандартное для Rust
действие кеширования в CI. Оно кеширует ~/.cargo и target/ между запусками, но большим
воркспейсам нужна тонкая настройка:
# Базовый вариант (то, что используем выше)
- uses: Swatinem/rust-cache@v2
# Настроенный для большого воркспейса:
- uses: Swatinem/rust-cache@v2
with:
# Отдельные кеши для каждой джобы — не даём артефактам тестов раздувать кеш сборки
prefix-key: "v1-rust"
key: ${{ matrix.os }}-${{ matrix.target || 'default' }}
# Сохраняем кеш только в ветке main (PR читают, но не записывают)
save-if: ${{ github.ref == 'refs/heads/main' }}
# Кешируем реестр Cargo, git-чекауты и директорию target
cache-targets: true
cache-all-crates: true
Подводные камни инвалидации кеша:
| Проблема | Решение |
|---|---|
| Кеш растёт без границ (>5 ГБ) | Задайте prefix-key: "v2-rust", чтобы принудительно создать новый кеш |
| Разные фичи засоряют кеш | Используйте key: ${{ hashFiles('**/Cargo.lock') }} |
| Кеш из PR перезаписывает main | Задайте save-if: ${{ github.ref == 'refs/heads/main' }} |
| Цели кросс-компиляции раздувают кеш | Используйте отдельный key для каждой тройки целей (target triple) |
Совместное использование кеша между джобами:
Джоба check сохраняет кеш, а последующие джобы (test, cross, coverage) его читают.
Благодаря save-if только для main запуски PR получают выгоду от кешированных зависимостей
и не записывают устаревшие кеши.
Измеренный эффект на крупном воркспейсе: холодная сборка ~4 мин → сборка из кеша ~45 с. Одно только действие кеширования экономит около 25 минут CI-времени за запуск конвейера (по всем параллельным джобам).
Makefile.toml с cargo-make
cargo-make — переносимый исполнитель задач,
который работает на разных платформах (в отличие от make/Makefile):
# Установка
cargo install cargo-make
# Makefile.toml — в корне воркспейса
[config]
default_to_workspace = false
# ─── Рабочие сценарии разработчика ───
[tasks.dev]
description = "Полная локальная проверка (те же проверки, что и в CI)"
dependencies = ["check", "test", "clippy", "fmt-check"]
[tasks.check]
command = "cargo"
args = ["check", "--workspace", "--all-targets"]
[tasks.test]
command = "cargo"
args = ["test", "--workspace"]
[tasks.clippy]
command = "cargo"
args = ["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"]
[tasks.fmt]
command = "cargo"
args = ["fmt", "--all"]
[tasks.fmt-check]
command = "cargo"
args = ["fmt", "--all", "--", "--check"]
# ─── Покрытие ───
[tasks.coverage]
description = "Генерация HTML-отчёта о покрытии"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--html", "--open"]
[tasks.coverage-ci]
description = "Генерация LCOV для загрузки в CI"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--lcov", "--output-path", "lcov.info"]
# ─── Бенчмарки ───
[tasks.bench]
description = "Запуск всех бенчмарков"
command = "cargo"
args = ["bench"]
# ─── Кросс-компиляция ───
[tasks.build-musl]
description = "Сборка статического бинарника (musl)"
command = "cargo"
args = ["build", "--release", "--target", "x86_64-unknown-linux-musl"]
[tasks.build-arm]
description = "Сборка для aarch64 (требует cross)"
command = "cross"
args = ["build", "--release", "--target", "aarch64-unknown-linux-gnu"]
[tasks.build-all]
description = "Сборка для всех целей развёртывания"
dependencies = ["build-musl", "build-arm"]
# ─── Проверка безопасности ───
[tasks.miri]
description = "Запуск Miri на всех тестах"
toolchain = "nightly"
command = "cargo"
args = ["miri", "test", "--workspace"]
[tasks.audit]
description = "Проверка на известные уязвимости"
install_crate = "cargo-audit"
command = "cargo"
args = ["audit"]
# ─── Релиз ───
[tasks.release-dry]
description = "Показать, что сделал бы cargo-release (без выполнения)"
install_crate = "cargo-release"
command = "cargo"
args = ["release", "--workspace", "--dry-run"]
Использование:
# Аналог конвейера CI, но локально
cargo make dev
# Генерация и просмотр покрытия
cargo make coverage
# Сборка под все цели
cargo make build-all
# Проверки безопасности
cargo make miri
# Проверка на уязвимости
cargo make audit
Pre-commit-хуки: собственные скрипты и cargo-husky
Ловите проблемы до того, как они попадут в CI. Рекомендуемый подход — собственный git-хук: он простой, прозрачный и не имеет внешних зависимостей:
#!/bin/sh
# .githooks/pre-commit
set -e
echo "=== Проверки перед коммитом ==="
# Сначала быстрые проверки
echo "→ cargo fmt --check"
cargo fmt --all -- --check
echo "→ cargo check"
cargo check --workspace --all-targets
echo "→ cargo clippy"
cargo clippy --workspace --all-targets -- -D warnings
echo "→ cargo test (только lib, быстро)"
cargo test --workspace --lib
echo "=== Все проверки пройдены ==="
# Установка хука
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit
Альтернатива: cargo-husky (автоматически устанавливает хуки через build-скрипт):
⚠️ Примечание:
cargo-huskyне обновлялся с 2022 года. Он всё ещё работает, но фактически не поддерживается. Для новых проектов рассмотрите описанный выше собственный хук.
cargo install cargo-husky
# Cargo.toml — добавьте в dev-dependencies корневого крейта
[dev-dependencies]
cargo-husky = { version = "1", default-features = false, features = [
"precommit-hook",
"run-cargo-check",
"run-cargo-clippy",
"run-cargo-fmt",
"run-cargo-test",
] }
Workflow релиза: cargo-release и cargo-dist
cargo-release — автоматизирует повышение версии, создание тегов и публикацию:
# Установка
cargo install cargo-release
# release.toml — в корне воркспейса
[workspace]
consolidate-commits = true
pre-release-commit-message = "chore: release {{version}}"
tag-message = "v{{version}}"
tag-name = "v{{version}}"
# Не публикуем внутренние крейты
[[package]]
name = "core_lib"
release = false
[[package]]
name = "diag_framework"
release = false
# Публикуем только основной бинарник
[[package]]
name = "diag_tool"
release = true
# Предварительный просмотр релиза
cargo release patch --dry-run
# Выполнение релиза (повышает версию, коммитит, ставит тег, при необходимости публикует)
cargo release patch --execute
# 0.1.0 → 0.1.1
cargo release minor --execute
# 0.1.1 → 0.2.0
cargo-dist — генерирует готовые к скачиванию бинарники для GitHub Releases:
# Установка
cargo install cargo-dist
# Инициализация (создаёт CI-workflow и метаданные)
cargo dist init
# Предварительный просмотр того, что будет собрано
cargo dist plan
# Генерация релиза (обычно выполняется CI при пуше тега)
cargo dist build
# Дополнения в Cargo.toml, которые добавляет `cargo dist init`
[workspace.metadata.dist]
cargo-dist-version = "0.28.0"
ci = "github"
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
]
install-path = "CARGO_HOME"
Это генерирует workflow GitHub Actions, который при пуше тега:
- Собирает бинарник для всех целевых платформ
- Создаёт GitHub Release с архивами
.tar.gz/.zipдля скачивания - Генерирует установочные скрипты для shell и PowerShell
- Публикует на crates.io (если это настроено)
Try It Yourself — итоговое упражнение
Это упражнение связывает все главы. Вы создадите полный инженерный конвейер для нового воркспейса Rust:
-
Создайте новый воркспейс из двух крейтов: библиотеки (
core_lib) и бинарника (cli). Добавьтеbuild.rs, который встраивает хеш git и временную метку сборки, используяSOURCE_DATE_EPOCH(гл. 1). -
Настройте кросс-компиляцию для
x86_64-unknown-linux-muslиaarch64-unknown-linux-gnu. Убедитесь, что обе цели собираются черезcargo zigbuildилиcross(гл. 2). -
Добавьте бенчмарк с помощью Criterion или Divan для одной функции в
core_lib. Запустите его локально и зафиксируйте базовую линию (гл. 3). -
Измерьте покрытие кода с помощью
cargo llvm-cov. Задайте минимальный порог 80% и убедитесь, что он проходит (гл. 4). -
Запустите
cargo +nightly careful testиcargo miri test. Добавьте тест, который проверяетunsafe-код, если он у вас есть (гл. 5). -
Настройте
cargo-denyсdeny.toml, который запрещаетopensslи требует лицензии MIT/Apache-2.0 (гл. 6). -
Оптимизируйте профиль релиза с
lto = "thin",strip = trueиcodegen-units = 1. Замерьте размер бинарника до и после с помощьюcargo bloat(гл. 7). -
Добавьте проверку
cargo hack --each-feature. Создайте флаг фичи для необязательной зависимости и убедитесь, что он компилируется отдельно (гл. 9). -
Напишите workflow GitHub Actions (эта глава) со всеми 6 этапами. Добавьте
Swatinem/rust-cache@v2с настройкойsave-if.
Критерий успеха: отправьте код на GitHub → все этапы CI зелёные → cargo dist plan
показывает ваши целевые платформы релиза. Теперь у вас есть продакшн-уровня конвейер на Rust.
Архитектура CI-конвейера
flowchart LR
subgraph "Этап 1 — быстрая обратная связь < 2 мин"
CHECK["cargo check<br/>cargo clippy<br/>cargo fmt"]
end
subgraph "Этап 2 — тесты < 5 мин"
TEST["cargo nextest<br/>cargo test --doc"]
end
subgraph "Этап 3 — покрытие"
COV["cargo llvm-cov<br/>порог 80%"]
end
subgraph "Этап 4 — безопасность"
SEC["cargo audit<br/>cargo deny check"]
end
subgraph "Этап 5 — кросс-сборка"
CROSS["musl static<br/>aarch64 + x86_64"]
end
subgraph "Этап 6 — релиз (только теги)"
REL["cargo dist<br/>GitHub Release"]
end
CHECK --> TEST --> COV --> SEC --> CROSS --> REL
style CHECK fill:#91e5a3,color:#000
style TEST fill:#91e5a3,color:#000
style COV fill:#e3f2fd,color:#000
style SEC fill:#ffd43b,color:#000
style CROSS fill:#e3f2fd,color:#000
style REL fill:#b39ddb,color:#000
Ключевые выводы
- Стройте CI как параллельные этапы: быстрые проверки — первыми, дорогие джобы — за шлюзами
Swatinem/rust-cache@v2сsave-if: ${{ github.ref == 'refs/heads/main' }}предотвращает перезапись кеша из PR- Запускайте Miri и более тяжёлые санитайзеры по расписанию
schedule:в nightly, а не на каждый push Makefile.toml(cargo make) объединяет многоинструментальные сценарии в одну команду для локальной разработкиcargo-distавтоматизирует сборку релизов для разных платформ — больше не нужно писать матрицу платформ в YAML вручную
Проверенные на практике приёмы 🟡
Чему вы научитесь:
- Проверенные в бою паттерны, которые не умещаются в одну главу
- Типичные ловушки и способы их обойти — от нестабильного CI до раздувания бинарника
- Быстрые приёмы, которые можно применить к любому проекту на Rust уже сегодня
Перекрёстные ссылки: каждая глава этой книги — приёмы затрагивают все темы
Эта глава собирает инженерные паттерны, которые регулярно встречаются в продакшн-кодовых базах на Rust. Каждый приём самостоятельный — читайте их в любом порядке.
1. Ловушка deny(warnings)
Проблема: #![deny(warnings)] в исходном коде ломает сборку, когда Clippy добавляет новые
линты: код, который компилировался вчера, сегодня не собирается.
Решение: используйте CARGO_ENCODED_RUSTFLAGS в CI вместо атрибута на уровне исходного кода:
# CI: считаем предупреждения ошибками, не трогая исходный код
env:
CARGO_ENCODED_RUSTFLAGS: "-Dwarnings"
Или используйте [workspace.lints] для более точной настройки:
# Cargo.toml
[workspace.lints.rust]
unsafe_code = "deny"
[workspace.lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }
См. Инструменты времени компиляции, линты воркспейса для полного паттерна.
2. Компилируем один раз, тестируем везде
Проблема: cargo test пересобирает код при переключении между --lib, --doc и --test,
потому что они используют разные профили.
Решение: используйте cargo nextest для модульных и интеграционных тестов, а doc-тесты
запускайте отдельно:
cargo nextest run --workspace # Быстро: параллельно, с кешем
cargo test --workspace --doc # Doc-тесты (nextest их запустить не может)
См. Инструменты времени компиляции для настройки
cargo-nextest.
3. Гигиена фич
Проблема: у библиотечного крейта default = ["std"], но никто не тестирует
--no-default-features. Однажды пользователь для встраиваемых систем сообщает, что крейт
не компилируется.
Решение: добавьте cargo-hack в CI:
- name: Матрица фич
run: |
cargo hack check --each-feature --no-dev-deps
cargo check --no-default-features
cargo check --all-features
См.
no_stdи проверка фич для полного паттерна.
4. Дебаты о lock-файле — коммитить или игнорировать?
Простое правило:
| Тип крейта | Коммитить Cargo.lock? | Почему |
|---|---|---|
| Бинарник / приложение | Да | Воспроизводимые сборки |
| Библиотека | Нет (.gitignore) | Пусть потребители выбирают версии |
| Воркспейс, содержащий оба типа | Да | Побеждает бинарник |
Добавьте проверку в CI, чтобы lock-файл оставался актуальным:
- name: Проверка lock-файла
run: cargo update --locked # Падает, если Cargo.lock устарел
5. Отладочные сборки с оптимизированными зависимостями
Проблема: отладочные сборки мучительно медленные, потому что зависимости (особенно serde,
regex) не оптимизированы.
Решение: оптимизируйте зависимости в dev-профиле, оставив свой код неоптимизированным для быстрой пересборки:
# Cargo.toml
[profile.dev.package."*"]
opt-level = 2 # Оптимизировать все зависимости в dev-режиме
Это немного замедляет первую сборку, зато заметно ускоряет выполнение во время разработки. Особенно полезно для сервисов с базами данных и парсеров.
См. Профили релиза для переопределений профиля по отдельным крейтам.
6. Постоянная перезапись кеша CI
Проблема: Swatinem/rust-cache@v2 сохраняет новый кеш при каждом PR, раздувая хранилище
и замедляя восстановление.
Решение: сохраняйте кеш только из main, а восстанавливать его можно откуда угодно:
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
Для воркспейсов с несколькими бинарниками добавьте shared-key:
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci-${{ matrix.target }}"
save-if: ${{ github.ref == 'refs/heads/main' }}
См. Собираем всё вместе для полного workflow.
7. RUSTFLAGS против CARGO_ENCODED_RUSTFLAGS
Проблема: RUSTFLAGS="-Dwarnings" применяется ко всему — включая build-скрипты и
процедурные макросы. Предупреждение в build.rs крейта serde_derive роняет ваш CI.
Решение: используйте CARGO_ENCODED_RUSTFLAGS, который действует только на верхнеуровневый крейт:
# ПЛОХО — ломается из-за предупреждений build-скриптов сторонних крейтов
RUSTFLAGS="-Dwarnings" cargo build
# ХОРОШО — затрагивает только ваш крейт
CARGO_ENCODED_RUSTFLAGS="-Dwarnings" cargo build
# ТОЖЕ ХОРОШО — линты воркспейса (Cargo.toml)
[workspace.lints.rust]
warnings = "deny"
8. Воспроизводимые сборки с SOURCE_DATE_EPOCH
Проблема: встраивание chrono::Utc::now() в build.rs делает сборки невоспроизводимыми:
каждая сборка даёт другой хеш бинарника.
Решение: учитывайте SOURCE_DATE_EPOCH:
#![allow(unused)]
fn main() {
// build.rs
let timestamp = std::env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or_else(|| chrono::Utc::now().timestamp());
println!("cargo:rustc-env=BUILD_TIMESTAMP={timestamp}");
}
См. Build-скрипты для полных паттернов build.rs.
9. Workflow дедупликации cargo tree
Проблема: cargo tree --duplicates показывает 5 версий syn и 3 версии tokio-util.
Время компиляции мучительно большое.
Решение: систематическая дедупликация:
# Шаг 1: находим дубликаты
cargo tree --duplicates
# Шаг 2: находим, кто тянет старую версию
cargo tree --invert --package syn@1.0.109
# Шаг 3: обновляем виновника
cargo update -p serde_derive # Может подтянуть syn 2.x
# Шаг 4: если обновления нет, фиксируем через [patch]
# [patch.crates-io]
# old-crate = { git = "...", branch = "syn2-migration" }
# Шаг 5: проверяем
cargo tree --duplicates # Должно стать короче
См. Управление зависимостями для
cargo-denyи безопасности цепочки поставок.
10. Дымовой тест перед push
Проблема: вы делаете push, CI работает 10 минут и падает из-за проблемы с форматированием.
Решение: запускайте быстрые проверки локально перед push:
# Makefile.toml (cargo-make)
[tasks.pre-push]
description = "Локальный дымовой тест перед push"
script = '''
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --lib
'''
cargo make pre-push # < 30 секунд
git push
Или используйте git-хук pre-push:
#!/bin/sh
# .git/hooks/pre-push
cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings
См. Собираем всё вместе для паттернов
Makefile.toml.
🏋️ Упражнения
🟢 Упражнение 1: примените три приёма
Выберите три приёма из этой главы и примените их к существующему проекту на Rust. Какой оказал наибольший эффект?
Решение
Типичная комбинация с наибольшим эффектом:
-
[profile.dev.package."*"] opt-level = 2— немедленное улучшение скорости выполнения в dev-режиме (в 2–10× быстрее для кода, который много парсит) -
CARGO_ENCODED_RUSTFLAGS— устраняет ложные падения CI из-за предупреждений сторонних крейтов -
cargo-hack --each-feature— обычно находит хотя бы одну нерабочую комбинацию фич в любом проекте с 3+ фичами
# Применяем приём 5:
echo '[profile.dev.package."*"]' >> Cargo.toml
echo 'opt-level = 2' >> Cargo.toml
# Применяем приём 7 в CI:
# Заменяем RUSTFLAGS на CARGO_ENCODED_RUSTFLAGS
# Применяем приём 3:
cargo install cargo-hack
cargo hack check --each-feature --no-dev-deps
🟡 Упражнение 2: устраните дубликаты в дереве зависимостей
Запустите cargo tree --duplicates на реальном проекте. Устраните хотя бы один дубликат.
Замерьте время компиляции до и после.
Решение
# До
time cargo build --release 2>&1 | tail -1
cargo tree --duplicates | wc -l # Считаем строки с дубликатами
# Находим и исправляем один дубликат
cargo tree --duplicates
cargo tree --invert --package <duplicate-crate>@<old-version>
cargo update -p <parent-crate>
# После
time cargo build --release 2>&1 | tail -1
cargo tree --duplicates | wc -l # Должно стать меньше
# Типичный результат: снижение времени компиляции на 5–15% за каждый устранённый
# дубликат (особенно для тяжёлых крейтов вроде syn, tokio)
Ключевые выводы
- Используйте
CARGO_ENCODED_RUSTFLAGSвместоRUSTFLAGS, чтобы не ломать build-скрипты сторонних крейтов [profile.dev.package."*"] opt-level = 2— самый эффективный приём для улучшения опыта разработки- Настройка кеша (
save-ifтолько для main) предотвращает раздувание кеша CI в активных репозиториях cargo tree --duplicates+cargo update— бесплатный выигрыш во времени компиляции: делайте это раз в месяц- Запускайте быстрые проверки локально через
cargo make pre-push, чтобы не тратить время на круги через CI
Краткая справочная карточка
Шпаргалка: команды под рукой
# ─── Build-скрипты ───
cargo build # Сначала компилирует build.rs, затем крейт
cargo build -vv # Подробный режим — показывает вывод build.rs
# ─── Кросс-компиляция ───
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17
cross build --release --target aarch64-unknown-linux-gnu
# ─── Бенчмаркинг ───
cargo bench # Запустить все бенчмарки
cargo bench -- parse # Запустить бенчмарки, в названии которых есть «parse»
cargo flamegraph -- --args # Построить флеймграф по бинарнику
perf record -g ./target/release/bin # Записать данные perf
perf report # Просмотреть данные perf в интерактивном режиме
# ─── Покрытие ───
cargo llvm-cov --html # HTML-отчёт
cargo llvm-cov --lcov --output-path lcov.info
cargo llvm-cov --workspace --fail-under-lines 80
cargo tarpaulin --out Html # Альтернативный инструмент
# ─── Проверка безопасности ───
cargo +nightly miri test # Запустить тесты под Miri
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test
valgrind --leak-check=full ./target/debug/binary
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu
# ─── Аудит и цепочка поставок ───
cargo audit # Поиск известных уязвимостей
cargo audit --deny warnings # Падать в CI при любой рекомендации
cargo deny check # Проверки лицензий, рекомендаций, запретов и источников
cargo deny list # Список всех лицензий в дереве зависимостей
cargo vet # Проверка доверия к цепочке поставок
cargo outdated --workspace # Поиск устаревших зависимостей
cargo semver-checks # Обнаружение несовместимых изменений API
cargo geiger # Подсчёт unsafe в дереве зависимостей
# ─── Оптимизация бинарника ───
cargo bloat --release --crates # Вклад каждого крейта в размер
cargo bloat --release -n 20 # 20 самых больших функций
cargo +nightly udeps --workspace # Поиск неиспользуемых зависимостей
cargo machete # Быстрый поиск неиспользуемых зависимостей
cargo expand --lib module::name # Посмотреть раскрытия макросов
cargo msrv find # Определить минимальную версию Rust
cargo clippy --fix --workspace --allow-dirty # Автоисправление предупреждений линтеров
# ─── Оптимизация времени компиляции ───
export RUSTC_WRAPPER=sccache # Общий кеш компиляции
sccache --show-stats # Статистика попаданий в кеш
cargo nextest run # Более быстрый запускатель тестов
cargo nextest run --retries 2 # Повторять нестабильные тесты
# ─── Платформенная инженерия ───
cargo check --target thumbv7em-none-eabihf # Проверить сборку no_std
cargo build --target x86_64-pc-windows-gnu # Кросс-компиляция под Windows
cargo xwin build --target x86_64-pc-windows-msvc # Кросс-компиляция с ABI MSVC
cfg!(target_os = "linux") # cfg времени компиляции (даёт bool)
# ─── Релиз ───
cargo release patch --dry-run # Предварительный просмотр релиза
cargo release patch --execute # Повысить версию, закоммитить, поставить тег, опубликовать
cargo dist plan # Предварительный просмотр артефактов дистрибуции
Таблица решений: какой инструмент когда
| Задача | Инструмент | Когда использовать |
|---|---|---|
| Встроить хеш git / информацию о сборке | build.rs | Бинарнику нужна прослеживаемость |
| Скомпилировать C-код из Rust | крейт cc в build.rs | FFI к небольшим C-библиотекам |
| Сгенерировать код из схем | prost-build / tonic-build | Protobuf, gRPC, FlatBuffers |
| Слинковать системную библиотеку | pkg-config в build.rs | OpenSSL, libpci, systemd |
| Статический бинарник для Linux | --target x86_64-unknown-linux-musl | Развёртывание в контейнерах и облаке |
| Собрать под старую glibc | cargo-zigbuild | Совместимость с RHEL 7, CentOS 7 |
| Бинарник для ARM-сервера | cross или cargo-zigbuild | Развёртывание на Graviton/Ampere |
| Статистические бенчмарки | Criterion.rs | Обнаружение регрессий производительности |
| Быстрая проверка производительности | Divan | Профилирование во время разработки |
| Поиск горячих точек | cargo flamegraph / perf | Когда бенчмарк уже показал медленный код |
| Покрытие строк и ветвлений | cargo-llvm-cov | Пороги покрытия в CI, анализ пробелов |
| Быстрая проверка покрытия | cargo-tarpaulin | Локальная разработка |
| Обнаружение UB в Rust | Miri | Чистый Rust unsafe-код |
| Безопасность памяти при FFI с C | Valgrind memcheck | Смешанные кодовые базы Rust/C |
| Обнаружение гонок данных | TSan или Miri | Конкурентный unsafe-код |
| Обнаружение переполнения буфера | ASan | unsafe арифметика указателей |
| Обнаружение утечек | Valgrind или LSan | Долго работающие сервисы |
| Локальный аналог CI | cargo-make | Автоматизация рабочих сценариев разработчика |
| Проверки перед коммитом | cargo-husky или git-хуки | Ловить проблемы до push |
| Автоматические релизы | cargo-release + cargo-dist | Управление версиями и дистрибуция |
| Аудит зависимостей | cargo-audit / cargo-deny | Безопасность цепочки поставок |
| Соответствие лицензиям | cargo-deny (лицензии) | Коммерческие и корпоративные проекты |
| Доверие к цепочке поставок | cargo-vet | Среды с высокими требованиями безопасности |
| Поиск устаревших зависимостей | cargo-outdated | Плановое обслуживание |
| Обнаружение несовместимых изменений | cargo-semver-checks | Публикация библиотечных крейтов |
| Анализ дерева зависимостей | cargo tree --duplicates | Дедупликация и сокращение графа зависимостей |
| Анализ размера бинарника | cargo-bloat | Развёртывания с ограничением по размеру |
| Поиск неиспользуемых зависимостей | cargo-udeps / cargo-machete | Сокращение времени компиляции и размера |
| Настройка LTO | lto = true или "thin" | Оптимизация релизного бинарника |
| Бинарник, оптимизированный по размеру | opt-level = "z" + strip = true | Встраиваемые системы / WASM / контейнеры |
| Аудит использования unsafe | cargo-geiger | Соблюдение политики безопасности |
| Отладка макросов | cargo-expand | Отладка derive и macro_rules |
| Более быстрая линковка | линкер mold | Внутренний цикл разработчика |
| Кеш компиляции | sccache | Скорость CI и локальных сборок |
| Более быстрые тесты | cargo-nextest | Скорость тестов в CI и локально |
| Соответствие MSRV | cargo-msrv | Публикация библиотек |
Библиотека no_std | #![no_std] + default-features = false | Встраиваемые системы, UEFI, WASM |
| Кросс-компиляция под Windows | cargo-xwin / MinGW | Сборки Linux → Windows |
| Платформенная абстракция | #[cfg] + паттерн с трейтами | Кодовые базы для нескольких ОС |
| Вызовы API Windows | windows-sys / крейт windows | Нативная функциональность Windows |
| Сквозной замер времени | hyperfine | Бенчмарки всего бинарника, сравнение до и после |
| Property-based тестирование | proptest | Поиск крайних случаев, устойчивость парсеров |
| Snapshot-тестирование | insta | Проверка больших структурированных выводов |
| Фаззинг с обратной связью по покрытию | cargo-fuzz | Поиск падений в парсерах |
| Проверка моделей конкурентности | loom | Структуры данных без блокировок, порядок атомарных операций |
| Тестирование комбинаций фич | cargo-hack | Крейты с несколькими фичами #[cfg] |
| Быстрые проверки UB (почти нативная скорость) | cargo-careful | Ворота безопасности в CI, легче Miri |
| Автопересборка при сохранении | cargo-watch | Внутренний цикл разработчика, быстрая обратная связь |
| Документация воркспейса | cargo doc + rustdoc | Поиск API, онбординг, проверка ссылок в CI |
| Воспроизводимые сборки | --locked + SOURCE_DATE_EPOCH | Проверка целостности релиза |
| Настройка кеша CI | Swatinem/rust-cache@v2 | Сокращение времени сборки (с холодного старта → из кеша) |
| Политика линтов воркспейса | [workspace.lints] в Cargo.toml | Единые линты Clippy и компилятора во всех крейтах |
| Автоисправление предупреждений линтеров | cargo clippy --fix | Автоматическая очистка тривиальных проблем |
Дополнительная литература
Создано как сопроводительный справочник к книгам «Паттерны Rust» и «Корректность на уровне типов».
Версия 1.3 — добавлены cargo-hack, cargo-careful, cargo-watch, cargo doc, воспроизводимые сборки, стратегии кеширования CI, итоговое упражнение и диаграмма зависимостей между главами для полноты.