xtask_lib/tasks/util/
cmd.rs

1//! Command execution helpers.
2//!
3//! All tasks use [`run`] to execute external processes.  It prints the command
4//! before running it (for CI log visibility) and converts non-zero exit codes
5//! into descriptive [`anyhow::Error`] values so callers can use `?`.
6
7use std::{
8    ffi::OsStr,
9    path::Path,
10    process::{Command, ExitStatus},
11};
12
13use anyhow::{Context, Result, bail};
14
15/// Runs an external command, streaming its output to the terminal.
16///
17/// The command is printed before execution so CI logs show exactly what ran.
18/// A non-zero exit status is converted to an error.
19///
20/// # Arguments
21///
22/// * `program` – The executable to run (looked up via `PATH`).
23/// * `args` – Arguments passed to the program.
24/// * `dir` – Working directory for the process.
25/// * `env` – Additional environment variables (key-value pairs).
26///
27/// # Errors
28///
29/// Returns an error if the process cannot be spawned or exits with a non-zero
30/// status code.
31///
32/// # Examples
33///
34/// ```no_run
35/// use std::path::Path;
36/// use xtask_lib::tasks::util::cmd::run;
37///
38/// // Run `cargo fmt --check` in the workspace root.
39/// run("cargo", &["fmt", "--check"], Path::new("."), &[])?;
40/// # Ok::<(), anyhow::Error>(())
41/// ```
42pub fn run(program: &str, args: &[&str], dir: &Path, env: &[(&str, &str)]) -> Result<()> {
43    let status = build_command(program, args, dir, env)
44        .status()
45        .with_context(|| format!("failed to spawn `{program}`"))?;
46
47    check_status(program, status)
48}
49
50/// Runs an external command and captures its stdout as a `String`.
51///
52/// Stderr is inherited (printed to the terminal).  The returned string has
53/// leading/trailing whitespace trimmed.
54///
55/// # Errors
56///
57/// Returns an error if the process cannot be spawned, exits with a non-zero
58/// status, or produces non-UTF-8 output.
59///
60/// # Examples
61///
62/// ```no_run
63/// use std::path::Path;
64/// use xtask_lib::tasks::util::cmd::output;
65///
66/// let version = output("cargo", &["--version"], Path::new("."), &[])?;
67/// assert!(version.starts_with("cargo "));
68/// # Ok::<(), anyhow::Error>(())
69/// ```
70pub fn output(program: &str, args: &[&str], dir: &Path, env: &[(&str, &str)]) -> Result<String> {
71    let out = build_command(program, args, dir, env)
72        .output()
73        .with_context(|| format!("failed to spawn `{program}`"))?;
74
75    check_status(program, out.status)?;
76
77    let stdout = String::from_utf8(out.stdout)
78        .with_context(|| format!("`{program}` produced non-UTF-8 output"))?;
79
80    Ok(stdout.trim().to_owned())
81}
82
83fn build_command(program: &str, args: &[&str], dir: &Path, env: &[(&str, &str)]) -> Command {
84    let display_args = args.join(" ");
85    println!("$ {program} {display_args}");
86
87    let mut cmd = Command::new(program);
88    cmd.args(args).current_dir(dir);
89
90    for (key, value) in env {
91        cmd.env(OsStr::new(key), OsStr::new(value));
92    }
93
94    cmd
95}
96
97fn check_status(program: &str, status: ExitStatus) -> Result<()> {
98    if status.success() {
99        return Ok(());
100    }
101
102    match status.code() {
103        Some(code) => bail!("`{program}` exited with status {code}"),
104        None => bail!("`{program}` was terminated by a signal"),
105    }
106}