oxdock_process/
builder.rs1use std::{
2 ffi::{OsStr, OsString},
3 iter::IntoIterator,
4};
5
6#[cfg(not(miri))]
7use anyhow::Context;
8use anyhow::Result;
9#[cfg(miri)]
10use anyhow::bail;
11#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
12use std::fs::File;
13#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
14use std::path::{Path, PathBuf};
15#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
16use std::process::{Command as ProcessCommand, ExitStatus, Output as StdOutput, Stdio};
17
18use crate::child::ChildHandle;
19
20#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
22pub struct CommandBuilder {
23 inner: ProcessCommand,
24 program: OsString,
25 args: Vec<OsString>,
26 envs: Vec<(OsString, OsString)>,
27 cwd: Option<PathBuf>,
28}
29
30#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
31impl CommandBuilder {
32 pub fn new(program: impl AsRef<OsStr>) -> Self {
33 let prog = program.as_ref().to_os_string();
34 Self {
35 inner: ProcessCommand::new(&prog),
36 program: prog,
37 args: Vec::new(),
38 envs: Vec::new(),
39 cwd: None,
40 }
41 }
42
43 pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
44 let val = arg.as_ref().to_os_string();
45 self.inner.arg(&val);
46 self.args.push(val);
47 self
48 }
49
50 pub fn args<S, I>(&mut self, args: I) -> &mut Self
51 where
52 S: AsRef<OsStr>,
53 I: IntoIterator<Item = S>,
54 {
55 for arg in args {
56 self.arg(arg);
57 }
58 self
59 }
60
61 pub fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
62 let key = key.as_ref().to_os_string();
63 let value = value.as_ref().to_os_string();
64 self.inner.env(&key, &value);
65 self.envs.retain(|(k, _)| k != &key);
66 self.envs.push((key, value));
67 self
68 }
69
70 pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
71 let key = key.as_ref();
72 self.inner.env_remove(key);
73 self.envs.retain(|(k, _)| k != key);
74 self
75 }
76
77 pub fn stdin_file(&mut self, file: File) -> &mut Self {
78 self.inner.stdin(Stdio::from(file));
79 self
80 }
81
82 pub fn stdin_piped(&mut self) -> &mut Self {
83 self.inner.stdin(Stdio::piped());
84 self
85 }
86
87 pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
88 let path = dir.as_ref();
89 self.inner.current_dir(path);
90 self.cwd = Some(path.to_path_buf());
91 self
92 }
93
94 pub fn status(&mut self) -> Result<ExitStatus> {
95 #[cfg(miri)]
96 {
97 let snap = self.snapshot();
98 synthetic_status(&snap)
99 }
100
101 #[cfg(not(miri))]
102 {
103 let desc = format!("{:?}", self.inner);
104 let status = self
105 .inner
106 .status()
107 .with_context(|| format!("failed to run {desc}"))?;
108 Ok(status)
109 }
110 }
111
112 pub fn output(&mut self) -> Result<CommandOutput> {
113 #[cfg(miri)]
114 {
115 let snap = self.snapshot();
116 synthetic_output(&snap)
117 }
118
119 #[cfg(not(miri))]
120 {
121 let desc = format!("{:?}", self.inner);
122 let out = self
123 .inner
124 .output()
125 .with_context(|| format!("failed to run {desc}"))?;
126 Ok(CommandOutput::from(out))
127 }
128 }
129
130 pub fn spawn(&mut self) -> Result<ChildHandle> {
131 #[cfg(miri)]
132 {
133 bail!("spawn is not supported under miri synthetic process backend")
134 }
135
136 #[cfg(not(miri))]
137 {
138 let desc = format!("{:?}", self.inner);
139 let child = self
140 .inner
141 .spawn()
142 .with_context(|| format!("failed to spawn {desc}"))?;
143 Ok(ChildHandle::new(child, None, Vec::new()))
144 }
145 }
146
147 pub fn snapshot(&self) -> CommandSnapshot {
149 CommandSnapshot {
150 program: self.program.clone(),
151 args: self.args.clone(),
152 envs: self.envs.clone(),
153 cwd: self.cwd.clone(),
154 }
155 }
156}
157
158#[derive(Clone, Debug, PartialEq, Eq)]
159#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
160pub struct CommandSnapshot {
161 pub program: OsString,
162 pub args: Vec<OsString>,
163 pub envs: Vec<(OsString, OsString)>,
164 pub cwd: Option<PathBuf>,
165}
166
167pub struct CommandOutput {
168 pub status: ExitStatus,
169 pub stdout: Vec<u8>,
170 pub stderr: Vec<u8>,
171}
172
173impl CommandOutput {
174 pub fn success(&self) -> bool {
175 self.status.success()
176 }
177}
178
179#[allow(clippy::disallowed_types)]
180impl From<StdOutput> for CommandOutput {
181 fn from(value: StdOutput) -> Self {
182 Self {
183 status: value.status,
184 stdout: value.stdout,
185 stderr: value.stderr,
186 }
187 }
188}
189
190#[cfg(miri)]
191use crate::synthetic::{synthetic_output, synthetic_status};
192
193#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use oxdock_fs::GuardedPath;
198
199 #[test]
200 fn command_builder_snapshot_tracks_configuration() {
201 let temp = GuardedPath::tempdir().expect("tempdir");
202 let dir = temp.as_guarded_path().display().to_string();
203
204 let mut builder = CommandBuilder::new("prog");
205 builder.arg("a").args(["b", "c"]);
206 builder.env("K", "V");
207 builder.env("K", "V2"); builder.env("GONE", "x");
209 builder.env_remove("GONE");
210 builder.current_dir(&dir);
211
212 let snap = builder.snapshot();
213 assert_eq!(snap.program, OsString::from("prog"));
214 assert_eq!(
215 snap.args,
216 vec![
217 OsString::from("a"),
218 OsString::from("b"),
219 OsString::from("c")
220 ]
221 );
222 assert!(
223 snap.envs
224 .contains(&(OsString::from("K"), OsString::from("V2")))
225 );
226 assert!(
227 !snap.envs.iter().any(|(k, _)| k == "GONE"),
228 "env_remove must drop tracked entries"
229 );
230 assert_eq!(snap.cwd.as_deref(), Some(std::path::Path::new(&dir)));
231 }
232}