Avatar ·

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

Всем привет! Я пишу CLI-утилиту на Rust и хочу, чтобы при запуске с флагом `--version` выводилась версия программы. Но я не хочу хардкодить строку "0.1.0" в коде — это же неудобно, версию нужно менять только в `Cargo.toml`. Проблема: я не могу понять, как из самой программы на Rust прочитать данные из `Cargo.toml`. Я попробовал использовать `std::fs::read_to_string("Cargo.toml")`, но это работает только если программа запущена из корня проекта, а после установки через `cargo install` файл `Cargo.toml` рядом с бинарником не лежит. Вот мой текущий код, который, конечно, не работает как надо: ```rust fn main() { let version = std::fs::read_to_string("Cargo.toml") .ok() .and_then(|s| { s.lines() .find(|l| l.starts_with("version")) .map(|l| l.trim().trim_start_matches("version = \"").trim_end_matches('"')) }) .unwrap_or("unknown"); println!("MyApp v{}", version); } ``` Он выводит "unknown" после установки. Как правильно получать метаданные пакета (имя, версию, описание, авторов) из кода на Rust? Есть ли встроенный макрос или крейт для этого? Заранее спасибо! <<>>

Использование макроса env!() для доступа к метаданным Cargo

Проблема, с которой вы столкнулись, очень распространена. К счастью, в Rust есть элегантное встроенное решение — макрос env!(). Во время компиляции Cargo передаёт множество переменных окружения, включая CARGO_PKG_VERSION, CARGO_PKG_NAME и другие. Макрос env!() позволяет считать эти переменные на этапе компиляции и встроить их значения прямо в бинарный файл.

Вот как это работает:

fn main() {
    let name = env!("CARGO_PKG_NAME");
    let version = env!("CARGO_PKG_VERSION");
    let description = env!("CARGO_PKG_DESCRIPTION");
    let authors = env!("CARGO_PKG_AUTHORS");
    let repository = env!("CARGO_PKG_REPOSITORY");

    println!("{} v{}", name, version);
    if !description.is_empty() {
        println!("{}", description);
    }
    println!("Авторы: {}", authors);
    if !repository.is_empty() {
        println!("Репозиторий: {}", repository);
    }
}

Этот код будет работать всегда — и при запуске из исходников, и после установки через cargo install, и в Docker-контейнере. Значения подставляются компилятором на этапе сборки.

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

Cargo предоставляет следующие переменные окружения для доступа к метаданным пакета:

  • CARGO_PKG_NAME — имя пакета из [package].name
  • CARGO_PKG_VERSION — полная версия (например, "1.2.3")
  • CARGO_PKG_VERSION_MAJOR — только мажорная версия (1)
  • CARGO_PKG_VERSION_MINOR — только минорная версия (2)
  • CARGO_PKG_VERSION_PATCH — только патч-версия (3)
  • 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_RUST_VERSION — минимальная версия Rust
  • CARGO_CRATE_NAME — имя крейта (может отличаться от имени пакета)

Обработка отсутствующих полей

Макрос env!() вызовет ошибку компиляции, если переменная не задана. Для опциональных полей (например, описание или репозиторий могут быть пустыми) используйте option_env!():

fn main() {
    let name = env!("CARGO_PKG_NAME");
    let version = env!("CARGO_PKG_VERSION");

    let description = option_env!("CARGO_PKG_DESCRIPTION")
        .unwrap_or("Без описания");

    println!("{} v{} — {}", name, version, description);
}

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

Для больших проектов можно создать отдельную структуру:

struct PackageInfo {
    name: &'static str,
    version: &'static str,
    description: Option<&'static str>,
    authors: Vec<&'static str>,
}

impl PackageInfo {
    fn from_cargo() -> Self {
        Self {
            name: env!("CARGO_PKG_NAME"),
            version: env!("CARGO_PKG_VERSION"),
            description: option_env!("CARGO_PKG_DESCRIPTION"),
            authors: env!("CARGO_PKG_AUTHORS")
                .split(':')
                .collect(),
        }
    }

    fn print_version(&self) {
        println!("{} v{}", self.name, self.version);
    }
}

fn main() {
    let info = PackageInfo::from_cargo();
    info.print_version();
}

Этот подход гарантирует, что метаданные всегда актуальны и соответствуют вашему Cargo.toml. Никакого хардкода и проблем с чтением файлов во время выполнения!

<<>>

Альтернативный подход: крейт built для расширенных метаданных

Если вам нужно больше, чем просто поля из Cargo.toml — например, хэш коммита Git, дата сборки или информация о зависимостях — рекомендую использовать крейт built.

Он работает через build script (build.rs) и генерирует Rust-файл с метаданными на этапе компиляции:

// build.rs
fn main() {
    built::write_built_file().expect("Не удалось сгенерировать метаданные");
}

Затем в основном коде:

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

fn main() {
    println!("Версия: {}", built::PKG_VERSION);
    println!("Git commit: {}", built::GIT_COMMIT_HASH.unwrap_or("неизвестно"));
    println!("Время сборки: {}", built::BUILT_TIME_UTC);
}

Плюсы: можно получить Git-хэш, информацию о зависимостях, флаги компиляции.

Минусы: требуется build script, что усложняет сборку. Для простого получения версии из Cargo.toml макрос env!() — более лёгкое и быстрое решение.

Выбирайте подход в зависимости от ваших потребностей: env!() для базовых метаданных, built для расширенной информации о сборке.