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}