1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
use std::ffi::OsStr;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::Command;

use color_eyre::eyre::eyre;
use color_eyre::Result;
use owo_colors::OwoColorize;

use crate::Binaries;

pub struct Runner {
    binaries: Binaries,
}

impl Runner {
    pub fn new(binaries: Binaries) -> Self {
        Self { binaries }
    }

    pub fn step(&self, name: &str) {
        eprintln!("{}", format!("🧾 running step `{name}`").bold());
    }

    pub fn run(&self, command: impl IntoCommand) -> Result<()> {
        let mut command = command.into_command(&self.binaries)?;

        self.print_subprocess("running", &command)?;
        command.spawn()?.wait()?.exit_ok()?;

        Ok(())
    }

    pub fn exec(&self, command: impl IntoCommand) -> Result<()> {
        let mut command = command.into_command(&self.binaries)?;

        self.print_subprocess("launching", &command)?;
        Err(command.exec())?;

        unreachable!("exec should not return");
    }

    fn print_subprocess(&self, action: &str, command: &Command) -> Result<()> {
        let program = command.get_program();
        let args = command.get_args();
        let command_line = std::iter::once(program)
            .chain(args)
            .map(|s| s.to_str_or_err())
            .map(|s| {
                s.map(|s| {
                    // If an argument contains any whitespace, surround it with quotes. This is just
                    // a human-readable string, so it doesn't need to be perfectly correct with
                    // respect to shell parsing.
                    if s.contains(char::is_whitespace) {
                        format!("\"{}\"", s.replace('\"', "\\\""))
                    } else {
                        s.to_string()
                    }
                })
            })
            .collect::<Result<Vec<_>>>()?
            .join(" ");

        if let Some(current_dir) = command.get_current_dir() {
            let current_dir = current_dir.to_str_or_err()?;
            eprintln!("⭐ {} `{}` in {}", action, command_line, current_dir);
        } else {
            eprintln!("⭐ {} `{}`", action, command_line);
        }

        Ok(())
    }

    pub fn done(self) {
        eprintln!("{}", "💜 done!".bold());
    }
}

pub trait IntoCommand {
    fn into_command(self, binaries: &Binaries) -> Result<Command>;
}

trait ToStrOrError {
    fn to_str_or_err(&self) -> Result<&str>;
}

impl ToStrOrError for OsStr {
    fn to_str_or_err(&self) -> Result<&str> {
        self.to_str()
            .ok_or_else(|| eyre!("OsStr should be valid utf-8"))
    }
}

impl ToStrOrError for Path {
    fn to_str_or_err(&self) -> Result<&str> {
        self.to_str()
            .ok_or_else(|| eyre!("Path should be valid utf-8"))
    }
}