1use std::ffi::OsString;
2use std::io;
3use std::path::PathBuf;
4
5#[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 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 #[must_use]
39 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
40 self.args.push(arg.into());
41 self
42 }
43
44 #[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 #[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 #[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 #[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 pub fn input(&self) -> Option<&[u8]> {
78 self.stdin.as_deref()
79 }
80
81 pub fn program(&self) -> &OsString {
83 &self.program
84 }
85
86 pub fn arguments(&self) -> &[OsString] {
88 &self.args
89 }
90
91 pub fn envs(&self) -> &[(OsString, OsString)] {
93 &self.envs
94 }
95
96 pub fn working_dir(&self) -> Option<&PathBuf> {
98 self.current_dir.as_ref()
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Default)]
104pub struct Output {
105 pub status: Option<i32>,
107 pub stdout: Vec<u8>,
109 pub stderr: Vec<u8>,
111}
112
113impl Output {
114 pub fn success(&self) -> bool {
124 self.status == Some(0)
125 }
126}
127
128pub trait ProcessRunner: std::fmt::Debug + Send + Sync {
130 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}