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