Как Rust-программа читает свои метаданные из Cargo.toml

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как Rust-программа читает свои метаданные из Cargo.toml

Вы когда-нибудь писали CLI-утилиту на Rust и хотели, чтобы она при запуске с флагом --version выводила не "0.1.0", а именно ту версию, что указана в Cargo.toml? Или, может быть, вам нужно было логировать имя крейта и его описание — но тянуть эти строки из кода было неудобно и чревато ошибками?

Этот вопрос — один из самых популярных на Stack Overflow (тысячи просмотров!). И не зря: казалось бы, что может быть проще, чем прочитать собственный конфиг? Но в Rust есть свои подводные камни, а главное — несколько элегантных способов решения.

В этой статье мы разберём три основных подхода к получению метаданных пакета Cargo прямо из кода: от простого макроса env!() до гибкого build.rs. Вы узнаете, как правильно читать версию, имя крейта, описание, а также как избежать типичных ошибок.

Зачем это нужно? Три типичных сценария

Прежде чем нырять в код, давайте поймём, зачем вообще лезть в метаданные пакета. Вот три реальных кейса:

  • CLI-утилиты: команда myapp --version должна выводить версию из Cargo.toml, а не хардкод.
  • Логи и телеметрия: при старте приложения полезно записать в лог имя крейта и его версию — это упрощает отладку.
  • Автоматическая генерация документации: можно подставить описание крейта в help-сообщение или README, генерируемый на лету.

Теперь, когда мы поняли "зачем", переходим к "как".

Способ №1: Макрос env!() — самый простой и популярный

Самый прямой путь — использовать встроенный макрос env!(). Он позволяет читать переменные окружения, которые Cargo автоматически устанавливает во время компиляции. Вот список самых полезных:

  • CARGO_PKG_NAME — имя пакета (например, "my_cool_app")
  • CARGO_PKG_VERSION — версия (например, "0.2.1")
  • CARGO_PKG_DESCRIPTION — описание из поля description в Cargo.toml
  • CARGO_PKG_AUTHORS — список авторов, разделённый двоеточием
  • CARGO_PKG_LICENSE — лицензия
  • CARGO_PKG_REPOSITORY — URL репозитория

Пример использования:

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

    println!("{} v{}", name, version);
    // Вывод: my_cool_app v0.2.1
}

Важно: env!() — это макрос времени компиляции. Если переменная окружения не установлена, программа даже не скомпилируется! Это плюс (безопасность типов) и минус (нельзя сделать fallback на хардкод без дополнительных ухищрений).

Если вам нужна версия только для отладки, можно использовать option_env!(), который возвращает Option<&str>:

let version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown");
println!("Version: {}", version);

Этот способ идеален для 90% случаев. Он не требует дополнительных зависимостей и работает "из коробки".

Способ №2: include_str!() + ручной парсинг — для максимального контроля

Иногда вам нужно не просто прочитать одно поле, а получить структурированные данные из Cargo.toml. Например, все зависимости или ключевые слова. В этом случае можно прочитать сам файл как строку и распарсить его.

fn main() {
    let cargo_toml = include_str!("../Cargo.toml");
    // Здесь можно использовать toml-парсер, но для примера — простой поиск
    if let Some(line) = cargo_toml.lines().find(|l| l.starts_with("version =")) {
        let version = line.trim_start_matches("version = \"").trim_end_matches('"');
        println!("Version from file: {}", version);
    }
}

Плюсы:

  • Вы получаете полный контроль над тем, какие данные извлекать.
  • Можно обрабатывать ошибки (например, если поле отсутствует).

Минусы:

  • Нужно добавлять зависимость toml (или писать свой примитивный парсер).
  • Файл читается на этапе компиляции — любые изменения в Cargo.toml потребуют перекомпиляции.
  • Код становится более громоздким.

Этот подход стоит использовать, когда env!() не хватает. Например, если вам нужно получить массив ключевых слов (keywords = ["rust", "cli"]) или список зависимостей.

Способ №3: Build Script (build.rs) — продвинутый и гибкий

Если первые два способа — это "быстро и грязно", то build.rs — это "правильно и масштабируемо". С помощью скрипта сборки вы можете сгенерировать Rust-код, который будет содержать любые метаданные, и даже выполнить дополнительные проверки.

Создайте файл build.rs в корне вашего проекта:

use std::env;
use std::fs;
use std::path::Path;

fn main() {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let cargo_toml_path = Path::new(&manifest_dir).join("Cargo.toml");
    let content = fs::read_to_string(&cargo_toml_path).unwrap();
    let cargo: toml::Value = content.parse().unwrap();

    // Извлекаем версию
    if let Some(version) = cargo.get("package").and_then(|p| p.get("version")) {
        println!("cargo:rustc-env=MY_APP_VERSION={}", version.as_str().unwrap());
    }

    // Извлекаем описание
    if let Some(description) = cargo.get("package").and_then(|p| p.get("description")) {
        println!("cargo:rustc-env=MY_APP_DESCRIPTION={}", description.as_str().unwrap());
    }
}

Не забудьте добавить toml в build-dependencies в Cargo.toml:

[build-dependencies]
toml = "0.8"

Теперь в основном коде вы можете использовать env!("MY_APP_VERSION") так же, как и в первом способе:

fn main() {
    println!("Version: {}", env!("MY_APP_VERSION"));
    println!("Description: {}", env!("MY_APP_DESCRIPTION"));
}

Преимущества build.rs:

  • Вы можете выполнять любую логику

Блоги

Рекомендаций по книгам