rtb_app/version.rs
1//! Build-time version information.
2
3use semver::Version;
4use serde::{Deserialize, Serialize};
5
6/// Capture the **calling crate's** version into a [`VersionInfo`].
7///
8/// Expands to [`VersionInfo::from_pkg_version`] applied to
9/// `env!("CARGO_PKG_VERSION")`. Because `macro_rules!` expands at the
10/// call site, the `env!` is evaluated while *your* crate is compiled and
11/// therefore yields *your* version.
12///
13/// This has to be a macro. A plain function cannot do it: the `env!`
14/// inside a function body is expanded when the *defining* crate is
15/// compiled, so it would always report rtb-app's version. That is
16/// exactly the bug [`VersionInfo::from_env`] has, and why it is
17/// deprecated.
18///
19/// ```
20/// let v = rtb_app::version_info!();
21/// assert_eq!(v.version.to_string(), env!("CARGO_PKG_VERSION"));
22/// ```
23///
24/// Chain the fluent setters to add build metadata:
25///
26/// ```
27/// let v = rtb_app::version_info!()
28/// .with_commit("deadbeef")
29/// .with_date("2026-08-02T00:00:00Z");
30/// assert_eq!(v.commit.as_deref(), Some("deadbeef"));
31/// ```
32#[macro_export]
33macro_rules! version_info {
34 () => {
35 $crate::version::VersionInfo::from_pkg_version(::core::env!("CARGO_PKG_VERSION"))
36 };
37}
38
39/// Version information captured at build time.
40///
41/// Populate the `version` field with the [`version_info!`] macro, and
42/// inject `commit` / `date` via your `build.rs` (the `vergen` or `built`
43/// crates are canonical).
44///
45/// [`version_info!`]: crate::version_info
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct VersionInfo {
48 /// Parsed semantic version.
49 pub version: Version,
50
51 /// Short commit SHA, if known at build time.
52 #[serde(default)]
53 pub commit: Option<String>,
54
55 /// ISO-8601 build timestamp, if known at build time.
56 #[serde(default)]
57 pub date: Option<String>,
58}
59
60impl VersionInfo {
61 /// Construct from a parsed semver. `commit` and `date` start unset
62 /// — add them with [`Self::with_commit`] / [`Self::with_date`].
63 #[must_use]
64 pub const fn new(version: Version) -> Self {
65 Self { version, commit: None, date: None }
66 }
67
68 /// Fluent setter for the commit SHA.
69 #[must_use]
70 pub fn with_commit(mut self, commit: impl Into<String>) -> Self {
71 self.commit = Some(commit.into());
72 self
73 }
74
75 /// Fluent setter for the build timestamp.
76 #[must_use]
77 pub fn with_date(mut self, date: impl Into<String>) -> Self {
78 self.date = Some(date.into());
79 self
80 }
81
82 /// Parse a `CARGO_PKG_VERSION`-shaped string, with a silent fallback
83 /// to `0.0.0` when parsing fails (which in turn is flagged by
84 /// [`Self::is_development`]).
85 ///
86 /// Prefer the [`version_info!`] macro, which passes the *calling*
87 /// crate's version for you. Call this directly only when the version
88 /// string comes from somewhere else — a `build.rs` output, say.
89 ///
90 /// [`version_info!`]: crate::version_info
91 #[must_use]
92 pub fn from_pkg_version(raw: &str) -> Self {
93 let version = Version::parse(raw).unwrap_or_else(|_| Version::new(0, 0, 0));
94 Self::new(version)
95 }
96
97 /// Parse *rtb-app's own* `CARGO_PKG_VERSION`.
98 ///
99 /// # This is almost certainly not what you want
100 ///
101 /// `env!` is expanded by the compiler wherever it is written — and it
102 /// is written *here*, inside rtb-app. So this returns the version of
103 /// the framework, never the version of the tool calling it. Every
104 /// downstream tool using it reported rtb-app's version as its own.
105 ///
106 /// That is not merely cosmetic: [`crate::metadata`]-driven self-update
107 /// verifies a freshly staged binary by checking the version it
108 /// reports against the release tag it came from. A binary that
109 /// misreports its version fails that check, and the update is
110 /// refused.
111 ///
112 /// Use the [`version_info!`] macro instead — it expands `env!` at
113 /// *your* call site.
114 ///
115 /// [`version_info!`]: crate::version_info
116 #[must_use]
117 #[deprecated(
118 since = "0.9.0",
119 note = "returns rtb-app's version, not the calling tool's — `env!` expands where it is written. Use the `rtb_app::version_info!()` macro instead."
120 )]
121 pub fn from_env() -> Self {
122 Self::from_pkg_version(env!("CARGO_PKG_VERSION"))
123 }
124
125 /// `true` when this build is a development / pre-release build.
126 ///
127 /// Development is any of:
128 ///
129 /// * `major == 0` (pre-1.0 builds are always considered development),
130 /// * a non-empty pre-release identifier (`-alpha`, `-dev.5`, …),
131 /// * version exactly `0.0.0` (the [`Self::from_pkg_version`] fallback).
132 #[must_use]
133 pub fn is_development(&self) -> bool {
134 self.version.major == 0 || !self.version.pre.is_empty()
135 }
136}