use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct BuildInfo {
pub name: &'static str,
pub version: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub commit: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub built_at: Option<&'static str>,
pub profile: &'static str,
}
impl BuildInfo {
pub fn short_commit(&self) -> Option<&str> {
self.commit.map(|commit| &commit[..commit.len().min(7)])
}
pub fn is_debug(&self) -> bool {
self.profile == "debug"
}
pub fn summary(&self) -> String {
let mut parts = Vec::new();
if let Some(commit) = self.short_commit() {
parts.push(commit.to_string());
}
parts.push(self.profile.to_string());
format!("{} {} ({})", self.name, self.version, parts.join(", "))
}
}
impl std::fmt::Display for BuildInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.summary())
}
}
#[macro_export]
macro_rules! build_info {
() => {
$crate::build_info::BuildInfo {
name: env!("CARGO_PKG_NAME"),
version: env!("CARGO_PKG_VERSION"),
commit: option_env!("GIT_SHA")
.or(option_env!("GITHUB_SHA"))
.or(option_env!("VERGEN_GIT_SHA")),
built_at: option_env!("BUILD_TIMESTAMP").or(option_env!("SOURCE_DATE_EPOCH")),
profile: if cfg!(debug_assertions) { "debug" } else { "release" },
}
};
}
#[cfg(test)]
mod tests {
use super::*;
fn info() -> BuildInfo {
BuildInfo {
name: "identity",
version: "2.4.1",
commit: Some("9f3c2ab1d4e5f60718293a4b5c6d7e8f90123456"),
built_at: Some("2026-07-25T09:14:00Z"),
profile: "release",
}
}
#[test]
fn the_macro_reads_the_crate_it_is_written_in() {
let info = build_info!();
assert_eq!(info.name, "rainier-support");
assert_eq!(info.version, env!("CARGO_PKG_VERSION"));
assert_eq!(info.profile, "debug");
assert!(info.is_debug());
}
#[test]
fn a_commit_is_shortened_to_what_people_paste() {
assert_eq!(info().short_commit(), Some("9f3c2ab"));
}
#[test]
fn a_short_commit_is_not_truncated_past_its_end() {
let mut info = info();
info.commit = Some("ab1");
assert_eq!(info.short_commit(), Some("ab1"));
}
#[test]
fn no_commit_is_none_rather_than_unknown() {
let mut info = info();
info.commit = None;
assert_eq!(info.short_commit(), None);
assert_eq!(info.summary(), "identity 2.4.1 (release)");
}
#[test]
fn the_summary_is_one_line_worth_reading() {
assert_eq!(info().summary(), "identity 2.4.1 (9f3c2ab, release)");
assert_eq!(info().to_string(), info().summary());
}
#[test]
fn absent_fields_are_left_out_of_the_json_entirely() {
let mut info = info();
info.commit = None;
info.built_at = None;
let json = serde_json::to_value(&info).unwrap();
assert_eq!(
json,
serde_json::json!({
"name": "identity",
"version": "2.4.1",
"profile": "release",
})
);
}
#[test]
fn the_whole_thing_serialises() {
assert_eq!(
serde_json::to_value(info()).unwrap(),
serde_json::json!({
"name": "identity",
"version": "2.4.1",
"commit": "9f3c2ab1d4e5f60718293a4b5c6d7e8f90123456",
"built_at": "2026-07-25T09:14:00Z",
"profile": "release",
})
);
}
}