Skip to main content

henad_core/
provenance.rs

1//! Identity of compiled crates and the source of each model.
2//!
3//! A [`BuildInfo`] identifies one compiled crate: its package, its version and, once a build script stamps it, its
4//! commit, a dirty flag and a hash of its sources. [`build_info!`](crate::build_info) returns the build of the crate it
5//! expands in. A [`ModelSource`] identifies the origin of a model's code: the type registered, and the build of the
6//! crate that registered it.
7
8use std::borrow::Cow;
9
10/// Identity of one compiled crate.
11#[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    /// Returns the build described by the environment variables that [`build_info!`](crate::build_info) reads.
24    ///
25    /// `dirty` accepts `true` or `false`, and `source_hash` 16 lowercase hexadecimal digits. Any other text is treated
26    /// as unknown.
27    #[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    /// Cargo package name.
61    pub fn package(&self) -> &'static str {
62        self.package
63    }
64
65    /// Package version.
66    pub fn version(&self) -> &'static str {
67        self.version
68    }
69
70    /// Short commit hash, empty when the build could not learn it.
71    pub fn commit(&self) -> &'static str {
72        self.commit
73    }
74
75    /// Commit date, empty in a registry or vendored build.
76    pub fn commit_date(&self) -> &'static str {
77        self.commit_date
78    }
79
80    /// Whether the crate's sources, manifest or lockfile differed from its commit. `None` when the build could not
81    /// tell.
82    pub fn dirty(&self) -> Option<bool> {
83        self.dirty
84    }
85
86    /// Hash of the crate's files under `src` and its manifest. A crate stamped with its commit, Henad's engine
87    /// included, adds the nearest `Cargo.lock` outside a package. In Henad's checkout the engine's hash covers its four
88    /// crates. `None` when the build did not compute a hash.
89    pub fn source_hash(&self) -> Option<u64> {
90        self.source_hash
91    }
92
93    /// Whether the crate was compiled with debug assertions.
94    pub fn debug_build(&self) -> bool {
95        self.debug_build
96    }
97}
98
99/// Returns `true` for "true", `false` for "false", and `None` for any other text.
100const 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
108/// Returns the value of 16 lowercase hexadecimal digits, or `None` for any other text.
109const 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/// Returns the [`BuildInfo`] of the crate the macro expands in.
129///
130/// The package and version come from Cargo. The commit, its date, the dirty flag and the source hash come from
131/// variables that a build script sets, and are unknown where no script sets them.
132#[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/// Origin of a model's code.
148///
149/// Registration records the model's type path. The registering crate's build is recorded once the entry joins a
150/// model set, and the accessors that read it return empty text or `None` until then.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct ModelSource {
153    type_path: Cow<'static, str>,
154    build: Option<BuildInfo>,
155}
156
157impl ModelSource {
158    /// Returns a source that records `type_path` and no build yet.
159    #[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    /// Returns the source with `build` recorded as the registering crate's build.
168    #[doc(hidden)]
169    pub fn __with_build(self, build: BuildInfo) -> Self {
170        Self {
171            build: Some(build),
172            ..self
173        }
174    }
175
176    /// Build of the registering crate, `None` until a set records it.
177    pub fn build(&self) -> Option<&BuildInfo> {
178        self.build.as_ref()
179    }
180
181    /// Package of the registering crate, empty until a set records its build.
182    pub fn package(&self) -> &str {
183        self.build.as_ref().map_or("", BuildInfo::package)
184    }
185
186    /// Version of the registering crate, empty until a set records its build.
187    pub fn version(&self) -> &str {
188        self.build.as_ref().map_or("", BuildInfo::version)
189    }
190
191    /// Short commit hash of the registering crate, as [`BuildInfo::commit`] returns it, and empty until a set
192    /// records its build.
193    pub fn commit(&self) -> &str {
194        self.build.as_ref().map_or("", BuildInfo::commit)
195    }
196
197    /// Whether the registering crate's sources, manifest or lockfile differed from its commit, as
198    /// [`BuildInfo::dirty`] returns it.
199    pub fn dirty(&self) -> Option<bool> {
200        self.build.as_ref().and_then(BuildInfo::dirty)
201    }
202
203    /// Hash of the registering crate's files, as [`BuildInfo::source_hash`] returns it.
204    pub fn source_hash(&self) -> Option<u64> {
205        self.build.as_ref().and_then(BuildInfo::source_hash)
206    }
207
208    /// Type path of the registered model, from `std::any::type_name`.
209    ///
210    /// Note that the format of a type path is not stable across compiler releases, so no check compares it.
211    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}