1#[cfg(feature = "mock-process")]
2mod mock;
3pub mod serial_cargo_env;
4mod shell;
5
6use anyhow::{Context, Result, bail};
7use oxdock_fs::{GuardedPath, PolicyPath};
8use shell::shell_cmd;
9pub use shell::{ShellLauncher, shell_program};
10use std::collections::HashMap;
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::{Child, Command as ProcessCommand, ExitStatus, Output as StdOutput, Stdio};
17use std::{
18 ffi::{OsStr, OsString},
19 iter::IntoIterator,
20};
21
22#[cfg(miri)]
23use oxdock_fs::PathResolver;
24
25#[cfg(feature = "mock-process")]
26pub use mock::{MockHandle, MockProcessManager, MockRunCall, MockSpawnCall};
27
28#[derive(Clone, Debug)]
32pub struct CommandContext {
33 cwd: PolicyPath,
34 envs: HashMap<String, String>,
35 cargo_target_dir: GuardedPath,
36 workspace_root: GuardedPath,
37 build_context: GuardedPath,
38}
39
40impl CommandContext {
41 #[allow(clippy::too_many_arguments)]
42 pub fn new(
43 cwd: &PolicyPath,
44 envs: &HashMap<String, String>,
45 cargo_target_dir: &GuardedPath,
46 workspace_root: &GuardedPath,
47 build_context: &GuardedPath,
48 ) -> Self {
49 Self {
50 cwd: cwd.clone(),
51 envs: envs.clone(),
52 cargo_target_dir: cargo_target_dir.clone(),
53 workspace_root: workspace_root.clone(),
54 build_context: build_context.clone(),
55 }
56 }
57
58 pub fn cwd(&self) -> &PolicyPath {
59 &self.cwd
60 }
61
62 pub fn envs(&self) -> &HashMap<String, String> {
63 &self.envs
64 }
65
66 pub fn cargo_target_dir(&self) -> &GuardedPath {
67 &self.cargo_target_dir
68 }
69
70 pub fn workspace_root(&self) -> &GuardedPath {
71 &self.workspace_root
72 }
73
74 pub fn build_context(&self) -> &GuardedPath {
75 &self.build_context
76 }
77}
78
79pub trait BackgroundHandle {
81 fn try_wait(&mut self) -> Result<Option<ExitStatus>>;
82 fn kill(&mut self) -> Result<()>;
83 fn wait(&mut self) -> Result<ExitStatus>;
84}
85
86pub trait ProcessManager: Clone {
91 type Handle: BackgroundHandle;
92
93 fn run(&mut self, ctx: &CommandContext, script: &str) -> Result<()>;
94 fn run_capture(&mut self, ctx: &CommandContext, script: &str) -> Result<Vec<u8>>;
95 fn spawn_bg(&mut self, ctx: &CommandContext, script: &str) -> Result<Self::Handle>;
96}
97
98#[derive(Clone, Default)]
100#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
101pub struct ShellProcessManager;
102
103impl ProcessManager for ShellProcessManager {
104 type Handle = ChildHandle;
105
106 #[allow(clippy::disallowed_types, clippy::disallowed_methods)]
107 fn run(&mut self, ctx: &CommandContext, script: &str) -> Result<()> {
108 let mut command = shell_cmd(script);
109 apply_ctx(&mut command, ctx);
110 run_cmd(&mut command)
111 }
112
113 #[allow(clippy::disallowed_types, clippy::disallowed_methods)]
114 fn run_capture(&mut self, ctx: &CommandContext, script: &str) -> Result<Vec<u8>> {
115 let mut command = shell_cmd(script);
116 apply_ctx(&mut command, ctx);
117 command.stdout(Stdio::piped());
118 let output = command
119 .output()
120 .with_context(|| format!("failed to run {:?}", command))?;
121 if !output.status.success() {
122 bail!("command {:?} failed with status {}", command, output.status);
123 }
124 Ok(output.stdout)
125 }
126
127 #[allow(clippy::disallowed_types, clippy::disallowed_methods)]
128 fn spawn_bg(&mut self, ctx: &CommandContext, script: &str) -> Result<Self::Handle> {
129 let mut command = shell_cmd(script);
130 apply_ctx(&mut command, ctx);
131 let child = command
132 .spawn()
133 .with_context(|| format!("failed to spawn {:?}", command))?;
134 Ok(ChildHandle { child })
135 }
136}
137
138#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
139fn apply_ctx(command: &mut ProcessCommand, ctx: &CommandContext) {
140 let cwd_path: std::borrow::Cow<std::path::Path> = match ctx.cwd() {
146 PolicyPath::Guarded(p) => oxdock_fs::command_path(p),
147 PolicyPath::Unguarded(p) => std::borrow::Cow::Borrowed(p.as_path()),
148 };
149 command.current_dir(cwd_path);
150 command.envs(ctx.envs());
151 command.env(
152 "CARGO_TARGET_DIR",
153 oxdock_fs::command_path(ctx.cargo_target_dir()).into_owned(),
154 );
155}
156
157#[derive(Debug)]
158#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
159pub struct ChildHandle {
160 child: Child,
161}
162
163impl BackgroundHandle for ChildHandle {
164 fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
165 Ok(self.child.try_wait()?)
166 }
167
168 fn kill(&mut self) -> Result<()> {
169 if self.child.try_wait()?.is_none() {
170 let _ = self.child.kill();
171 }
172 Ok(())
173 }
174
175 fn wait(&mut self) -> Result<ExitStatus> {
176 Ok(self.child.wait()?)
177 }
178}
179
180#[cfg(miri)]
185#[derive(Clone, Default)]
186pub struct SyntheticProcessManager;
187
188#[cfg(miri)]
189#[derive(Clone)]
190pub struct SyntheticBgHandle {
191 ctx: CommandContext,
192 actions: Vec<Action>,
193 remaining: std::time::Duration,
194 last_polled: std::time::Instant,
195 status: ExitStatus,
196 applied: bool,
197 killed: bool,
198}
199
200#[cfg(miri)]
201#[derive(Clone)]
202enum Action {
203 WriteFile { target: GuardedPath, data: Vec<u8> },
204}
205
206#[cfg(miri)]
207impl BackgroundHandle for SyntheticBgHandle {
208 fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
209 if self.killed {
210 self.applied = true;
211 return Ok(Some(self.status));
212 }
213 if self.applied {
214 return Ok(Some(self.status));
215 }
216 let now = std::time::Instant::now();
217 let elapsed = now.saturating_duration_since(self.last_polled);
218 const MAX_ADVANCE: std::time::Duration = std::time::Duration::from_millis(15);
219 let advance = elapsed.min(MAX_ADVANCE).min(self.remaining);
220 self.remaining = self.remaining.saturating_sub(advance);
221 self.last_polled = now;
222
223 if self.remaining.is_zero() {
224 apply_actions(&self.ctx, &self.actions)?;
225 self.applied = true;
226 Ok(Some(self.status))
227 } else {
228 Ok(None)
229 }
230 }
231
232 fn kill(&mut self) -> Result<()> {
233 self.killed = true;
234 Ok(())
235 }
236
237 fn wait(&mut self) -> Result<ExitStatus> {
238 if self.killed {
239 self.applied = true;
240 return Ok(self.status);
241 }
242 if !self.applied {
243 if !self.remaining.is_zero() {
244 std::thread::sleep(self.remaining);
245 }
246 apply_actions(&self.ctx, &self.actions)?;
247 self.applied = true;
248 }
249 Ok(self.status)
250 }
251}
252
253#[cfg(miri)]
254impl ProcessManager for SyntheticProcessManager {
255 type Handle = SyntheticBgHandle;
256
257 fn run(&mut self, ctx: &CommandContext, script: &str) -> Result<()> {
258 let (_out, status) = execute_sync(ctx, script, false)?;
259 if !status.success() {
260 bail!("command {:?} failed with status {}", script, status);
261 }
262 Ok(())
263 }
264
265 fn run_capture(&mut self, ctx: &CommandContext, script: &str) -> Result<Vec<u8>> {
266 let (out, status) = execute_sync(ctx, script, true)?;
267 if !status.success() {
268 bail!("command {:?} failed with status {}", script, status);
269 }
270 Ok(out)
271 }
272
273 fn spawn_bg(&mut self, ctx: &CommandContext, script: &str) -> Result<Self::Handle> {
274 let plan = plan_background(ctx, script)?;
275 Ok(plan)
276 }
277}
278
279#[cfg(miri)]
280fn execute_sync(
281 ctx: &CommandContext,
282 script: &str,
283 capture: bool,
284) -> Result<(Vec<u8>, ExitStatus)> {
285 let mut stdout = Vec::new();
286 let mut status = exit_status_from_code(0);
287 let resolver = PathResolver::new(
288 ctx.workspace_root().as_path(),
289 ctx.build_context().as_path(),
290 )?;
291
292 let script = normalize_shell(script);
293 for raw in script.split(';') {
294 let cmd = raw.trim();
295 if cmd.is_empty() {
296 continue;
297 }
298 let (action, sleep_dur, exit_code) = parse_command(cmd, ctx, &resolver, capture)?;
299 if sleep_dur > std::time::Duration::ZERO {
300 std::thread::sleep(sleep_dur);
301 }
302 if let Some(action) = action {
303 match action {
304 CommandAction::Write { target, data } => {
305 if let Some(parent) = target.as_path().parent() {
306 let parent_guard = GuardedPath::new(target.root(), parent)?;
307 resolver.create_dir_all(&parent_guard)?;
308 }
309 resolver.write_file(&target, &data)?;
310 }
311 CommandAction::Stdout { data } => {
312 stdout.extend_from_slice(&data);
313 }
314 }
315 }
316 if let Some(code) = exit_code {
317 status = exit_status_from_code(code);
318 break;
319 }
320 }
321
322 Ok((stdout, status))
323}
324
325#[cfg(miri)]
326fn plan_background(ctx: &CommandContext, script: &str) -> Result<SyntheticBgHandle> {
327 let resolver = PathResolver::new(
328 ctx.workspace_root().as_path(),
329 ctx.build_context().as_path(),
330 )?;
331 let mut actions: Vec<Action> = Vec::new();
332 let mut ready = std::time::Duration::ZERO;
333 let mut status = exit_status_from_code(0);
334
335 let script = normalize_shell(script);
336 for raw in script.split(';') {
337 let cmd = raw.trim();
338 if cmd.is_empty() {
339 continue;
340 }
341 let (action, sleep_dur, exit_code) = parse_command(cmd, ctx, &resolver, false)?;
342 ready += sleep_dur;
343 if let Some(CommandAction::Write { target, data }) = action {
344 actions.push(Action::WriteFile { target, data });
345 }
346 if let Some(code) = exit_code {
347 status = exit_status_from_code(code);
348 break;
349 }
350 }
351
352 let min_ready = std::time::Duration::from_millis(50);
353 ready = ready.max(min_ready);
354
355 let handle = SyntheticBgHandle {
356 ctx: ctx.clone(),
357 actions,
358 remaining: ready,
359 last_polled: std::time::Instant::now(),
360 status,
361 applied: false,
362 killed: false,
363 };
364 Ok(handle)
365}
366
367#[cfg(miri)]
368enum CommandAction {
369 Write { target: GuardedPath, data: Vec<u8> },
370 Stdout { data: Vec<u8> },
371}
372
373#[cfg(miri)]
374fn parse_command(
375 cmd: &str,
376 ctx: &CommandContext,
377 resolver: &PathResolver,
378 capture: bool,
379) -> Result<(Option<CommandAction>, std::time::Duration, Option<i32>)> {
380 let (core, redirect) = split_redirect(cmd);
381 let tokens: Vec<&str> = core.split_whitespace().collect();
382 if tokens.is_empty() {
383 return Ok((None, std::time::Duration::ZERO, None));
384 }
385
386 match tokens[0] {
387 "sleep" => {
388 let dur = tokens
389 .get(1)
390 .and_then(|s| s.parse::<f64>().ok())
391 .unwrap_or(0.0);
392 let duration = std::time::Duration::from_secs_f64(dur);
393 Ok((None, duration, None))
394 }
395 "exit" => {
396 let code = tokens
397 .get(1)
398 .and_then(|s| s.parse::<i32>().ok())
399 .unwrap_or(0);
400 Ok((None, std::time::Duration::ZERO, Some(code)))
401 }
402 "printf" => {
403 let body = extract_body(&core, "printf %s");
404 let expanded = expand_env(&body, ctx);
405 let data = expanded.into_bytes();
406 if let Some(path_str) = redirect {
407 let target = resolve_write(resolver, ctx, &path_str)?;
408 Ok((
409 Some(CommandAction::Write { target, data }),
410 std::time::Duration::ZERO,
411 None,
412 ))
413 } else if capture {
414 Ok((
415 Some(CommandAction::Stdout { data }),
416 std::time::Duration::ZERO,
417 None,
418 ))
419 } else {
420 Ok((None, std::time::Duration::ZERO, None))
421 }
422 }
423 "echo" => {
424 let body = core.strip_prefix("echo").unwrap_or("").trim();
425 let expanded = expand_env(body, ctx);
426 let mut data = expanded.into_bytes();
427 data.push(b'\n');
428 if let Some(path_str) = redirect {
429 let target = resolve_write(resolver, ctx, &path_str)?;
430 Ok((
431 Some(CommandAction::Write { target, data }),
432 std::time::Duration::ZERO,
433 None,
434 ))
435 } else if capture {
436 Ok((
437 Some(CommandAction::Stdout { data }),
438 std::time::Duration::ZERO,
439 None,
440 ))
441 } else {
442 Ok((None, std::time::Duration::ZERO, None))
443 }
444 }
445 _ => {
446 Ok((None, std::time::Duration::ZERO, None))
448 }
449 }
450}
451
452#[cfg(miri)]
453fn resolve_write(resolver: &PathResolver, ctx: &CommandContext, path: &str) -> Result<GuardedPath> {
454 match ctx.cwd() {
455 PolicyPath::Guarded(p) => resolver.resolve_write(p, path),
456 PolicyPath::Unguarded(_) => bail!("unguarded writes not supported in Miri"),
457 }
458}
459
460#[cfg(miri)]
461fn split_redirect(cmd: &str) -> (String, Option<String>) {
462 if let Some(idx) = cmd.find('>') {
463 let (left, right) = cmd.split_at(idx);
464 let path = right.trim_start_matches('>').trim();
465 (left.trim().to_string(), Some(path.to_string()))
466 } else {
467 (cmd.trim().to_string(), None)
468 }
469}
470
471#[cfg(miri)]
472fn extract_body(cmd: &str, prefix: &str) -> String {
473 cmd.strip_prefix(prefix)
474 .unwrap_or(cmd)
475 .trim()
476 .trim_matches('"')
477 .to_string()
478}
479
480#[cfg(miri)]
481fn expand_env(input: &str, ctx: &CommandContext) -> String {
482 let mut out = String::new();
483 let mut chars = input.chars().peekable();
484 while let Some(c) = chars.next() {
485 if c == '$' {
486 if let Some(&'{') = chars.peek() {
487 chars.next();
488 let mut name = String::new();
489 while let Some(&ch) = chars.peek() {
490 chars.next();
491 if ch == '}' {
492 break;
493 }
494 name.push(ch);
495 }
496 out.push_str(&env_lookup(&name, ctx));
497 } else {
498 let mut name = String::new();
499 while let Some(&ch) = chars.peek() {
500 if ch.is_ascii_alphanumeric() || ch == '_' {
501 name.push(ch);
502 chars.next();
503 } else {
504 break;
505 }
506 }
507 if name.is_empty() {
508 out.push('$');
509 } else {
510 out.push_str(&env_lookup(&name, ctx));
511 }
512 }
513 } else if c == '%' {
514 let mut name = String::new();
516 while let Some(&ch) = chars.peek() {
517 chars.next();
518 if ch == '%' {
519 break;
520 }
521 name.push(ch);
522 }
523 if name.is_empty() {
524 out.push('%');
525 } else {
526 out.push_str(&env_lookup(&name, ctx));
527 }
528 } else {
529 out.push(c);
530 }
531 }
532 out
533}
534
535#[cfg(miri)]
536fn env_lookup(name: &str, ctx: &CommandContext) -> String {
537 if name == "CARGO_TARGET_DIR" {
538 return ctx.cargo_target_dir().display().to_string();
539 }
540 ctx.envs()
541 .get(name)
542 .cloned()
543 .or_else(|| std::env::var(name).ok())
544 .unwrap_or_default()
545}
546
547#[cfg(miri)]
548fn normalize_shell(script: &str) -> String {
549 let trimmed = script.trim();
550 if let Some(rest) = trimmed.strip_prefix("sh -c ") {
551 return rest.trim_matches(&['"', '\''] as &[_]).to_string();
552 }
553 if let Some(rest) = trimmed.strip_prefix("cmd /C ") {
554 return rest.trim_matches(&['"', '\''] as &[_]).to_string();
555 }
556 trimmed.to_string()
557}
558
559#[cfg(miri)]
560fn apply_actions(ctx: &CommandContext, actions: &[Action]) -> Result<()> {
561 let resolver = PathResolver::new(
562 ctx.workspace_root().as_path(),
563 ctx.build_context().as_path(),
564 )?;
565 for action in actions {
566 match action {
567 Action::WriteFile { target, data } => {
568 if let Some(parent) = target.as_path().parent() {
569 let parent_guard = GuardedPath::new(target.root(), parent)?;
570 resolver.create_dir_all(&parent_guard)?;
571 }
572 resolver.write_file(target, data)?;
573 }
574 }
575 }
576 Ok(())
577}
578
579#[cfg(miri)]
580fn exit_status_from_code(code: i32) -> ExitStatus {
581 #[cfg(unix)]
582 {
583 use std::os::unix::process::ExitStatusExt;
584 ExitStatusExt::from_raw(code << 8)
585 }
586 #[cfg(windows)]
587 {
588 use std::os::windows::process::ExitStatusExt;
589 ExitStatusExt::from_raw(code as u32)
590 }
591}
592
593#[cfg(not(miri))]
594pub type DefaultProcessManager = ShellProcessManager;
595
596#[cfg(miri)]
597pub type DefaultProcessManager = SyntheticProcessManager;
598
599pub fn default_process_manager() -> DefaultProcessManager {
600 DefaultProcessManager::default()
601}
602
603#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
604fn run_cmd(cmd: &mut ProcessCommand) -> Result<()> {
605 let status = cmd
606 .status()
607 .with_context(|| format!("failed to run {:?}", cmd))?;
608 if !status.success() {
609 bail!("command {:?} failed with status {}", cmd, status);
610 }
611 Ok(())
612}
613
614#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
616pub struct CommandBuilder {
617 inner: ProcessCommand,
618 program: OsString,
619 args: Vec<OsString>,
620 cwd: Option<PathBuf>,
621}
622
623#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
624impl CommandBuilder {
625 pub fn new(program: impl AsRef<OsStr>) -> Self {
626 let prog = program.as_ref().to_os_string();
627 Self {
628 inner: ProcessCommand::new(&prog),
629 program: prog,
630 args: Vec::new(),
631 cwd: None,
632 }
633 }
634
635 pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
636 let val = arg.as_ref().to_os_string();
637 self.inner.arg(&val);
638 self.args.push(val);
639 self
640 }
641
642 pub fn args<S, I>(&mut self, args: I) -> &mut Self
643 where
644 S: AsRef<OsStr>,
645 I: IntoIterator<Item = S>,
646 {
647 for arg in args {
648 self.arg(arg);
649 }
650 self
651 }
652
653 pub fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
654 self.inner.env(key, value);
655 self
656 }
657
658 pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
659 self.inner.env_remove(key);
660 self
661 }
662
663 pub fn stdin_file(&mut self, file: File) -> &mut Self {
664 self.inner.stdin(Stdio::from(file));
665 self
666 }
667
668 pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
669 let path = dir.as_ref();
670 self.inner.current_dir(path);
671 self.cwd = Some(path.to_path_buf());
672 self
673 }
674
675 pub fn status(&mut self) -> Result<ExitStatus> {
676 #[cfg(miri)]
677 {
678 let snap = self.snapshot();
679 synthetic_status(&snap)
680 }
681
682 #[cfg(not(miri))]
683 {
684 let desc = format!("{:?}", self.inner);
685 let status = self
686 .inner
687 .status()
688 .with_context(|| format!("failed to run {desc}"))?;
689 Ok(status)
690 }
691 }
692
693 pub fn output(&mut self) -> Result<CommandOutput> {
694 #[cfg(miri)]
695 {
696 let snap = self.snapshot();
697 synthetic_output(&snap)
698 }
699
700 #[cfg(not(miri))]
701 {
702 let desc = format!("{:?}", self.inner);
703 let out = self
704 .inner
705 .output()
706 .with_context(|| format!("failed to run {desc}"))?;
707 Ok(CommandOutput::from(out))
708 }
709 }
710
711 pub fn spawn(&mut self) -> Result<ChildHandle> {
712 #[cfg(miri)]
713 {
714 bail!("spawn is not supported under miri synthetic process backend")
715 }
716
717 #[cfg(not(miri))]
718 {
719 let desc = format!("{:?}", self.inner);
720 let child = self
721 .inner
722 .spawn()
723 .with_context(|| format!("failed to spawn {desc}"))?;
724 Ok(ChildHandle { child })
725 }
726 }
727
728 pub fn snapshot(&self) -> CommandSnapshot {
730 CommandSnapshot {
731 program: self.program.clone(),
732 args: self.args.clone(),
733 cwd: self.cwd.clone(),
734 }
735 }
736}
737
738#[derive(Clone, Debug, PartialEq, Eq)]
739#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
740pub struct CommandSnapshot {
741 pub program: OsString,
742 pub args: Vec<OsString>,
743 pub cwd: Option<PathBuf>,
744}
745
746pub struct CommandOutput {
747 pub status: ExitStatus,
748 pub stdout: Vec<u8>,
749 pub stderr: Vec<u8>,
750}
751
752impl CommandOutput {
753 pub fn success(&self) -> bool {
754 self.status.success()
755 }
756}
757
758#[allow(clippy::disallowed_types)]
759impl From<StdOutput> for CommandOutput {
760 fn from(value: StdOutput) -> Self {
761 Self {
762 status: value.status,
763 stdout: value.stdout,
764 stderr: value.stderr,
765 }
766 }
767}
768
769#[cfg(miri)]
770fn synthetic_status(snapshot: &CommandSnapshot) -> Result<ExitStatus> {
771 Ok(synthetic_output(snapshot)?.status)
772}
773
774#[cfg(miri)]
775fn synthetic_output(snapshot: &CommandSnapshot) -> Result<CommandOutput> {
776 let program = snapshot.program.to_string_lossy().to_string();
777 let args: Vec<String> = snapshot
778 .args
779 .iter()
780 .map(|a| a.to_string_lossy().to_string())
781 .collect();
782
783 if program == "git" {
784 return simulate_git(&args);
785 }
786 if program == "cargo" {
787 return simulate_cargo(&args);
788 }
789
790 Ok(CommandOutput {
791 status: exit_status_from_code(0),
792 stdout: Vec::new(),
793 stderr: Vec::new(),
794 })
795}
796
797#[cfg(miri)]
798fn simulate_git(args: &[String]) -> Result<CommandOutput> {
799 let mut iter = args.iter();
800 if matches!(iter.next(), Some(arg) if arg == "-C") {
801 let _ = iter.next();
802 }
803 let remaining: Vec<String> = iter.map(|s| s.to_string()).collect();
804
805 if remaining.len() >= 2 && remaining[0] == "rev-parse" && remaining[1] == "HEAD" {
806 return Ok(CommandOutput {
807 status: exit_status_from_code(0),
808 stdout: b"HEAD\n".to_vec(),
809 stderr: Vec::new(),
810 });
811 }
812
813 Ok(CommandOutput {
815 status: exit_status_from_code(0),
816 stdout: Vec::new(),
817 stderr: Vec::new(),
818 })
819}
820
821#[cfg(miri)]
822fn simulate_cargo(args: &[String]) -> Result<CommandOutput> {
823 let mut status = exit_status_from_code(0);
825 if args.iter().any(|a| a.contains("build_exit_fail")) {
826 status = exit_status_from_code(1);
827 }
828 Ok(CommandOutput {
829 status,
830 stdout: Vec::new(),
831 stderr: Vec::new(),
832 })
833}