Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Инженерные практики Rust — за пределами cargo build

О проекте

  • Курс основан на оригинальных материалах Rust Training. Лицензии: MIT (код) и CC-BY-4.0 (текст).
  • Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus.
  • Замечания и исправления присылайте через Issues и Pull Requests репозитория.

Практическое руководство по возможностям инструментария Rust, которые большинство команд открывают слишком поздно: build-скрипты, кросс-компиляция, бенчмаркинг, покрытие кода и проверка безопасности с помощью Miri и Valgrind. Каждая глава опирается на конкретные примеры из реального кода для диагностики оборудования — большого многокрейтового воркспейса, — поэтому каждый приём напрямую переносится в продакшн-код.

Как пользоваться этой книгой

Эта книга рассчитана на самостоятельное изучение или командные воркшопы. Каждая глава в значительной степени независима: читайте их по порядку или сразу переходите к нужной теме.

Обозначения сложности

СимволУровеньЗначение
🟢НачальныйПростые инструменты с понятными шаблонами — полезны с первого дня
🟡СреднийТребуют понимания внутреннего устройства инструментов или платформенных концепций
🔴ПродвинутыйГлубокое знание инструментов, nightly-функции или оркестрация нескольких инструментов

План изучения

ЧастьГлавыПримерное времяКлючевой результат
I — Сборка и поставкаch01–023–4 чМетаданные сборки, кросс-компиляция, статические бинарные файлы
II — Измерение и верификацияch03–054–5 чСтатистический бенчмаркинг, пороги покрытия, Miri и санитайзеры
III — Укрепление и оптимизацияch06–106–8 чБезопасность цепочки поставок, профили релиза, инструменты времени компиляции, no_std, Windows
IV — Интеграцияch11–133–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 — Сборка и поставка

№ГлаваСложностьОписание
1Build-скрипты — 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
5Miri, 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
9no_std и проверка фич🔴cargo-hack, слои core/alloc/std, пользовательские обработчики паники, тестирование кода no_std
10Windows и условная компиляция🟡Паттерны #[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-changed Cargo перезапускает 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-changedbuild.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-скриптов в большой кодовой базе — признак чистой архитектуры, а не пробел. См. Управление зависимостями, чтобы узнать, как проект управляет цепочкой поставок без собственной логики сборки.

Попробуйте сами

  1. Встройте метаданные git: создайте build.rs, который выводит APP_GIT_HASH и APP_BUILD_EPOCH как переменные окружения. Прочитайте их через env!() в main.rs и выведите информацию о сборке. Убедитесь, что хеш меняется после коммита.

  2. Проверьте системную библиотеку: напишите build.rs, который использует pkg-config, чтобы найти libz (zlib). Если библиотека найдена, выведите cargo::rustc-cfg=has_zlib. В main.rs выведите «zlib доступен» или «zlib не найден» в зависимости от флага cfg.

  3. Понаблюдайте за лишними перезапусками: удалите строку 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"

Порядок поиска файла конфигурации (побеждает первый найденный):

  1. <project>/.cargo/config.toml
  2. <project>/../.cargo/config.toml (родительские директории, подъём вверх по дереву)
  3. $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 и ручной способ:

ВозможностьВручнуюcrosscargo-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-gnuARM-серверы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 выбрать» для корпоративных дистрибутивов Linux
  • cross — самый простой путь для 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: холодный и горячий кеш не контролируются
}

Проблемы ручного замера:

  1. Исключение мёртвого кода — компилятор может вообще пропустить вычисление, если результат не используется.
  2. Нет прогрева — первый запуск включает промахи кеша, ошибки страниц ОС и ленивую инициализацию. (JIT-эффекты к Rust неприменимы, а вот ошибки страниц ОС — применимы.)
  3. Нет статистического анализа — одно измерение ничего не говорит о разбросе, выбросах или доверительных интервалах.
  4. Нет обнаружения регрессий — нельзя сравнить с предыдущими запусками.

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-querydevice_diagВызывается для каждого GPU, до 8× за запуск
Разбор событий сенсоровevent_logТысячи записей на загруженных серверах
JSON топологии PCIetopology_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);
}

Попробуйте сами

  1. Напишите бенчмарк Criterion: выберите любую функцию парсинга в своей кодовой базе. Создайте директорию benches/, настройте бенчмарк Criterion, который измеряет пропускную способность в байтах в секунду. Запустите cargo bench и изучите HTML-отчёт.

  2. Постройте флеймграф: соберите проект с debug = true в [profile.release], затем запустите cargo flamegraph -- <ваши аргументы>. Определите три самых широких стека наверху флеймграфа — это и есть ваши горячие точки.

  3. Сравните с 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-covcargo-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 и т. д.Для практических порогов ориентируйтесь на покрытие строк; покрытие ветвлений по природе ниже

Попробуйте сами

  1. Измерьте покрытие своего проекта: запустите cargo llvm-cov --workspace --html и откройте отчёт. Найдите три файла с самым низким покрытием. Они просто не тестируются или им по природе трудно быть протестированными (код, зависящий от железа)?

  2. Задайте порог покрытия: добавьте cargo llvm-cov --workspace --fail-under-lines 60 в CI. Намеренно закомментируйте тест и убедитесь, что CI падает. Затем поднимите порог до фактического покрытия проекта минус 2%.

  3. Покрытие ветвлений и строк: напишите функцию с 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-std
  • cargo-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 — когда что использовать:

АспектMiriValgrind
Проверяет 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?
ASan2× память, 2× CPUПереполнение буфера, use-after-free, переполнение стекаДаДа
MSan3× память, 3× CPUЧтение неинициализированных данныхДаДа
TSan5–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 через ioctllibc::ioctl() обходит подпроцесс ipmitoolMiri + Valgrind
Прямые запросы к драйверу GPUFFI к accel-mgmt вместо разбора вывода accel-queryValgrind (C-библиотека)
Memory-mapped конфигурация PCIemmap для прямого чтения конфигурационного пространстваASan + Valgrind
Буфер SEL без блокировокAtomicPtr для параллельного сбора событийMiri + TSan
Вариант для встраиваемых систем / no_stdМанипуляции с сырыми указателями для bare-metalMiri

Подготовка: прежде чем вводить 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 FFIMiri — интерпретатор Rust, он не может выполнять C-кодДля FFI-кода используйте Valgrind или ASan
error: unsupported operation: can't call foreign functionMiri встретил вызов extern "C"Замокайте границу FFI или ограничьте код через #[cfg(miri)]
Stacked Borrows violationНарушение правила алиасинга — даже если код «работает»Miri прав; перепишите код, чтобы не алиасить &mut с &
Санитайзер сообщает DEADLYSIGNALASan обнаружил переполнение буфераПроверьте индексацию массивов, операции со срезами и арифметику указателей
LeakSanitizer: detected memory leaksBox::leak(), forget() или пропущенный drop()Намеренно: подавите через __lsan_disable(); непреднамеренно: исправьте утечку
Miri невероятно медленныйMiri интерпретирует, а не компилирует — в 10–100× медленнееЗапускайте только тесты --lib или помечайте медленные тесты через #[cfg_attr(miri, ignore)]
TSan: false positive с атомарными операциямиTSan не до конца понимает модель порядка атомарных операций RustДобавьте TSAN_OPTIONS=suppressions=tsan.supp с конкретными подавлениями

Попробуйте сами

  1. Вызовите обнаружение UB в Miri: напишите unsafe-функцию, которая создаёт две ссылки &mut на один и тот же i32 (нарушение алиасинга). Запустите cargo +nightly miri test и посмотрите на ошибку «Stacked Borrows». Исправьте её через UnsafeCell или отдельные выделения.

  2. Запустите ASan на намеренной ошибке: создайте тест с unsafe-доступом к массиву за пределами границ. Соберите с RUSTFLAGS="-Zsanitizer=address" и посмотрите отчёт ASan. Обратите внимание, как он указывает точную строку.

  3. Измерьте накладные расходы 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 выходит далеко за рамки поиска уязвимостей. Он применяет политики по четырём направлениям:

  1. Рекомендации (advisories) — известные уязвимости (как и в cargo-audit)
  2. Лицензии — список разрешённых и запрещённых лицензий
  3. Ограничения (bans) — запрещённые крейты или дублирующиеся версии
  4. Источники (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:

  1. Установите курсор на макрос, который хотите проверить.
  2. Откройте палитру команд (например, F1 в VSCode).
  3. Найдите 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 testcargo 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 разделяет кеш компиляции между командой и CI
  • mold — самый быстрый линкер для 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 обязателен для любого крейта с условной компиляцией — запускайте его в CI
  • core → 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-syswindows
Стиль APIСырой FFI (вызовы через unsafe)Безопасные обёртки на Rust
Размер бинарникаМинимальный (только объявления extern)Больше (код обёрток)
Время компиляцииБыстроеМедленнее
ЭргономикаВ стиле C, безопасность вручнуюИдиоматично для Rust
Обработка ошибокСырые BOOL / HRESULTResult<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-gnux86_64-pc-windows-msvc
ЛинкерMinGW ldMSVC link.exe или lld-link
C-рантаймmsvcrt.dll (универсальный)ucrtbase.dll (современный)
Совместимость с C++ABI GCCABI MSVC
Кросс-компиляция из LinuxПросто (MinGW)Возможна (cargo-xwin)
Поддержка Windows APIПолнаяПолная
Формат отладочной информацииDWARFPDB
Рекомендуется дляПростых инструментов, сборок 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, который при пуше тега:

  1. Собирает бинарник для всех целевых платформ
  2. Создаёт GitHub Release с архивами .tar.gz / .zip для скачивания
  3. Генерирует установочные скрипты для shell и PowerShell
  4. Публикует на crates.io (если это настроено)

Try It Yourself — итоговое упражнение

Это упражнение связывает все главы. Вы создадите полный инженерный конвейер для нового воркспейса Rust:

  1. Создайте новый воркспейс из двух крейтов: библиотеки (core_lib) и бинарника (cli). Добавьте build.rs, который встраивает хеш git и временную метку сборки, используя SOURCE_DATE_EPOCH (гл. 1).

  2. Настройте кросс-компиляцию для x86_64-unknown-linux-musl и aarch64-unknown-linux-gnu. Убедитесь, что обе цели собираются через cargo zigbuild или cross (гл. 2).

  3. Добавьте бенчмарк с помощью Criterion или Divan для одной функции в core_lib. Запустите его локально и зафиксируйте базовую линию (гл. 3).

  4. Измерьте покрытие кода с помощью cargo llvm-cov. Задайте минимальный порог 80% и убедитесь, что он проходит (гл. 4).

  5. Запустите cargo +nightly careful test и cargo miri test. Добавьте тест, который проверяет unsafe-код, если он у вас есть (гл. 5).

  6. Настройте cargo-deny с deny.toml, который запрещает openssl и требует лицензии MIT/Apache-2.0 (гл. 6).

  7. Оптимизируйте профиль релиза с lto = "thin", strip = true и codegen-units = 1. Замерьте размер бинарника до и после с помощью cargo bloat (гл. 7).

  8. Добавьте проверку cargo hack --each-feature. Создайте флаг фичи для необязательной зависимости и убедитесь, что он компилируется отдельно (гл. 9).

  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. Какой оказал наибольший эффект?

Решение

Типичная комбинация с наибольшим эффектом:

  1. [profile.dev.package."*"] opt-level = 2 — немедленное улучшение скорости выполнения в dev-режиме (в 2–10× быстрее для кода, который много парсит)

  2. CARGO_ENCODED_RUSTFLAGS — устраняет ложные падения CI из-за предупреждений сторонних крейтов

  3. 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.rsFFI к небольшим C-библиотекам
Сгенерировать код из схемprost-build / tonic-buildProtobuf, gRPC, FlatBuffers
Слинковать системную библиотекуpkg-config в build.rsOpenSSL, libpci, systemd
Статический бинарник для Linux--target x86_64-unknown-linux-muslРазвёртывание в контейнерах и облаке
Собрать под старую glibccargo-zigbuildСовместимость с RHEL 7, CentOS 7
Бинарник для ARM-сервераcross или cargo-zigbuildРазвёртывание на Graviton/Ampere
Статистические бенчмаркиCriterion.rsОбнаружение регрессий производительности
Быстрая проверка производительностиDivanПрофилирование во время разработки
Поиск горячих точекcargo flamegraph / perfКогда бенчмарк уже показал медленный код
Покрытие строк и ветвленийcargo-llvm-covПороги покрытия в CI, анализ пробелов
Быстрая проверка покрытияcargo-tarpaulinЛокальная разработка
Обнаружение UB в RustMiriЧистый Rust unsafe-код
Безопасность памяти при FFI с CValgrind memcheckСмешанные кодовые базы Rust/C
Обнаружение гонок данныхTSan или MiriКонкурентный unsafe-код
Обнаружение переполнения буфераASanunsafe арифметика указателей
Обнаружение утечекValgrind или LSanДолго работающие сервисы
Локальный аналог CIcargo-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Сокращение времени компиляции и размера
Настройка LTOlto = true или "thin"Оптимизация релизного бинарника
Бинарник, оптимизированный по размеруopt-level = "z" + strip = trueВстраиваемые системы / WASM / контейнеры
Аудит использования unsafecargo-geigerСоблюдение политики безопасности
Отладка макросовcargo-expandОтладка derive и macro_rules
Более быстрая линковкалинкер moldВнутренний цикл разработчика
Кеш компиляцииsccacheСкорость CI и локальных сборок
Более быстрые тестыcargo-nextestСкорость тестов в CI и локально
Соответствие MSRVcargo-msrvПубликация библиотек
Библиотека no_std#![no_std] + default-features = falseВстраиваемые системы, UEFI, WASM
Кросс-компиляция под Windowscargo-xwin / MinGWСборки Linux → Windows
Платформенная абстракция#[cfg] + паттерн с трейтамиКодовые базы для нескольких ОС
Вызовы API Windowswindows-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Проверка целостности релиза
Настройка кеша CISwatinem/rust-cache@v2Сокращение времени сборки (с холодного старта → из кеша)
Политика линтов воркспейса[workspace.lints] в Cargo.tomlЕдиные линты Clippy и компилятора во всех крейтах
Автоисправление предупреждений линтеровcargo clippy --fixАвтоматическая очистка тривиальных проблем

Дополнительная литература

ТемаРесурс
Build-скрипты CargoКнига Cargo — Build Scripts
Кросс-компиляцияКросс-компиляция в Rust
Инструмент crosscross-rs/cross
cargo-zigbuildдокументация cargo-zigbuild
Criterion.rsРуководство по Criterion
Divanдокументация Divan
cargo-llvm-covcargo-llvm-cov
cargo-tarpaulinдокументация tarpaulin
MiriMiri на GitHub
Санитайзеры в Rustдокументация rustc по санитайзерам
cargo-makeкнига cargo-make
cargo-releaseдокументация cargo-release
cargo-distкнига cargo-dist
Оптимизация по профилю (PGO)Руководство Rust по PGO
Флеймграфыcargo-flamegraph
cargo-denyдокументация cargo-deny
cargo-vetдокументация cargo-vet
cargo-auditcargo-audit
cargo-bloatcargo-bloat
cargo-udepscargo-udeps
cargo-geigercargo-geiger
cargo-semver-checkscargo-semver-checks
cargo-nextestдокументация nextest
sccachesccache
Линкер moldmold
cargo-msrvcargo-msrv
LTOОпции генерации кода rustc
Профили CargoКнига Cargo — Profiles
no_stdEmbedded Book для Rust
Крейт windows-syswindows-rs
cargo-xwinдокументация cargo-xwin
cargo-hackcargo-hack
cargo-carefulcargo-careful
cargo-watchcargo-watch
Кеш Rust в CISwatinem/rust-cache
Книга по rustdocRustdoc Book
Условная компиляцияСправочник Rust — cfg
Встраиваемый RustAwesome Embedded Rust
hyperfinehyperfine
proptestproptest
instainsta — snapshot-тестирование
cargo-fuzzcargo-fuzz
loomloom — тестирование конкурентности

Создано как сопроводительный справочник к книгам «Паттерны Rust» и «Корректность на уровне типов».

Версия 1.3 — добавлены cargo-hack, cargo-careful, cargo-watch, cargo doc, воспроизводимые сборки, стратегии кеширования CI, итоговое упражнение и диаграмма зависимостей между главами для полноты.