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 current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
83 let path = dir.as_ref();
84 self.inner.current_dir(path);
85 self.cwd = Some(path.to_path_buf());
86 self
87 }
88
89 pub fn status(&mut self) -> Result<ExitStatus> {
90 #[cfg(miri)]
91 {
92 let snap = self.snapshot();
93 synthetic_status(&snap)
94 }
95
96 #[cfg(not(miri))]
97 {
98 let desc = format!("{:?}", self.inner);
99 let status = self
100 .inner
101 .status()
102 .with_context(|| format!("failed to run {desc}"))?;
103 Ok(status)
104 }
105 }
106
107 pub fn output(&mut self) -> Result<CommandOutput> {
108 #[cfg(miri)]
109 {
110 let snap = self.snapshot();
111 synthetic_output(&snap)
112 }
113
114 #[cfg(not(miri))]
115 {
116 let desc = format!("{:?}", self.inner);
117 let out = self
118 .inner
119 .output()
120 .with_context(|| format!("failed to run {desc}"))?;
121 Ok(CommandOutput::from(out))
122 }
123 }
124
125 pub fn spawn(&mut self) -> Result<ChildHandle> {
126 #[cfg(miri)]
127 {
128 bail!("spawn is not supported under miri synthetic process backend")
129 }
130
131 #[cfg(not(miri))]
132 {
133 let desc = format!("{:?}", self.inner);
134 let child = self
135 .inner
136 .spawn()
137 .with_context(|| format!("failed to spawn {desc}"))?;
138 Ok(ChildHandle::new(child, Vec::new()))
139 }
140 }
141
142 pub fn snapshot(&self) -> CommandSnapshot {
144 CommandSnapshot {
145 program: self.program.clone(),
146 args: self.args.clone(),
147 envs: self.envs.clone(),
148 cwd: self.cwd.clone(),
149 }
150 }
151}
152
153#[derive(Clone, Debug, PartialEq, Eq)]
154#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
155pub struct CommandSnapshot {
156 pub program: OsString,
157 pub args: Vec<OsString>,
158 pub envs: Vec<(OsString, OsString)>,
159 pub cwd: Option<PathBuf>,
160}
161
162pub struct CommandOutput {
163 pub status: ExitStatus,
164 pub stdout: Vec<u8>,
165 pub stderr: Vec<u8>,
166}
167
168impl CommandOutput {
169 pub fn success(&self) -> bool {
170 self.status.success()
171 }
172}
173
174#[allow(clippy::disallowed_types)]
175impl From<StdOutput> for CommandOutput {
176 fn from(value: StdOutput) -> Self {
177 Self {
178 status: value.status,
179 stdout: value.stdout,
180 stderr: value.stderr,
181 }
182 }
183}
184
185#[cfg(miri)]
186use crate::synthetic::{synthetic_output, synthetic_status};
187
188#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use oxdock_fs::GuardedPath;
193
194 #[test]
195 fn command_builder_snapshot_tracks_configuration() {
196 let temp = GuardedPath::tempdir().expect("tempdir");
197 let dir = temp.as_guarded_path().display().to_string();
198
199 let mut builder = CommandBuilder::new("prog");
200 builder.arg("a").args(["b", "c"]);
201 builder.env("K", "V");
202 builder.env("K", "V2"); builder.env("GONE", "x");
204 builder.env_remove("GONE");
205 builder.current_dir(&dir);
206
207 let snap = builder.snapshot();
208 assert_eq!(snap.program, OsString::from("prog"));
209 assert_eq!(
210 snap.args,
211 vec![
212 OsString::from("a"),
213 OsString::from("b"),
214 OsString::from("c")
215 ]
216 );
217 assert!(
218 snap.envs
219 .contains(&(OsString::from("K"), OsString::from("V2")))
220 );
221 assert!(
222 !snap.envs.iter().any(|(k, _)| k == "GONE"),
223 "env_remove must drop tracked entries"
224 );
225 assert_eq!(snap.cwd.as_deref(), Some(std::path::Path::new(&dir)));
226 }
227}