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