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}