vdev/
app.rs

1use std::{
2    borrow::Cow,
3    env,
4    ffi::{OsStr, OsString},
5    fmt::Write as _,
6    path::PathBuf,
7    process::{Command, ExitStatus},
8    sync::{LazyLock, OnceLock},
9    time::Duration,
10};
11
12use anyhow::{Context as _, Result, bail};
13use indicatif::{ProgressBar, ProgressStyle};
14use log::LevelFilter;
15
16use crate::utils::{self, platform};
17
18// Use the `bash` interpreter included as part of the standard `git` install for our default shell
19// if nothing is specified in the environment.
20#[cfg(windows)]
21const DEFAULT_SHELL: &str = "C:\\Program Files\\Git\\bin\\bash.EXE";
22
23// This default is not currently used on non-Windows, so this is just a placeholder for now.
24#[cfg(not(windows))]
25const DEFAULT_SHELL: &str = "/bin/sh";
26
27// Extract the shell from the environment variable `$SHELL` and substitute the above default value
28// if it isn't set.
29pub static SHELL: LazyLock<OsString> =
30    LazyLock::new(|| env::var_os("SHELL").unwrap_or_else(|| DEFAULT_SHELL.into()));
31
32static VERBOSITY: OnceLock<LevelFilter> = OnceLock::new();
33static PATH: OnceLock<String> = OnceLock::new();
34
35pub fn verbosity() -> &'static LevelFilter {
36    VERBOSITY.get().expect("verbosity is not initialized")
37}
38
39pub fn path() -> &'static String {
40    PATH.get().expect("path is not initialized")
41}
42
43pub fn set_repo_dir() -> Result<()> {
44    env::set_current_dir(path()).context("Could not change directory")
45}
46
47pub fn version() -> Result<String> {
48    let mut version = utils::cargo::get_version()?;
49
50    let channel = utils::git::get_channel();
51
52    if channel == "release" {
53        let head = utils::git::git_head()?;
54        if !head.status.success() {
55            let error = String::from_utf8_lossy(&head.stderr);
56            bail!("Error running `git describe`:\n{error}");
57        }
58        let tag = String::from_utf8_lossy(&head.stdout).trim().to_string();
59        if tag != format!("v{version}") {
60            bail!(
61                "On latest release channel and tag {tag:?} is different from Cargo.toml {version:?}. Aborting"
62            );
63        }
64
65    // extend version for custom builds if not already
66    } else if channel == "custom" && !version.contains("custom") {
67        let sha = utils::git::get_git_sha()?;
68
69        // use '.' instead of '-' or '_' to avoid issues with rpm and deb package naming
70        // format requirements.
71        version = format!("{version}.custom.{sha}");
72    }
73
74    Ok(version)
75}
76
77/// Overlay some extra helper functions onto `std::process::Command`
78pub trait CommandExt {
79    fn script(script: &str) -> Self;
80    fn in_repo(&mut self) -> &mut Self;
81    fn check_output(&mut self) -> Result<String>;
82    fn check_run(&mut self) -> Result<()>;
83    fn run(&mut self) -> Result<ExitStatus>;
84    fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()>;
85    fn pre_exec(&self);
86    fn features(&mut self, features: &[String]) -> &mut Self;
87}
88
89impl CommandExt for Command {
90    /// Create a new command to execute the named script in the repository `scripts` directory.
91    fn script(script: &str) -> Self {
92        let path: PathBuf = [path(), "scripts", script].into_iter().collect();
93        if cfg!(windows) {
94            // On Windows, all scripts must be run through an explicit interpreter.
95            let mut command = Command::new(&*SHELL);
96            command.arg(path);
97            command
98        } else {
99            // On all other systems, we can run scripts directly.
100            Command::new(path)
101        }
102    }
103
104    /// Set the command's working directory to the repository directory.
105    fn in_repo(&mut self) -> &mut Self {
106        self.current_dir(path())
107    }
108
109    /// Run the command and capture its output.
110    fn check_output(&mut self) -> Result<String> {
111        self.pre_exec();
112
113        let output = self.output()?;
114
115        if output.status.success() {
116            // If the command exits successfully, return stdout as a string
117            Ok(String::from_utf8(output.stdout)?)
118        } else {
119            bail!(
120                "{}",
121                format_command_error(&output, Some(&format!("Command: {self:?}")))
122            )
123        }
124    }
125
126    /// Run the command and catch its exit code.
127    fn run(&mut self) -> Result<ExitStatus> {
128        self.pre_exec();
129        self.status().map_err(Into::into)
130    }
131
132    fn check_run(&mut self) -> Result<()> {
133        let status = self.run()?;
134        if status.success() {
135            Ok(())
136        } else {
137            let exit = status.code().unwrap();
138            bail!("command: {self:?}\n  failed with exit code: {exit}")
139        }
140    }
141
142    /// Run the command, capture its output, and display a progress bar while it's
143    /// executing. Intended to be used for long-running processes with little interaction.
144    fn wait(&mut self, message: impl Into<Cow<'static, str>>) -> Result<()> {
145        self.pre_exec();
146
147        let progress_bar = get_progress_bar()?;
148        progress_bar.set_message(message);
149
150        let result = self.output();
151        progress_bar.finish_and_clear();
152
153        let Ok(output) = result else {
154            bail!("could not run command")
155        };
156
157        if output.status.success() {
158            Ok(())
159        } else {
160            bail!("{}", format_command_error(&output, None))
161        }
162    }
163
164    /// Print out a pre-execution debug message.
165    fn pre_exec(&self) {
166        debug!("Executing: {self:?}");
167        if let Some(cwd) = self.get_current_dir() {
168            debug!("  in working directory {cwd:?}");
169        }
170        for (key, value) in self.get_envs() {
171            let key = key.to_string_lossy();
172            if let Some(value) = value {
173                debug!("  ${key}={:?}", value.to_string_lossy());
174            } else {
175                debug!("  unset ${key}");
176            }
177        }
178    }
179
180    fn features(&mut self, features: &[String]) -> &mut Self {
181        self.arg("--no-default-features");
182        self.arg("--features");
183        if features.is_empty() {
184            self.arg(platform::default_features());
185        } else {
186            self.arg(features.join(","));
187        }
188        self
189    }
190}
191
192/// Helper function to build an error message from command output
193fn format_command_error(
194    output: &std::process::Output,
195    command_description: Option<&str>,
196) -> String {
197    let mut error_msg = String::new();
198
199    if !output.stdout.is_empty() {
200        error_msg.push_str(&String::from_utf8_lossy(&output.stdout));
201        error_msg.push('\n');
202    }
203
204    if !output.stderr.is_empty() {
205        error_msg.push_str(&String::from_utf8_lossy(&output.stderr));
206        error_msg.push('\n');
207    }
208
209    if let Some(description) = command_description {
210        let _ = writeln!(error_msg, "{description}");
211    }
212
213    let _ = write!(
214        error_msg,
215        "failed with exit code: {}",
216        output.status.code().unwrap()
217    );
218
219    error_msg
220}
221
222/// Short-cut wrapper to create a new command, feed in the args, set the working directory, and then
223/// run it, checking the resulting exit code.
224pub fn exec<T: AsRef<OsStr>>(
225    program: &str,
226    args: impl IntoIterator<Item = T>,
227    in_repo: bool,
228) -> Result<()> {
229    let mut command = match program.strip_prefix("scripts/") {
230        Some(script) => Command::script(script),
231        None => Command::new(program),
232    };
233    command.args(args);
234    if in_repo {
235        command.in_repo();
236    }
237    command.check_run()
238}
239
240fn get_progress_bar() -> Result<ProgressBar> {
241    let progress_bar = ProgressBar::new_spinner();
242    progress_bar.enable_steady_tick(Duration::from_millis(125));
243    progress_bar.set_style(
244        ProgressStyle::with_template("{spinner} {msg:.magenta.bold}")?
245            // https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json
246            .tick_strings(&["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]),
247    );
248
249    Ok(progress_bar)
250}
251
252pub fn set_global_verbosity(verbosity: LevelFilter) {
253    VERBOSITY.set(verbosity).expect("could not set verbosity");
254}
255
256pub fn set_global_path(path: String) {
257    PATH.set(path).expect("could not set path");
258}