saluki_metadata/
lib.rs

1use std::sync::OnceLock;
2
3use serde::{Serialize, Serializer};
4
5#[allow(dead_code)]
6mod details {
7    include!(concat!(env!("OUT_DIR"), "/details.rs"));
8}
9
10static APP_DETAILS: OnceLock<AppDetails> = OnceLock::new();
11
12/// Details reported before an application has registered its own.
13static UNREGISTERED_APP_DETAILS: AppDetails =
14    AppDetails::new("unknown", "unknown", "unknown", Version::new("0.0.0", 0, 0, 0));
15
16/// Gets the details for this application.
17///
18/// This includes basic information like the application name and semantic version, and information that might otherwise
19/// fall under the general umbrella of "build metadata."
20///
21/// If the application hasn't registered its details through [`set_app_details`], the name and version fields report
22/// `"unknown"` and `0.0.0` respectively. Build metadata is always populated, since it's captured at compile time.
23pub fn get_app_details() -> &'static AppDetails {
24    APP_DETAILS.get().unwrap_or(&UNREGISTERED_APP_DETAILS)
25}
26
27/// Registers the details for this application.
28///
29/// Binaries declare their details with [`declare_app_details!`] and register them here, which should be the first
30/// thing `main` does: anything read through [`get_app_details`] beforehand reports an unknown application.
31///
32/// # Panics
33///
34/// Panics if the details have already been registered.
35pub fn set_app_details(details: AppDetails) {
36    assert!(
37        APP_DETAILS.set(details).is_ok(),
38        "application details have already been registered"
39    );
40}
41
42/// Declares the details for the application being compiled.
43///
44/// The caller supplies the three names that identify the application. Everything else is filled in automatically: the
45/// version comes from the calling crate's Cargo manifest, and the build metadata from `saluki-metadata`'s own build
46/// script.
47///
48/// # Examples
49///
50/// <!-- vale off -->
51/// ```
52/// # use saluki_metadata::{declare_app_details, AppDetails};
53/// pub const APP_DETAILS: AppDetails = declare_app_details!(
54///     full_name = "Example Application",
55///     short_name = "example",
56///     identifier = "ex",
57/// );
58/// ```
59/// <!-- vale on -->
60#[macro_export]
61macro_rules! declare_app_details {
62    (full_name = $full_name:expr, short_name = $short_name:expr, identifier = $identifier:expr $(,)?) => {
63        $crate::AppDetails::new(
64            $full_name,
65            $short_name,
66            $identifier,
67            $crate::Version::new(
68                env!("CARGO_PKG_VERSION"),
69                $crate::const_parse_u32(env!("CARGO_PKG_VERSION_MAJOR")),
70                $crate::const_parse_u32(env!("CARGO_PKG_VERSION_MINOR")),
71                $crate::const_parse_u32(env!("CARGO_PKG_VERSION_PATCH")),
72            ),
73        )
74    };
75}
76
77/// Parses a `u32` from a string in a `const` context.
78///
79/// Only intended for the version components that Cargo hands us, which are always plain decimal numbers. Anything else
80/// fails the build.
81#[doc(hidden)]
82pub const fn const_parse_u32(s: &str) -> u32 {
83    match u32::from_str_radix(s, 10) {
84        Ok(value) => value,
85        Err(_) => panic!("version component was not a number"),
86    }
87}
88
89/// Application details.
90///
91/// # Configuration
92///
93/// The name and version fields identify a specific application, so they're declared by the binary itself through
94/// [`declare_app_details!`] and registered with [`set_app_details`]. The version comes from that binary's Cargo
95/// manifest.
96///
97/// The remaining fields describe the build rather than the application, so they're captured at compile time from the
98/// following environment variables:
99///
100/// - `APP_GIT_HASH`: Git hash of the application. If this isn't set, the default value is `"unknown"`.
101/// - `APP_BUILD_TIME`: Build time of the application. If this isn't set, the default value is `"0000-00-00 00:00:00"`.
102/// - `APP_DEV_BUILD`: Whether the application is a development build. If this isn't set, the default value is `true`.
103/// - `TARGET`: Target architecture of the application. If this isn't set, the default value is `"unknown-arch"`.
104///
105/// Environment variables prefixed with `APP_` are expected to be set by the build script/tooling, while others are
106/// provided automatically by Cargo.
107#[derive(Serialize)]
108pub struct AppDetails {
109    full_name: &'static str,
110    short_name: &'static str,
111    identifier: &'static str,
112    git_hash: &'static str,
113    version: Version,
114    build_time: &'static str,
115    dev_build: bool,
116    target_arch: &'static str,
117}
118
119impl AppDetails {
120    /// Creates a new `AppDetails` from the given application identity.
121    ///
122    /// Build metadata is filled in from the values captured at compile time, so callers can't get it wrong. Prefer
123    /// [`declare_app_details!`], which also derives the version from the calling crate's Cargo manifest.
124    pub const fn new(
125        full_name: &'static str, short_name: &'static str, identifier: &'static str, version: Version,
126    ) -> Self {
127        Self {
128            full_name,
129            short_name,
130            identifier,
131            version,
132            git_hash: details::DETECTED_GIT_HASH,
133            build_time: details::DETECTED_APP_BUILD_TIME,
134            dev_build: details::DETECTED_APP_DEV_BUILD,
135            target_arch: details::DETECTED_TARGET_ARCH,
136        }
137    }
138
139    /// Returns the application's full name.
140    ///
141    /// This is typically a human-friendly/"pretty" name of the binary/executable, such as `"Agent Data Plane"`.
142    ///
143    /// If the application hasn't registered its details, this will return `"unknown"`.
144    pub fn full_name(&self) -> &'static str {
145        self.full_name
146    }
147
148    /// Returns the application's short name.
149    ///
150    /// This is typically a shorter version of the name of the binary/executable, such as `"Data Plane"` or `"DATAPLANE"`.
151    ///
152    /// If the application hasn't registered its details, this will return `"unknown"`.
153    pub fn short_name(&self) -> &'static str {
154        self.short_name
155    }
156
157    /// Returns the application's identifier.
158    ///
159    /// This is typically a very condensed form of the name of the binary/executable, like an acronym, such as `"adp"`
160    /// or `"ADP"`.
161    ///
162    /// If the application hasn't registered its details, this will return `"unknown"`.
163    pub fn identifier(&self) -> &'static str {
164        self.identifier
165    }
166
167    /// Returns the Git hash used to build the application.
168    ///
169    /// If the Git hash couldn't be detected, this will return `"unknown"`.
170    pub fn git_hash(&self) -> &'static str {
171        self.git_hash
172    }
173
174    /// Returns the application's version.
175    ///
176    /// If the application hasn't registered its details, this will return a version equivalent to `"0.0.0"`.
177    pub fn version(&self) -> &Version {
178        &self.version
179    }
180
181    /// Returns the build time of the application.
182    ///
183    /// If the build time couldn't be detected, this will return `"0000-00-00 00:00:00"`.
184    pub fn build_time(&self) -> &'static str {
185        self.build_time
186    }
187
188    /// Returns `true` if this application is a development build.
189    ///
190    /// Development builds generally encompass all local builds, and any CI builds which aren't related to versioned
191    /// artifacts intended for public release.
192    ///
193    /// If the development build flag couldn't be detected, this will return `true`.
194    pub fn is_dev_build(&self) -> bool {
195        self.dev_build
196    }
197
198    /// Returns the target architecture of the application.
199    ///
200    /// This returns a _target triple_, which is a string that generally has _four_ components: the processor
201    /// architecture (x86-64, ARM64, etc), vendor (`"apple"`, `"pc"`, etc), operating system (`"linux"`, `"windows"`,
202    /// `"darwin"`, etc) and environment/ABI (`"gnu"`, `"musl"`, etc).
203    ///
204    /// The environment/ABI component can sometimes be omitted in scenarios where there are no meaningful distinctions
205    /// for the given operating system.
206    ///
207    /// If the target architecture couldn't be detected, this will return `"unknown-arch"`.
208    pub fn target_arch(&self) -> &'static str {
209        self.target_arch
210    }
211}
212
213/// A simple representation of a semantic version.
214pub struct Version {
215    raw: &'static str,
216    major: u32,
217    minor: u32,
218    patch: u32,
219}
220
221impl Version {
222    /// Creates a new `Version` from the given raw string and its component numbers.
223    pub const fn new(raw: &'static str, major: u32, minor: u32, patch: u32) -> Self {
224        Self {
225            raw,
226            major,
227            minor,
228            patch,
229        }
230    }
231
232    /// Returns the raw version string.
233    pub fn raw(&self) -> &'static str {
234        self.raw
235    }
236
237    /// Returns the major version number.
238    ///
239    /// If the major version number isn't present in the version string, this will return `0`.
240    pub fn major(&self) -> u32 {
241        self.major
242    }
243
244    /// Returns the minor version number.
245    ///
246    /// If the minor version number isn't present in the version string, this will return `0`.
247    pub fn minor(&self) -> u32 {
248        self.minor
249    }
250
251    /// Returns the patch version number.
252    ///
253    /// If the patch version number isn't present in the version string, this will return `0`.
254    pub fn patch(&self) -> u32 {
255        self.patch
256    }
257}
258
259impl Serialize for Version {
260    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
261    where
262        S: Serializer,
263    {
264        // Redirect serialization entirely to the 'raw' string slice
265        serializer.serialize_str(self.raw)
266    }
267}