Skip to main content

release_tool/
command.rs

1use anyhow::{Context, Result, bail};
2use std::collections::BTreeMap;
3use std::path::PathBuf;
4use std::process::{Command, Stdio};
5
6#[derive(Clone, Eq, PartialEq)]
7pub struct CommandRequest {
8    pub program: String,
9    pub arguments: Vec<String>,
10    pub current_dir: PathBuf,
11    pub environment: BTreeMap<String, String>,
12}
13
14impl std::fmt::Debug for CommandRequest {
15    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        let redacted_environment: BTreeMap<_, _> = self
17            .environment
18            .keys()
19            .map(|key| (key, "[REDACTED]"))
20            .collect();
21        formatter
22            .debug_struct("CommandRequest")
23            .field("program", &self.program)
24            .field("arguments", &self.arguments)
25            .field("current_dir", &self.current_dir)
26            .field("environment", &redacted_environment)
27            .finish()
28    }
29}
30
31impl CommandRequest {
32    pub fn new(
33        program: impl Into<String>,
34        arguments: impl IntoIterator<Item = impl Into<String>>,
35        current_dir: impl Into<PathBuf>,
36    ) -> Self {
37        Self {
38            program: program.into(),
39            arguments: arguments.into_iter().map(Into::into).collect(),
40            current_dir: current_dir.into(),
41            environment: BTreeMap::new(),
42        }
43    }
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct CommandResult {
48    pub status: i32,
49    pub stdout: String,
50    pub stderr: String,
51}
52
53impl CommandResult {
54    pub fn redact(mut self, secrets: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
55        for secret in secrets {
56            let secret = secret.as_ref();
57            if secret.is_empty() {
58                continue;
59            }
60            self.stdout = self.stdout.replace(secret, "[REDACTED]");
61            self.stderr = self.stderr.replace(secret, "[REDACTED]");
62        }
63        self
64    }
65
66    pub fn require_success(self, description: &str) -> Result<Self> {
67        if self.status == 0 {
68            return Ok(self);
69        }
70        let detail = self.stderr.trim();
71        if detail.is_empty() {
72            bail!("{description} failed with exit status {}", self.status);
73        }
74        bail!(
75            "{description} failed with exit status {}: {detail}",
76            self.status
77        )
78    }
79}
80
81pub trait CommandRunner: Send + Sync {
82    fn execute(&self, request: &CommandRequest) -> Result<CommandResult>;
83
84    /// Runs a project-owned command with native stdout/stderr when the adapter supports it.
85    /// Custom adapters retain capture semantics until they explicitly opt into streaming.
86    fn execute_inheriting_output(&self, request: &CommandRequest) -> Result<CommandResult> {
87        self.execute(request)
88    }
89}
90
91#[derive(Clone, Copy, Debug, Default)]
92pub struct SystemCommandRunner;
93
94impl CommandRunner for SystemCommandRunner {
95    fn execute(&self, request: &CommandRequest) -> Result<CommandResult> {
96        let output = command(request)
97            .output()
98            .with_context(|| format!("failed to execute `{}`", request.program))?;
99        Ok(CommandResult {
100            status: output.status.code().unwrap_or(1),
101            stdout: String::from_utf8(output.stdout)
102                .context("command stdout is not valid UTF-8")?,
103            stderr: String::from_utf8(output.stderr)
104                .context("command stderr is not valid UTF-8")?,
105        })
106    }
107
108    fn execute_inheriting_output(&self, request: &CommandRequest) -> Result<CommandResult> {
109        let status = command(request)
110            .stdout(Stdio::inherit())
111            .stderr(Stdio::inherit())
112            .status()
113            .with_context(|| format!("failed to execute `{}`", request.program))?;
114        Ok(CommandResult {
115            status: status.code().unwrap_or(1),
116            stdout: String::new(),
117            stderr: String::new(),
118        })
119    }
120}
121
122fn command(request: &CommandRequest) -> Command {
123    let mut command = Command::new(&request.program);
124    command
125        .args(&request.arguments)
126        .current_dir(&request.current_dir)
127        .envs(&request.environment)
128        .stdin(Stdio::null());
129    command
130}