Как 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.tomlCARGO_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:
- Вы можете выполнять любую логику