1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4pub type Version = String;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub struct BuildInfo {
8 pub version: Version,
9 pub git_hash: String,
10 pub build_profile: String,
11 pub build_features: String,
12 pub build_timestamp: String,
13 pub rust_version: String,
14 pub target: String,
15 pub host: String,
16}
17
18impl BuildInfo {
19 pub fn new() -> Self {
20 Self {
21 version: env!("CARGO_PKG_VERSION").to_string(),
22 git_hash: option_env!("REPO_VERSION").unwrap_or("unknown").to_string(),
23 build_profile: option_env!("BUILD_PROFILE")
24 .unwrap_or("unknown")
25 .to_string(),
26 build_features: option_env!("BUILD_FEATURES").unwrap_or("none").to_string(),
27 build_timestamp: option_env!("BUILD_TIMESTAMP")
28 .unwrap_or("unknown")
29 .to_string(),
30 rust_version: option_env!("RUST_VERSION").unwrap_or("unknown").to_string(),
31 target: option_env!("BUILD_TARGET").unwrap_or("unknown").to_string(),
32 host: option_env!("BUILD_HOST").unwrap_or("unknown").to_string(),
33 }
34 }
35
36 pub fn is_debug(&self) -> bool {
37 self.build_profile == "debug"
38 }
39
40 pub fn is_release(&self) -> bool {
41 self.build_profile == "release"
42 }
43
44 pub fn has_feature(&self, feature: &str) -> bool {
45 self.build_features.split(',').any(|f| f.trim() == feature)
46 }
47
48 pub fn features(&self) -> Vec<&str> {
49 if self.build_features == "none" {
50 Vec::new()
51 } else {
52 self.build_features.split(',').map(|f| f.trim()).collect()
53 }
54 }
55
56 pub fn short_hash(&self) -> &str {
57 if self.git_hash.len() > 7 {
58 &self.git_hash[..7]
59 } else {
60 &self.git_hash
61 }
62 }
63
64 pub fn is_dirty(&self) -> bool {
65 self.git_hash.contains("-dirty")
66 }
67}
68
69impl Default for BuildInfo {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl fmt::Display for BuildInfo {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 write!(
78 f,
79 "{} ({}) built with {} on {} for {}",
80 self.version,
81 self.short_hash(),
82 self.rust_version,
83 self.build_timestamp,
84 self.target
85 )
86 }
87}
88
89#[macro_export]
90macro_rules! build_info {
91 () => {
92 $crate::version::BuildInfo::new()
93 };
94}
95
96#[macro_export]
97macro_rules! version_string {
98 () => {
99 format!(
100 "{} ({})",
101 env!("CARGO_PKG_VERSION"),
102 option_env!("REPO_VERSION").unwrap_or("unknown")
103 )
104 };
105}
106
107pub fn version() -> String {
108 version_string!()
109}
110
111pub fn build_info() -> BuildInfo {
112 build_info!()
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_build_info_creation() {
121 let info = BuildInfo::new();
122 assert!(!info.version.is_empty());
123 assert!(!info.git_hash.is_empty());
124 assert!(!info.build_profile.is_empty());
125 }
126
127 #[test]
128 fn test_version_string() {
129 let version = version();
130 assert!(version.contains(env!("CARGO_PKG_VERSION")));
131 }
132
133 #[test]
134 fn test_has_feature() {
135 let mut info = BuildInfo::new();
136 info.build_features = "serde,async,tokio".to_string();
137
138 assert!(info.has_feature("serde"));
139 assert!(info.has_feature("async"));
140 assert!(info.has_feature("tokio"));
141 assert!(!info.has_feature("nonexistent"));
142 }
143
144 #[test]
145 fn test_features_list() {
146 let mut info = BuildInfo::new();
147 info.build_features = "serde,async,tokio".to_string();
148
149 let features = info.features();
150 assert_eq!(features.len(), 3);
151 assert!(features.contains(&"serde"));
152 assert!(features.contains(&"async"));
153 assert!(features.contains(&"tokio"));
154 }
155
156 #[test]
157 fn test_no_features() {
158 let mut info = BuildInfo::new();
159 info.build_features = "none".to_string();
160
161 let features = info.features();
162 assert!(features.is_empty());
163 assert!(!info.has_feature("serde"));
164 }
165
166 #[test]
167 fn test_short_hash() {
168 let mut info = BuildInfo::new();
169 info.git_hash = "abcdef123456789".to_string();
170
171 assert_eq!(info.short_hash(), "abcdef1");
172 }
173
174 #[test]
175 fn test_is_dirty() {
176 let mut info = BuildInfo::new();
177 info.git_hash = "abcdef123456-dirty".to_string();
178
179 assert!(info.is_dirty());
180
181 info.git_hash = "abcdef123456".to_string();
182 assert!(!info.is_dirty());
183 }
184
185 #[test]
186 fn test_build_profile_checks() {
187 let mut info = BuildInfo::new();
188
189 info.build_profile = "debug".to_string();
190 assert!(info.is_debug());
191 assert!(!info.is_release());
192
193 info.build_profile = "release".to_string();
194 assert!(!info.is_debug());
195 assert!(info.is_release());
196 }
197}