use std::borrow::Cow;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuildInfo {
package: &'static str,
version: &'static str,
commit: &'static str,
commit_date: &'static str,
dirty: Option<bool>,
source_hash: Option<u64>,
debug_build: bool,
}
impl BuildInfo {
#[doc(hidden)]
pub const fn __from_env(
package: &'static str,
version: &'static str,
commit: Option<&'static str>,
commit_date: Option<&'static str>,
dirty: Option<&'static str>,
source_hash: Option<&'static str>,
debug_build: bool,
) -> Self {
Self {
package,
version,
commit: match commit {
Some(commit) => commit,
None => "",
},
commit_date: match commit_date {
Some(date) => date,
None => "",
},
dirty: match dirty {
Some(text) => parse_flag(text),
None => None,
},
source_hash: match source_hash {
Some(text) => parse_hash(text),
None => None,
},
debug_build,
}
}
pub fn package(&self) -> &'static str {
self.package
}
pub fn version(&self) -> &'static str {
self.version
}
pub fn commit(&self) -> &'static str {
self.commit
}
pub fn commit_date(&self) -> &'static str {
self.commit_date
}
pub fn dirty(&self) -> Option<bool> {
self.dirty
}
pub fn source_hash(&self) -> Option<u64> {
self.source_hash
}
pub fn debug_build(&self) -> bool {
self.debug_build
}
}
const fn parse_flag(text: &str) -> Option<bool> {
match text.as_bytes() {
b"true" => Some(true),
b"false" => Some(false),
_ => None,
}
}
const fn parse_hash(text: &str) -> Option<u64> {
let bytes = text.as_bytes();
if bytes.len() != 16 {
return None;
}
let mut value = 0_u64;
let mut index = 0;
while index < bytes.len() {
let digit = match bytes[index] {
byte @ b'0'..=b'9' => byte - b'0',
byte @ b'a'..=b'f' => byte - b'a' + 10,
_ => return None,
};
value = (value << 4) | digit as u64;
index += 1;
}
Some(value)
}
#[macro_export]
macro_rules! build_info {
() => {
$crate::__macro_support::BuildInfo::__from_env(
::core::env!("CARGO_PKG_NAME"),
::core::env!("CARGO_PKG_VERSION"),
::core::option_env!("HENAD_BUILD_COMMIT"),
::core::option_env!("HENAD_BUILD_COMMIT_DATE"),
::core::option_env!("HENAD_BUILD_DIRTY"),
::core::option_env!("HENAD_BUILD_SOURCE_HASH"),
::core::cfg!(debug_assertions),
)
};
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelSource {
type_path: Cow<'static, str>,
build: Option<BuildInfo>,
}
impl ModelSource {
#[doc(hidden)]
pub fn __from_type_path(type_path: &'static str) -> Self {
Self {
type_path: Cow::Borrowed(type_path),
build: None,
}
}
#[doc(hidden)]
pub fn __with_build(self, build: BuildInfo) -> Self {
Self {
build: Some(build),
..self
}
}
pub fn build(&self) -> Option<&BuildInfo> {
self.build.as_ref()
}
pub fn package(&self) -> &str {
self.build.as_ref().map_or("", BuildInfo::package)
}
pub fn version(&self) -> &str {
self.build.as_ref().map_or("", BuildInfo::version)
}
pub fn commit(&self) -> &str {
self.build.as_ref().map_or("", BuildInfo::commit)
}
pub fn dirty(&self) -> Option<bool> {
self.build.as_ref().and_then(BuildInfo::dirty)
}
pub fn source_hash(&self) -> Option<u64> {
self.build.as_ref().and_then(BuildInfo::source_hash)
}
pub fn type_path(&self) -> &str {
&self.type_path
}
}
#[cfg(test)]
mod tests {
use super::{BuildInfo, ModelSource};
#[test]
fn an_unstamped_build_reads_as_unknown() {
let build = crate::build_info!();
assert_eq!(build.package(), "henad-core");
assert_eq!(build.version(), env!("CARGO_PKG_VERSION"));
assert_eq!(build.commit(), "");
assert_eq!(build.dirty(), None);
assert_eq!(build.source_hash(), None);
assert_eq!(build.debug_build(), cfg!(debug_assertions));
}
#[test]
fn a_stamped_build_reads_its_flag_and_hash() {
let build = BuildInfo::__from_env(
"pkg",
"1.0.0",
Some("abc1234"),
Some("2026-10-01"),
Some("true"),
Some("00000000deadbeef"),
false,
);
assert_eq!(build.commit(), "abc1234");
assert_eq!(build.commit_date(), "2026-10-01");
assert_eq!(build.dirty(), Some(true));
assert_eq!(build.source_hash(), Some(0xDEAD_BEEF));
}
#[test]
fn malformed_stamps_read_as_unknown() {
let build = BuildInfo::__from_env("pkg", "1.0.0", None, None, Some("yes"), Some("DEADBEEF"), false);
assert_eq!(build.dirty(), None);
assert_eq!(build.source_hash(), None);
}
#[test]
fn a_source_reads_its_build_once_recorded() {
let source = ModelSource::__from_type_path("crate::Model");
assert_eq!((source.package(), source.type_path()), ("", "crate::Model"));
let build = BuildInfo::__from_env("pkg", "1.0.0", Some("abc1234"), None, None, None, false);
let source = source.__with_build(build);
assert_eq!(
(source.package(), source.version(), source.commit()),
("pkg", "1.0.0", "abc1234")
);
assert_eq!(source.build(), Some(&build));
}
}