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