1use crate::process_error::ProcessError;
2use crate::read2;
3
4use anyhow::{Context, Result, bail};
5use jobserver::Client;
6use shell_escape::escape;
7use tempfile::NamedTempFile;
8
9use std::collections::BTreeMap;
10use std::env;
11use std::ffi::{OsStr, OsString};
12use std::fmt;
13use std::io::{self, Write};
14use std::iter::once;
15use std::path::Path;
16use std::process::{Command, ExitStatus, Output};
17
18#[derive(Clone, Debug)]
20pub struct ProcessBuilder {
21 program: OsString,
23 arg0: Option<OsString>,
25 args: Vec<OsString>,
27 env: BTreeMap<String, Option<OsString>>,
29 cwd: Option<OsString>,
31 wrappers: Vec<OsString>,
34 jobserver: Option<Client>,
39 display_env_vars: bool,
41 retry_with_argfile: bool,
44 stdin: Option<Vec<u8>>,
46 stdout: Option<Stdio>,
47 stderr: Option<Stdio>,
48}
49
50impl fmt::Display for ProcessBuilder {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 write!(f, "`")?;
53
54 if self.display_env_vars {
55 for (key, val) in self.env.iter() {
56 if let Some(val) = val {
57 let val = escape(val.to_string_lossy());
58 if cfg!(windows) {
59 write!(f, "set {}={}&& ", key, val)?;
60 } else {
61 write!(f, "{}={} ", key, val)?;
62 }
63 }
64 }
65 }
66
67 write!(f, "{}", self.get_program().to_string_lossy())?;
68
69 for arg in self.get_args() {
70 write!(f, " {}", escape(arg.to_string_lossy()))?;
71 }
72
73 write!(f, "`")
74 }
75}
76
77impl ProcessBuilder {
78 pub fn new<T: AsRef<OsStr>>(cmd: T) -> ProcessBuilder {
80 ProcessBuilder {
81 program: cmd.as_ref().to_os_string(),
82 arg0: None,
83 args: Vec::new(),
84 cwd: None,
85 env: BTreeMap::new(),
86 wrappers: Vec::new(),
87 jobserver: None,
88 display_env_vars: false,
89 retry_with_argfile: false,
90 stdin: None,
91 stdout: None,
92 stderr: None,
93 }
94 }
95
96 pub fn program<T: AsRef<OsStr>>(&mut self, program: T) -> &mut ProcessBuilder {
98 self.program = program.as_ref().to_os_string();
99 self
100 }
101
102 pub fn arg0<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut ProcessBuilder {
104 self.arg0 = Some(arg.as_ref().to_os_string());
105 self
106 }
107
108 pub fn arg<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut ProcessBuilder {
110 self.args.push(arg.as_ref().to_os_string());
111 self
112 }
113
114 pub fn args<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
116 self.args
117 .extend(args.iter().map(|t| t.as_ref().to_os_string()));
118 self
119 }
120
121 pub fn args_replace<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
123 if let Some(program) = self.wrappers.pop() {
124 self.program = program;
128 self.wrappers = Vec::new();
129 }
130 self.args = args.iter().map(|t| t.as_ref().to_os_string()).collect();
131 self
132 }
133
134 pub fn cwd<T: AsRef<OsStr>>(&mut self, path: T) -> &mut ProcessBuilder {
136 self.cwd = Some(path.as_ref().to_os_string());
137 self
138 }
139
140 pub fn env<T: AsRef<OsStr>>(&mut self, key: &str, val: T) -> &mut ProcessBuilder {
142 self.env
143 .insert(key.to_string(), Some(val.as_ref().to_os_string()));
144 self
145 }
146
147 pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder {
149 self.env.insert(key.to_string(), None);
150 self
151 }
152
153 pub fn stdout<T: Into<Stdio>>(&mut self, cfg: T) -> &mut ProcessBuilder {
157 self.stdout = Some(cfg.into());
158 self
159 }
160
161 pub fn stderr<T: Into<Stdio>>(&mut self, cfg: T) -> &mut ProcessBuilder {
165 self.stderr = Some(cfg.into());
166 self
167 }
168
169 pub fn get_program(&self) -> &OsString {
171 self.wrappers.last().unwrap_or(&self.program)
172 }
173
174 pub fn get_arg0(&self) -> Option<&OsStr> {
176 self.arg0.as_deref()
177 }
178
179 pub fn get_args(&self) -> impl Iterator<Item = &OsString> {
181 self.wrappers
182 .iter()
183 .rev()
184 .chain(once(&self.program))
185 .chain(self.args.iter())
186 .skip(1) }
188
189 pub fn get_cwd(&self) -> Option<&Path> {
191 self.cwd.as_ref().map(Path::new)
192 }
193
194 pub fn get_env(&self, var: &str) -> Option<OsString> {
197 self.env
198 .get(var)
199 .cloned()
200 .or_else(|| Some(env::var_os(var)))
201 .and_then(|s| s)
202 }
203
204 pub fn get_envs(&self) -> &BTreeMap<String, Option<OsString>> {
207 &self.env
208 }
209
210 pub fn inherit_jobserver(&mut self, jobserver: &Client) -> &mut Self {
215 self.jobserver = Some(jobserver.clone());
216 self
217 }
218
219 pub fn display_env_vars(&mut self) -> &mut Self {
221 self.display_env_vars = true;
222 self
223 }
224
225 pub fn retry_with_argfile(&mut self, enabled: bool) -> &mut Self {
243 self.retry_with_argfile = enabled;
244 self
245 }
246
247 pub fn stdin<T: Into<Vec<u8>>>(&mut self, stdin: T) -> &mut Self {
249 self.stdin = Some(stdin.into());
250 self
251 }
252
253 fn should_retry_with_argfile(&self, err: &io::Error) -> bool {
254 self.retry_with_argfile && imp::command_line_too_big(err)
255 }
256
257 pub fn status(&self) -> Result<ExitStatus> {
259 self._status()
260 .with_context(|| ProcessError::could_not_execute(self))
261 }
262
263 fn _status(&self) -> io::Result<ExitStatus> {
264 if !debug_force_argfile(self.retry_with_argfile) {
265 let mut cmd = self.build_command();
266 if let Some(stdout) = &self.stdout {
267 cmd.stdout(stdout.to_std());
268 }
269 if let Some(stderr) = &self.stderr {
270 cmd.stderr(stderr.to_std());
271 }
272 match cmd.spawn() {
273 Err(ref e) if self.should_retry_with_argfile(e) => {}
274 Err(e) => return Err(e),
275 Ok(mut child) => return child.wait(),
276 }
277 }
278 let (mut cmd, argfile) = self.build_command_with_argfile()?;
279 if let Some(stdout) = &self.stdout {
280 cmd.stdout(stdout.to_std());
281 }
282 if let Some(stderr) = &self.stderr {
283 cmd.stderr(stderr.to_std());
284 }
285 let status = cmd.spawn()?.wait();
286 close_tempfile_and_log_error(argfile);
287 status
288 }
289
290 pub fn exec(&self) -> Result<()> {
292 let exit = self.status()?;
293 if exit.success() {
294 Ok(())
295 } else {
296 Err(ProcessError::new(
297 &format!("process didn't exit successfully: {}", self),
298 Some(exit),
299 None,
300 )
301 .into())
302 }
303 }
304
305 pub fn exec_replace(&self) -> Result<()> {
321 imp::exec_replace(self)
322 }
323
324 pub fn output(&self) -> Result<Output> {
326 self._output()
327 .with_context(|| ProcessError::could_not_execute(self))
328 }
329
330 fn _output(&self) -> io::Result<Output> {
331 if !debug_force_argfile(self.retry_with_argfile) {
332 let mut cmd = self.build_command();
333 match piped(&mut cmd, self.stdin.is_some()).spawn() {
334 Err(ref e) if self.should_retry_with_argfile(e) => {}
335 Err(e) => return Err(e),
336 Ok(mut child) => {
337 if let Some(stdin) = &self.stdin {
338 child.stdin.take().unwrap().write_all(stdin)?;
339 }
340 return child.wait_with_output();
341 }
342 }
343 }
344 let (mut cmd, argfile) = self.build_command_with_argfile()?;
345 let mut child = piped(&mut cmd, self.stdin.is_some()).spawn()?;
346 if let Some(stdin) = &self.stdin {
347 child.stdin.take().unwrap().write_all(stdin)?;
348 }
349 let output = child.wait_with_output();
350 close_tempfile_and_log_error(argfile);
351 output
352 }
353
354 pub fn exec_with_output(&self) -> Result<Output> {
356 let output = self.output()?;
357 if output.status.success() {
358 Ok(output)
359 } else {
360 Err(ProcessError::new(
361 &format!("process didn't exit successfully: {}", self),
362 Some(output.status),
363 Some(&output),
364 )
365 .into())
366 }
367 }
368
369 pub fn exec_with_streaming(
379 &self,
380 on_stdout_line: &mut dyn FnMut(&str) -> Result<()>,
381 on_stderr_line: &mut dyn FnMut(&str) -> Result<()>,
382 capture_output: bool,
383 ) -> Result<Output> {
384 let mut stdout = Vec::new();
385 let mut stderr = Vec::new();
386
387 let mut callback_error = None;
388 let mut stdout_pos = 0;
389 let mut stderr_pos = 0;
390
391 let spawn = |mut cmd| {
392 if !debug_force_argfile(self.retry_with_argfile) {
393 match piped(&mut cmd, false).spawn() {
394 Err(ref e) if self.should_retry_with_argfile(e) => {}
395 Err(e) => return Err(e),
396 Ok(child) => return Ok((child, None)),
397 }
398 }
399 let (mut cmd, argfile) = self.build_command_with_argfile()?;
400 Ok((piped(&mut cmd, false).spawn()?, Some(argfile)))
401 };
402
403 let status = (|| {
404 let cmd = self.build_command();
405 let (mut child, argfile) = spawn(cmd)?;
406 let out = child.stdout.take().unwrap();
407 let err = child.stderr.take().unwrap();
408 read2(out, err, &mut |is_out, data, eof| {
409 let pos = if is_out {
410 &mut stdout_pos
411 } else {
412 &mut stderr_pos
413 };
414 let idx = if eof {
415 data.len()
416 } else {
417 match data[*pos..].iter().rposition(|b| *b == b'\n') {
418 Some(i) => *pos + i + 1,
419 None => {
420 *pos = data.len();
421 return;
422 }
423 }
424 };
425
426 let new_lines = &data[..idx];
427
428 for line in String::from_utf8_lossy(new_lines).lines() {
429 if callback_error.is_some() {
430 break;
431 }
432 let callback_result = if is_out {
433 on_stdout_line(line)
434 } else {
435 on_stderr_line(line)
436 };
437 if let Err(e) = callback_result {
438 callback_error = Some(e);
439 break;
440 }
441 }
442
443 if capture_output {
444 let dst = if is_out { &mut stdout } else { &mut stderr };
445 dst.extend(new_lines);
446 }
447
448 data.drain(..idx);
449 *pos = 0;
450 })?;
451 let status = child.wait();
452 if let Some(argfile) = argfile {
453 close_tempfile_and_log_error(argfile);
454 }
455 status
456 })()
457 .with_context(|| ProcessError::could_not_execute(self))?;
458 let output = Output {
459 status,
460 stdout,
461 stderr,
462 };
463
464 {
465 let to_print = if capture_output { Some(&output) } else { None };
466 if let Some(e) = callback_error {
467 let cx = ProcessError::new(
468 &format!("failed to parse process output: {}", self),
469 Some(output.status),
470 to_print,
471 );
472 bail!(anyhow::Error::new(cx).context(e));
473 } else if !output.status.success() {
474 bail!(ProcessError::new(
475 &format!("process didn't exit successfully: {}", self),
476 Some(output.status),
477 to_print,
478 ));
479 }
480 }
481
482 Ok(output)
483 }
484
485 fn build_command_with_argfile(&self) -> io::Result<(Command, NamedTempFile)> {
488 use std::io::Write as _;
489
490 let mut tmp = tempfile::Builder::new()
491 .prefix("cargo-argfile.")
492 .tempfile()?;
493
494 let mut arg = OsString::from("@");
495 arg.push(tmp.path());
496 let mut cmd = self.build_command_without_args();
497 cmd.arg(arg);
498 tracing::debug!("created argfile at {} for {self}", tmp.path().display());
499
500 let cap = self.get_args().map(|arg| arg.len() + 1).sum::<usize>();
501 let mut buf = Vec::with_capacity(cap);
502 for arg in &self.args {
503 let arg = arg.to_str().ok_or_else(|| {
504 io::Error::new(
505 io::ErrorKind::Other,
506 format!(
507 "argument for argfile contains invalid UTF-8 characters: `{}`",
508 arg.to_string_lossy()
509 ),
510 )
511 })?;
512 if arg.contains('\n') {
513 return Err(io::Error::new(
514 io::ErrorKind::Other,
515 format!("argument for argfile contains newlines: `{arg}`"),
516 ));
517 }
518 writeln!(buf, "{arg}")?;
519 }
520 tmp.write_all(&mut buf)?;
521 Ok((cmd, tmp))
522 }
523
524 fn build_command_without_args(&self) -> Command {
526 let mut command = {
527 let mut iter = self.wrappers.iter().rev().chain(once(&self.program));
528 let mut cmd = Command::new(iter.next().expect("at least one `program` exists"));
529 cmd.args(iter);
530 cmd
531 };
532 #[cfg(unix)]
533 if let Some(arg0) = self.get_arg0() {
534 use std::os::unix::process::CommandExt as _;
535 command.arg0(arg0);
536 }
537 if let Some(cwd) = self.get_cwd() {
538 command.current_dir(cwd);
539 }
540 for (k, v) in &self.env {
541 match *v {
542 Some(ref v) => {
543 command.env(k, v);
544 }
545 None => {
546 command.env_remove(k);
547 }
548 }
549 }
550 if let Some(ref c) = self.jobserver {
551 c.configure(&mut command);
552 }
553 command
554 }
555
556 pub fn build_command(&self) -> Command {
562 let mut command = self.build_command_without_args();
563 for arg in &self.args {
564 command.arg(arg);
565 }
566 command
567 }
568
569 pub fn wrapped(mut self, wrapper: Option<impl AsRef<OsStr>>) -> Self {
582 if let Some(wrapper) = wrapper.as_ref() {
583 let wrapper = wrapper.as_ref();
584 if !wrapper.is_empty() {
585 self.wrappers.push(wrapper.to_os_string());
586 }
587 }
588 self
589 }
590}
591
592#[derive(Clone, Debug)]
593pub enum Stdio {
594 Piped,
595 Inherit,
596 Null,
597}
598
599impl Stdio {
600 fn to_std(&self) -> std::process::Stdio {
601 match self {
602 Self::Piped => std::process::Stdio::piped(),
603 Self::Inherit => std::process::Stdio::inherit(),
604 Self::Null => std::process::Stdio::null(),
605 }
606 }
607}
608
609fn debug_force_argfile(retry_enabled: bool) -> bool {
613 retry_enabled && env::var("__CARGO_TEST_FORCE_ARGFILE").is_ok()
614}
615
616fn piped(cmd: &mut Command, pipe_stdin: bool) -> &mut Command {
618 use std::process::Stdio;
619
620 cmd.stdout(Stdio::piped())
621 .stderr(Stdio::piped())
622 .stdin(if pipe_stdin {
623 Stdio::piped()
624 } else {
625 Stdio::null()
626 })
627}
628
629fn close_tempfile_and_log_error(file: NamedTempFile) {
630 file.close().unwrap_or_else(|e| {
631 tracing::warn!("failed to close temporary file: {e}");
632 });
633}
634
635#[cfg(unix)]
636mod imp {
637 use super::{ProcessBuilder, ProcessError, close_tempfile_and_log_error, debug_force_argfile};
638 use anyhow::Result;
639 use std::io;
640 use std::os::unix::process::CommandExt;
641
642 pub fn exec_replace(process_builder: &ProcessBuilder) -> Result<()> {
643 let mut error;
644 let mut file = None;
645 if debug_force_argfile(process_builder.retry_with_argfile) {
646 let (mut command, argfile) = process_builder.build_command_with_argfile()?;
647 file = Some(argfile);
648 error = command.exec()
649 } else {
650 let mut command = process_builder.build_command();
651 error = command.exec();
652 if process_builder.should_retry_with_argfile(&error) {
653 let (mut command, argfile) = process_builder.build_command_with_argfile()?;
654 file = Some(argfile);
655 error = command.exec()
656 }
657 }
658 if let Some(file) = file {
659 close_tempfile_and_log_error(file);
660 }
661
662 Err(anyhow::Error::from(error).context(ProcessError::new(
663 &format!("could not execute process {}", process_builder),
664 None,
665 None,
666 )))
667 }
668
669 pub fn command_line_too_big(err: &io::Error) -> bool {
670 err.raw_os_error() == Some(libc::E2BIG)
671 }
672}
673
674#[cfg(windows)]
675mod imp {
676 use super::{ProcessBuilder, ProcessError};
677 use anyhow::Result;
678 use std::io;
679 use windows_sys::Win32::Foundation::{FALSE, TRUE};
680 use windows_sys::Win32::System::Console::SetConsoleCtrlHandler;
681 use windows_sys::core::BOOL;
682
683 unsafe extern "system" fn ctrlc_handler(_: u32) -> BOOL {
684 TRUE
686 }
687
688 pub fn exec_replace(process_builder: &ProcessBuilder) -> Result<()> {
689 unsafe {
690 if SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) == FALSE {
691 return Err(ProcessError::new("Could not set Ctrl-C handler.", None, None).into());
692 }
693 }
694
695 process_builder.exec()
697 }
698
699 pub fn command_line_too_big(err: &io::Error) -> bool {
700 use windows_sys::Win32::Foundation::ERROR_FILENAME_EXCED_RANGE;
701 err.raw_os_error() == Some(ERROR_FILENAME_EXCED_RANGE as i32)
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use super::ProcessBuilder;
708 use std::fs;
709
710 #[test]
711 fn argfile_build_succeeds() {
712 let mut cmd = ProcessBuilder::new("echo");
713 cmd.args(["foo", "bar"].as_slice());
714 let (cmd, argfile) = cmd.build_command_with_argfile().unwrap();
715
716 assert_eq!(cmd.get_program(), "echo");
717 let cmd_args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
718 assert_eq!(cmd_args.len(), 1);
719 assert!(cmd_args[0].starts_with("@"));
720 assert!(cmd_args[0].contains("cargo-argfile."));
721
722 let buf = fs::read_to_string(argfile.path()).unwrap();
723 assert_eq!(buf, "foo\nbar\n");
724 }
725
726 #[test]
727 fn argfile_build_fails_if_arg_contains_newline() {
728 let mut cmd = ProcessBuilder::new("echo");
729 cmd.arg("foo\n");
730 let err = cmd.build_command_with_argfile().unwrap_err();
731 assert_eq!(
732 err.to_string(),
733 "argument for argfile contains newlines: `foo\n`"
734 );
735 }
736
737 #[test]
738 fn argfile_build_fails_if_arg_contains_invalid_utf8() {
739 let mut cmd = ProcessBuilder::new("echo");
740
741 #[cfg(windows)]
742 let invalid_arg = {
743 use std::os::windows::prelude::*;
744 std::ffi::OsString::from_wide(&[0x0066, 0x006f, 0xD800, 0x006f])
745 };
746
747 #[cfg(unix)]
748 let invalid_arg = {
749 use std::os::unix::ffi::OsStrExt;
750 std::ffi::OsStr::from_bytes(&[0x66, 0x6f, 0x80, 0x6f]).to_os_string()
751 };
752
753 cmd.arg(invalid_arg);
754 let err = cmd.build_command_with_argfile().unwrap_err();
755 assert_eq!(
756 err.to_string(),
757 "argument for argfile contains invalid UTF-8 characters: `fo�o`"
758 );
759 }
760}