1use anyhow::{Context, Result};
4use serde::Serialize;
5use std::io::{self, Write};
6
7const CARGO_TREE: &str = include_str!(concat!(env!("OUT_DIR"), "/cargo-tree.txt"));
9
10#[derive(Debug, Default, Eq, PartialEq, Serialize)]
12pub(crate) struct Version {
13 #[serde(skip)]
15 verbose: bool,
16
17 version: &'static str,
19
20 tag: &'static str,
22
23 commit: &'static str,
25
26 build_date: &'static str,
28
29 target: &'static str,
31
32 rust_version: &'static str,
34
35 cargo_version: &'static str,
37
38 cargo_tree: &'static str,
40
41 features: &'static [&'static str],
44}
45
46impl Version {
47 pub(crate) fn new(verbose: bool) -> Self {
49 Self {
50 verbose,
51 version: env!("CARGO_PKG_VERSION"),
52 tag: env!("BUILD_TAG"),
53 commit: env!("BUILD_COMMIT"),
54 build_date: env!("BUILD_TIME"),
55 target: env!("BUILD_TARGET"),
56 rust_version: env!("BUILD_RUST_VERSION"),
57 cargo_version: env!("BUILD_CARGO_VERSION"),
58 cargo_tree: if verbose { CARGO_TREE } else { "" },
59 features: Self::FEATURES,
60 }
61 }
62
63 const FEATURE_PORT_FORWARD: &'static str = "portforward";
65
66 const FEATURE_IDLE_TIMEOUT: &'static str = "idletimeout";
69
70 const FEATURE_STREAMING_PROTOCOLS: &'static str = "streamingprotocols";
73
74 const FEATURE_RESTORE: &'static str = "restore";
78
79 const FEATURES: &'static [&'static str] = &[
82 Self::FEATURE_PORT_FORWARD,
83 Self::FEATURE_IDLE_TIMEOUT,
84 Self::FEATURE_STREAMING_PROTOCOLS,
85 Self::FEATURE_RESTORE,
86 ];
87
88 pub(crate) fn features(&self) -> &'static [&'static str] {
90 self.features
91 }
92
93 pub(crate) fn version(&self) -> &'static str {
95 self.version
96 }
97
98 pub(crate) fn tag(&self) -> &'static str {
100 self.tag
101 }
102
103 pub(crate) fn commit(&self) -> &'static str {
105 self.commit
106 }
107
108 pub(crate) fn build_date(&self) -> &'static str {
110 self.build_date
111 }
112
113 pub(crate) fn target(&self) -> &'static str {
115 self.target
116 }
117
118 pub(crate) fn rust_version(&self) -> &'static str {
120 self.rust_version
121 }
122
123 pub(crate) fn cargo_version(&self) -> &'static str {
125 self.cargo_version
126 }
127
128 pub(crate) fn cargo_tree(&self) -> &'static str {
130 self.cargo_tree
131 }
132
133 fn text(&self) -> String {
138 let tag = if self.tag().is_empty() {
139 "none"
140 } else {
141 self.tag()
142 };
143 let mut text = format!(
144 "version: {}\ntag: {tag}\ncommit: {}\nbuild: {}\ntarget: {}\n{}\n{}\nfeatures: {}\n",
145 self.version(),
146 self.commit(),
147 self.build_date(),
148 self.target(),
149 self.rust_version(),
150 self.cargo_version(),
151 self.features().join(", "),
152 );
153 if self.verbose {
154 text.push_str(&format!("\ncargo tree: {}\n", self.cargo_tree()));
155 }
156 text
157 }
158
159 pub(crate) fn print(&self) -> Result<()> {
161 write_output(io::stdout().lock(), &self.text())
162 }
163
164 pub(crate) fn print_json(&self) -> Result<()> {
166 let mut json = serde_json::to_string(&self).context("serialize result")?;
167 json.push('\n');
168 write_output(io::stdout().lock(), &json)
169 }
170}
171
172fn write_output(mut writer: impl Write, text: &str) -> Result<()> {
175 match writer
176 .write_all(text.as_bytes())
177 .and_then(|()| writer.flush())
178 {
179 Err(e) if e.kind() != io::ErrorKind::BrokenPipe => Err(e).context("write output"),
180 _ => Ok(()),
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn version_fields_and_verbosity() {
190 let v = Version::new(false);
191 assert_eq!(v.version(), env!("CARGO_PKG_VERSION"));
192 assert!(!v.target().is_empty());
193 assert!(v.rust_version().starts_with("rustc "));
194 assert!(v.cargo_version().starts_with("cargo "));
195 assert!(v.cargo_tree().is_empty());
197 assert_eq!(Version::new(true).cargo_tree(), CARGO_TREE);
198
199 let text = v.text();
200 assert!(text.starts_with(&format!("version: {}\n", v.version())));
201 assert!(text.ends_with(&format!("features: {}\n", v.features().join(", "))));
202 assert!(Version::new(true).text().contains("\ncargo tree: "));
203 }
204
205 struct Failing(io::ErrorKind);
207
208 impl Write for Failing {
209 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
210 Err(self.0.into())
211 }
212
213 fn flush(&mut self) -> io::Result<()> {
214 Ok(())
215 }
216 }
217
218 #[test]
219 fn output_to_a_closed_pipe_is_no_error() {
220 assert!(write_output(Failing(io::ErrorKind::BrokenPipe), "version").is_ok());
221 assert!(write_output(Failing(io::ErrorKind::PermissionDenied), "version").is_err());
222 }
223
224 #[test]
226 fn version_json() -> Result<()> {
227 let v = Version::new(false);
228 let json = serde_json::to_value(&v)?;
229 let object = json.as_object().context("not an object")?;
230 for (key, value) in [
231 ("build_date", v.build_date()),
232 ("cargo_tree", v.cargo_tree()),
233 ("cargo_version", v.cargo_version()),
234 ("commit", v.commit()),
235 ("rust_version", v.rust_version()),
236 ("tag", v.tag()),
237 ("target", v.target()),
238 ("version", v.version()),
239 ] {
240 assert_eq!(
241 object.get(key).and_then(|v| v.as_str()),
242 Some(value),
243 "key {key}"
244 );
245 }
246 assert!(!object.contains_key("verbose"));
248 Ok(())
249 }
250
251 #[test]
254 fn version_features() -> Result<()> {
255 let v = Version::new(false);
256 assert_eq!(
257 v.features(),
258 [
259 "portforward",
260 "idletimeout",
261 "streamingprotocols",
262 "restore"
263 ]
264 );
265 let json = serde_json::to_value(&v)?;
266 assert_eq!(json.get("features"), Some(&serde_json::json!(v.features())));
267 Ok(())
268 }
269}