Avatar ·

Как получить метаданные Cargo пакета (версию, имя, описание) в программе на Rust?

Всем привет! Я начинаю осваивать Rust и пишу небольшую утилиту командной строки. Хочу, чтобы при запуске с флагом `--version` программа выводила не просто номер версии, а, например: "MyApp v0.2.1 (2024-05-20)". Проблема в том, что я не хочу хардкодить версию в коде — это неправильно, ведь она уже указана в файле `Cargo.toml`. Как правильно извлечь эту информацию во время компиляции или выполнения? Я пробовал читать файл `Cargo.toml` напрямую, но это выглядит костыльно, да и в релизной сборке файла может не быть. Слышал про макрос `env!()`, но не понимаю, как его привязать к Cargo. Вот пример моей текущей структуры: ```rust fn main() { // Как получить version из Cargo.toml? println!("MyApp v{}", get_version()); } ``` Мне нужно получить: имя пакета, версию, описание, авторов и, возможно, хэш коммита git. Подскажите, как это сделать правильно и идиоматично для Rust? Какие есть готовые крейты или встроенные средства? Заранее спасибо!<<>>

Использование встроенных макросов env!() и option_env!()

Самый простой и рекомендуемый способ — использовать макросы env!("CARGO_PKG_VERSION") и option_env!("CARGO_PKG_NAME"). Cargo автоматически устанавливает эти переменные окружения во время компиляции. Вот как это работает:

fn main() {
    // Обязательные поля (паника, если переменная не установлена)
    let version = env!("CARGO_PKG_VERSION");
    let name = env!("CARGO_PKG_NAME");
    
    // Необязательные поля (возвращают Option<&str>)
    let description = option_env!("CARGO_PKG_DESCRIPTION").unwrap_or("No description");
    let authors = option_env!("CARGO_PKG_AUTHORS").unwrap_or("Unknown");
    
    println!("{} v{}", name, version);
    println!("Description: {}", description);
    println!("Authors: {}", authors);
}

Полный список доступных переменных окружения Cargo:

  • CARGO_PKG_NAME — имя пакета
  • CARGO_PKG_VERSION — полная версия (например, "0.2.1")
  • CARGO_PKG_VERSION_MAJOR — мажорная версия ("0")
  • CARGO_PKG_VERSION_MINOR — минорная версия ("2")
  • CARGO_PKG_VERSION_PATCH — патч-версия ("1")
  • CARGO_PKG_VERSION_PRE — pre-release тег (если есть)
  • CARGO_PKG_AUTHORS — авторы через запятую
  • CARGO_PKG_DESCRIPTION — описание из Cargo.toml
  • CARGO_PKG_HOMEPAGE — URL домашней страницы
  • CARGO_PKG_REPOSITORY — URL репозитория
  • CARGO_PKG_LICENSE — лицензия
  • CARGO_PKG_LICENSE_FILE — путь к файлу лицензии
  • CARGO_PKG_RUST_VERSION — минимальная версия Rust
  • CARGO_PKG_README — путь к README файлу
  • CARGO_CRATE_NAME — имя крейта (может отличаться от имени пакета)
  • CARGO_MANIFEST_DIR — путь к директории с Cargo.toml

Получение хэша коммита Git

Для получения хэша коммита Git нужно использовать build.rs скрипт сборки. Создайте файл build.rs в корне вашего проекта:

use std::process::Command;
use std::env;

fn main() {
    // Получаем хэш текущего коммита
    let output = Command::new("git")
        .args(&["rev-parse", "--short", "HEAD"])
        .output();
    
    if let Ok(output) = output {
        let git_hash = String::from_utf8_lossy(&output.stdout);
        let git_hash = git_hash.trim();
        println!("cargo:rustc-env=GIT_HASH={}", git_hash);
    }
    
    // Также можно получить дату коммита
    let output = Command::new("git")
        .args(&["log", "-1", "--format=%cd", "--date=short"])
        .output();
    
    if let Ok(output) = output {
        let git_date = String::from_utf8_lossy(&output.stdout);
        let git_date = git_date.trim();
        println!("cargo:rustc-env=GIT_DATE={}", git_date);
    }
}

Теперь в основном коде:

fn main() {
    let version = env!("CARGO_PKG_VERSION");
    let git_hash = option_env!("GIT_HASH").unwrap_or("unknown");
    let git_date = option_env!("GIT_DATE").unwrap_or("unknown");
    
    println!("MyApp v{} ({} {})", version, git_hash, git_date);
}

Использование крейта built

Если вам нужно больше метаданных (например, время сборки, флаги компилятора, зависимости), рекомендую крейт built. Добавьте в Cargo.toml:

[build-dependencies]
built = "0.7"

В build.rs:

fn main() {
    built::write_built_file().expect("Failed to acquire build-time information");
}

В основном коде:

include!(concat!(env!("OUT_DIR"), "/built.rs"));

fn main() {
    println!("Version: {}", built::PKG_VERSION);
    println!("Git commit: {}", built::GIT_COMMIT_HASH.unwrap_or("unknown"));
    println!("Build time: {}", built::BUILT_TIME_UTC);
    println!("Rustc version: {}", built::RUSTC_VERSION);
}
<<>>

Использование крейта vergen для расширенной генерации метаданных

Альтернативный подход — крейт vergen, который генерирует ещё больше информации, включая информацию о Git, CI/CD, версии Rust и т.д. Вот как его использовать:

Добавьте в Cargo.toml:

[build-dependencies]
vergen = { version = "8", features = ["build", "git", "rustc"] }

В build.rs:

use vergen::EmitBuilder;

fn main() {
    EmitBuilder::builder()
        .all_build()
        .all_git()
        .all_rustc()
        .emit()
        .unwrap();
}

В основном коде:

fn main() {
    println!("Version: {}", env!("VERGEN_BUILD_SEMVER"));
    println!("Git SHA: {}", env!("VERGEN_GIT_SHA"));
    println!("Git branch: {}", env!("VERGEN_GIT_BRANCH"));
    println!("Build timestamp: {}", env!("VERGEN_BUILD_TIMESTAMP"));
    println!("Rustc: {}", env!("VERGEN_RUSTC_SEMVER"));
}

Преимущества перед built:

  • Больше готовых переменных окружения (включая информацию о CI)
  • Автоматическое определение, является ли сборка "грязной" (есть незакоммиченные изменения)
  • Поддержка Git-тегов

Недостаток: крейт тяжелее и может замедлить сборку, если не настроить выборочную генерацию.

Выбирайте подход в зависимости от ваших потребностей: для простого вывода версии достаточно встроенных макросов, для продвинутой информации используйте built или vergen.