Skip to main content

xbp_cli/sdk/
command.rs

1use anyhow::Context;
2use std::process::Output;
3use std::process::Stdio;
4use tokio::process::Command;
5use tracing::debug;
6
7use crate::sdk::{command_debug_log, command_failure_message, decode_output};
8
9/// Normalized command execution result.
10#[derive(Debug)]
11pub struct CommandOutcome {
12    pub output: Output,
13    pub stdout: String,
14    pub stderr: String,
15}
16
17impl CommandOutcome {
18    pub fn is_success(&self) -> bool {
19        self.output.status.success()
20    }
21}
22
23/// Thin async runner with optional debug tracing.
24#[derive(Clone, Debug)]
25pub struct CommandRunner {
26    debug: bool,
27}
28
29impl CommandRunner {
30    pub fn new(debug: bool) -> Self {
31        Self { debug }
32    }
33
34    pub async fn run(&self, program: &str, args: &[&str]) -> anyhow::Result<CommandOutcome> {
35        let output = Command::new(program)
36            .args(args)
37            .output()
38            .await
39            .with_context(|| format!("Failed to execute {} {}", program, args.join(" ")))?;
40
41        command_debug_log(self.debug, program, args, &output, |msg| debug!("{}", msg));
42
43        let stdout = decode_output(&output.stdout);
44        let stderr = decode_output(&output.stderr);
45
46        Ok(CommandOutcome {
47            output,
48            stdout,
49            stderr,
50        })
51    }
52
53    pub async fn run_checked(
54        &self,
55        program: &str,
56        args: &[&str],
57        hint: Option<&str>,
58    ) -> anyhow::Result<CommandOutcome> {
59        let outcome = self.run(program, args).await?;
60        if !outcome.output.status.success() {
61            let message = command_failure_message(program, args, &outcome.output, hint);
62            return Err(anyhow::anyhow!(message));
63        }
64        Ok(outcome)
65    }
66
67    pub async fn run_with_stdio(
68        &self,
69        program: &str,
70        args: &[&str],
71    ) -> anyhow::Result<std::process::ExitStatus> {
72        let status = Command::new(program)
73            .args(args)
74            .stdout(Stdio::inherit())
75            .stderr(Stdio::inherit())
76            .status()
77            .await
78            .with_context(|| format!("Failed to execute {} {}", program, args.join(" ")))?;
79
80        if self.debug {
81            debug!(
82                "{} {} -> status={}",
83                program,
84                args.join(" "),
85                status
86                    .code()
87                    .map(|c| c.to_string())
88                    .unwrap_or_else(|| "SIG".into())
89            );
90        }
91        Ok(status)
92    }
93
94    pub async fn run_builder<F>(
95        &self,
96        program: &str,
97        args: &[&str],
98        configure: F,
99    ) -> anyhow::Result<CommandOutcome>
100    where
101        F: FnOnce(&mut Command),
102    {
103        let mut cmd = Command::new(program);
104        cmd.args(args);
105        configure(&mut cmd);
106
107        let output = cmd
108            .output()
109            .await
110            .with_context(|| format!("Failed to execute {} {}", program, args.join(" ")))?;
111
112        command_debug_log(self.debug, program, args, &output, |msg| debug!("{}", msg));
113
114        let stdout = decode_output(&output.stdout);
115        let stderr = decode_output(&output.stderr);
116
117        Ok(CommandOutcome {
118            output,
119            stdout,
120            stderr,
121        })
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[tokio::test]
130    async fn command_runner_success() {
131        let runner = CommandRunner::new(true);
132        #[cfg(windows)]
133        let outcome = runner
134            .run_checked("cmd", &["/C", "echo hello"], None)
135            .await
136            .unwrap();
137
138        #[cfg(not(windows))]
139        let outcome = runner
140            .run_checked("sh", &["-c", "echo hello"], None)
141            .await
142            .unwrap();
143
144        assert!(outcome.is_success());
145        assert_eq!(outcome.stdout, "hello");
146    }
147
148    #[tokio::test]
149    async fn command_runner_failure_returns_error() {
150        let runner = CommandRunner::new(false);
151        #[cfg(windows)]
152        let err = runner
153            .run_checked("cmd", &["/C", "exit /b 1"], Some("intentional failure"))
154            .await
155            .unwrap_err();
156
157        #[cfg(not(windows))]
158        let err = runner
159            .run_checked("sh", &["-c", "false"], Some("intentional failure"))
160            .await
161            .unwrap_err();
162
163        assert!(err.to_string().contains("failed with status"));
164    }
165}