Skip to main content

odx_core/
process.rs

1use std::ffi::OsString;
2use std::io;
3use std::path::PathBuf;
4
5/// A program invocation: what to run, with which arguments, and where.
6///
7/// ```
8/// use odx_core::Invocation;
9///
10/// let git = Invocation::new("git").arg("status").current_dir("/tmp");
11/// assert_eq!(git.program(), "git");
12/// assert_eq!(git.arguments(), ["status"]);
13/// assert_eq!(git.input(), None);
14/// assert_eq!(git.stdin("diff").input(), Some(&b"diff"[..]));
15/// ```
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Invocation {
18    program: OsString,
19    args: Vec<OsString>,
20    current_dir: Option<PathBuf>,
21    envs: Vec<(OsString, OsString)>,
22    stdin: Option<Vec<u8>>,
23}
24
25impl Invocation {
26    /// Start an invocation of `program` with no arguments.
27    pub fn new(program: impl Into<OsString>) -> Self {
28        Self {
29            program: program.into(),
30            args: Vec::new(),
31            current_dir: None,
32            envs: Vec::new(),
33            stdin: None,
34        }
35    }
36
37    /// Append one argument.
38    #[must_use]
39    pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
40        self.args.push(arg.into());
41        self
42    }
43
44    /// Append several arguments.
45    #[must_use]
46    pub fn args<I, A>(mut self, args: I) -> Self
47    where
48        I: IntoIterator<Item = A>,
49        A: Into<OsString>,
50    {
51        self.args.extend(args.into_iter().map(Into::into));
52        self
53    }
54
55    /// Set an environment variable for the process.
56    #[must_use]
57    pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
58        self.envs.push((key.into(), value.into()));
59        self
60    }
61
62    /// Run in `dir` instead of the current directory.
63    #[must_use]
64    pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
65        self.current_dir = Some(dir.into());
66        self
67    }
68
69    /// Feed `input` to the process on standard input.
70    #[must_use]
71    pub fn stdin(mut self, input: impl Into<Vec<u8>>) -> Self {
72        self.stdin = Some(input.into());
73        self
74    }
75
76    /// What is fed on standard input, if anything.
77    pub fn input(&self) -> Option<&[u8]> {
78        self.stdin.as_deref()
79    }
80
81    /// The program to run.
82    pub fn program(&self) -> &OsString {
83        &self.program
84    }
85
86    /// The arguments, in order.
87    pub fn arguments(&self) -> &[OsString] {
88        &self.args
89    }
90
91    /// Extra environment variables, in order.
92    pub fn envs(&self) -> &[(OsString, OsString)] {
93        &self.envs
94    }
95
96    /// The working directory, if one was set.
97    pub fn working_dir(&self) -> Option<&PathBuf> {
98        self.current_dir.as_ref()
99    }
100}
101
102/// The result of a finished process.
103#[derive(Debug, Clone, PartialEq, Eq, Default)]
104pub struct Output {
105    /// Exit status code; `None` when the process was terminated by a signal.
106    pub status: Option<i32>,
107    /// Captured standard output.
108    pub stdout: Vec<u8>,
109    /// Captured standard error.
110    pub stderr: Vec<u8>,
111}
112
113impl Output {
114    /// Whether the process exited with status `0`.
115    ///
116    /// ```
117    /// use odx_core::Output;
118    ///
119    /// assert!(Output { status: Some(0), ..Output::default() }.success());
120    /// assert!(!Output { status: Some(1), ..Output::default() }.success());
121    /// assert!(!Output { status: None, ..Output::default() }.success());
122    /// ```
123    pub fn success(&self) -> bool {
124        self.status == Some(0)
125    }
126}
127
128/// Runs external programs such as `git`, `uv` and `mergiraf`.
129pub trait ProcessRunner: std::fmt::Debug + Send + Sync {
130    /// Run `invocation` to completion and capture its output.
131    ///
132    /// A non-zero exit status is not an error; inspect [`Output::success`].
133    ///
134    /// # Errors
135    ///
136    /// Returns an error when the program cannot be started.
137    fn run(&self, invocation: &Invocation) -> io::Result<Output>;
138}
139
140#[cfg(test)]
141mod tests {
142    use super::Invocation;
143    use std::path::PathBuf;
144
145    #[test]
146    fn invocation_builder_keeps_order_and_dir() {
147        let inv = Invocation::new("uv")
148            .arg("lock")
149            .args(["--upgrade-package", "x"])
150            .env("UV_OFFLINE", "1")
151            .current_dir("/work");
152        assert_eq!(inv.arguments(), ["lock", "--upgrade-package", "x"]);
153        assert_eq!(inv.envs(), [("UV_OFFLINE".into(), "1".into())]);
154        assert_eq!(inv.working_dir(), Some(&PathBuf::from("/work")));
155        assert_eq!(Invocation::new("git").working_dir(), None);
156    }
157}