1use std::borrow::Cow;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct BuildInfo {
13 package: &'static str,
14 version: &'static str,
15 commit: &'static str,
16 commit_date: &'static str,
17 dirty: Option<bool>,
18 source_hash: Option<u64>,
19 debug_build: bool,
20}
21
22impl BuildInfo {
23 #[doc(hidden)]
28 pub const fn __from_env(
29 package: &'static str,
30 version: &'static str,
31 commit: Option<&'static str>,
32 commit_date: Option<&'static str>,
33 dirty: Option<&'static str>,
34 source_hash: Option<&'static str>,
35 debug_build: bool,
36 ) -> Self {
37 Self {
38 package,
39 version,
40 commit: match commit {
41 Some(commit) => commit,
42 None => "",
43 },
44 commit_date: match commit_date {
45 Some(date) => date,
46 None => "",
47 },
48 dirty: match dirty {
49 Some(text) => parse_flag(text),
50 None => None,
51 },
52 source_hash: match source_hash {
53 Some(text) => parse_hash(text),
54 None => None,
55 },
56 debug_build,
57 }
58 }
59
60 pub fn package(&self) -> &'static str {
62 self.package
63 }
64
65 pub fn version(&self) -> &'static str {
67 self.version
68 }
69
70 pub fn commit(&self) -> &'static str {
72 self.commit
73 }
74
75 pub fn commit_date(&self) -> &'static str {
77 self.commit_date
78 }
79
80 pub fn dirty(&self) -> Option<bool> {
83 self.dirty
84 }
85
86 pub fn source_hash(&self) -> Option<u64> {
90 self.source_hash
91 }
92
93 pub fn debug_build(&self) -> bool {
95 self.debug_build
96 }
97}
98
99const fn parse_flag(text: &str) -> Option<bool> {
101 match text.as_bytes() {
102 b"true" => Some(true),
103 b"false" => Some(false),
104 _ => None,
105 }
106}
107
108const fn parse_hash(text: &str) -> Option<u64> {
110 let bytes = text.as_bytes();
111 if bytes.len() != 16 {
112 return None;
113 }
114 let mut value = 0_u64;
115 let mut index = 0;
116 while index < bytes.len() {
117 let digit = match bytes[index] {
118 byte @ b'0'..=b'9' => byte - b'0',
119 byte @ b'a'..=b'f' => byte - b'a' + 10,
120 _ => return None,
121 };
122 value = (value << 4) | digit as u64;
123 index += 1;
124 }
125 Some(value)
126}
127
128#[macro_export]
133macro_rules! build_info {
134 () => {
135 $crate::__macro_support::BuildInfo::__from_env(
136 ::core::env!("CARGO_PKG_NAME"),
137 ::core::env!("CARGO_PKG_VERSION"),
138 ::core::option_env!("HENAD_BUILD_COMMIT"),
139 ::core::option_env!("HENAD_BUILD_COMMIT_DATE"),
140 ::core::option_env!("HENAD_BUILD_DIRTY"),
141 ::core::option_env!("HENAD_BUILD_SOURCE_HASH"),
142 ::core::cfg!(debug_assertions),
143 )
144 };
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct ModelSource {
153 type_path: Cow<'static, str>,
154 build: Option<BuildInfo>,
155}
156
157impl ModelSource {
158 #[doc(hidden)]
160 pub fn __from_type_path(type_path: &'static str) -> Self {
161 Self {
162 type_path: Cow::Borrowed(type_path),
163 build: None,
164 }
165 }
166
167 #[doc(hidden)]
169 pub fn __with_build(self, build: BuildInfo) -> Self {
170 Self {
171 build: Some(build),
172 ..self
173 }
174 }
175
176 pub fn build(&self) -> Option<&BuildInfo> {
178 self.build.as_ref()
179 }
180
181 pub fn package(&self) -> &str {
183 self.build.as_ref().map_or("", BuildInfo::package)
184 }
185
186 pub fn version(&self) -> &str {
188 self.build.as_ref().map_or("", BuildInfo::version)
189 }
190
191 pub fn commit(&self) -> &str {
194 self.build.as_ref().map_or("", BuildInfo::commit)
195 }
196
197 pub fn dirty(&self) -> Option<bool> {
200 self.build.as_ref().and_then(BuildInfo::dirty)
201 }
202
203 pub fn source_hash(&self) -> Option<u64> {
205 self.build.as_ref().and_then(BuildInfo::source_hash)
206 }
207
208 pub fn type_path(&self) -> &str {
212 &self.type_path
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::{BuildInfo, ModelSource};
219
220 #[test]
221 fn an_unstamped_build_reads_as_unknown() {
222 let build = crate::build_info!();
223 assert_eq!(build.package(), "henad-core");
224 assert_eq!(build.version(), env!("CARGO_PKG_VERSION"));
225 assert_eq!(build.commit(), "");
226 assert_eq!(build.dirty(), None);
227 assert_eq!(build.source_hash(), None);
228 assert_eq!(build.debug_build(), cfg!(debug_assertions));
229 }
230
231 #[test]
232 fn a_stamped_build_reads_its_flag_and_hash() {
233 let build = BuildInfo::__from_env(
234 "pkg",
235 "1.0.0",
236 Some("abc1234"),
237 Some("2026-10-01"),
238 Some("true"),
239 Some("00000000deadbeef"),
240 false,
241 );
242 assert_eq!(build.commit(), "abc1234");
243 assert_eq!(build.commit_date(), "2026-10-01");
244 assert_eq!(build.dirty(), Some(true));
245 assert_eq!(build.source_hash(), Some(0xDEAD_BEEF));
246 }
247
248 #[test]
249 fn malformed_stamps_read_as_unknown() {
250 let build = BuildInfo::__from_env("pkg", "1.0.0", None, None, Some("yes"), Some("DEADBEEF"), false);
251 assert_eq!(build.dirty(), None);
252 assert_eq!(build.source_hash(), None);
253 }
254
255 #[test]
256 fn a_source_reads_its_build_once_recorded() {
257 let source = ModelSource::__from_type_path("crate::Model");
258 assert_eq!((source.package(), source.type_path()), ("", "crate::Model"));
259 let build = BuildInfo::__from_env("pkg", "1.0.0", Some("abc1234"), None, None, None, false);
260 let source = source.__with_build(build);
261 assert_eq!(
262 (source.package(), source.version(), source.commit()),
263 ("pkg", "1.0.0", "abc1234")
264 );
265 assert_eq!(source.build(), Some(&build));
266 }
267}