vdev/
app.rs

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