1use crate::process_error::ProcessError;
2use crate::read2;
3
4use anyhow::{bail, Context, Result};
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, Stdio};
17
18#[derive(Clone, Debug)]
20pub struct ProcessBuilder {
21 program: OsString,
23 args: Vec<OsString>,
25 env: BTreeMap<String, Option<OsString>>,
27 cwd: Option<OsString>,
29 wrappers: Vec<OsString>,
32 jobserver: Option<Client>,
37 display_env_vars: bool,
39 retry_with_argfile: bool,
42 stdin: Option<Vec<u8>>,
44}
45
46impl fmt::Display for ProcessBuilder {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "`")?;
49
50 if self.display_env_vars {
51 for (key, val) in self.env.iter() {
52 if let Some(val) = val {
53 let val = escape(val.to_string_lossy());
54 if cfg!(windows) {
55 write!(f, "set {}={}&& ", key, val)?;
56 } else {
57 write!(f, "{}={} ", key, val)?;
58 }
59 }
60 }
61 }
62
63 write!(f, "{}", self.get_program().to_string_lossy())?;
64
65 for arg in self.get_args() {
66 write!(f, " {}", escape(arg.to_string_lossy()))?;
67 }
68
69 write!(f, "`")
70 }
71}
72
73impl ProcessBuilder {
74 pub fn new<T: AsRef<OsStr>>(cmd: T) -> ProcessBuilder {
76 ProcessBuilder {
77 program: cmd.as_ref().to_os_string(),
78 args: Vec::new(),
79 cwd: None,
80 env: BTreeMap::new(),
81 wrappers: Vec::new(),
82 jobserver: None,
83 display_env_vars: false,
84 retry_with_argfile: false,
85 stdin: None,
86 }
87 }
88
89 pub fn program<T: AsRef<OsStr>>(&mut self, program: T) -> &mut ProcessBuilder {
91 self.program = program.as_ref().to_os_string();
92 self
93 }
94
95 pub fn arg<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut ProcessBuilder {
97 self.args.push(arg.as_ref().to_os_string());
98 self
99 }
100
101 pub fn args<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
103 self.args
104 .extend(args.iter().map(|t| t.as_ref().to_os_string()));
105 self
106 }
107
108 pub fn args_replace<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
110 if let Some(program) = self.wrappers.pop() {
111 self.program = program;
115 self.wrappers = Vec::new();
116 }
117 self.args = args.iter().map(|t| t.as_ref().to_os_string()).collect();
118 self
119 }
120
121 pub fn cwd<T: AsRef<OsStr>>(&mut self, path: T) -> &mut ProcessBuilder {
123 self.cwd = Some(path.as_ref().to_os_string());
124 self
125 }
126
127 pub fn env<T: AsRef<OsStr>>(&mut self, key: &str, val: T) -> &mut ProcessBuilder {
129 self.env
130 .insert(key.to_string(), Some(val.as_ref().to_os_string()));
131 self
132 }
133
134 pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder {
136 self.env.insert(key.to_string(), None);
137 self
138 }
139
140 pub fn get_program(&self) -> &OsString {
142 self.wrappers.last().unwrap_or(&self.program)
143 }
144
145 pub fn get_args(&self) -> impl Iterator<Item = &OsString> {
147 self.wrappers
148 .iter()
149 .rev()
150 .chain(once(&self.program))
151 .chain(self.args.iter())
152 .skip(1) }
154
155 pub fn get_cwd(&self) -> Option<&Path> {
157 self.cwd.as_ref().map(Path::new)
158 }
159
160 pub fn get_env(&self, var: &str) -> Option<OsString> {
163 self.env
164 .get(var)
165 .cloned()
166 .or_else(|| Some(env::var_os(var)))
167 .and_then(|s| s)
168 }
169
170 pub fn get_envs(&self) -> &BTreeMap<String, Option<OsString>> {
173 &self.env
174 }
175
176 pub fn inherit_jobserver(&mut self, jobserver: &Client) -> &mut Self {
181 self.jobserver = Some(jobserver.clone());
182 self
183 }
184
185 pub fn display_env_vars(&mut self) -> &mut Self {
187 self.display_env_vars = true;
188 self
189 }
190
191 pub fn retry_with_argfile(&mut self, enabled: bool) -> &mut Self {
209 self.retry_with_argfile = enabled;
210 self
211 }
212
213 pub fn stdin<T: Into<Vec<u8>>>(&mut self, stdin: T) -> &mut Self {
215 self.stdin = Some(stdin.into());
216 self
217 }
218
219 fn should_retry_with_argfile(&self, err: &io::Error) -> bool {
220 self.retry_with_argfile && imp::command_line_too_big(err)
221 }
222
223 pub fn status(&self) -> Result<ExitStatus> {
225 self._status()
226 .with_context(|| ProcessError::could_not_execute(self))
227 }
228
229 fn _status(&self) -> io::Result<ExitStatus> {
230 if !debug_force_argfile(self.retry_with_argfile) {
231 let mut cmd = self.build_command();
232 match cmd.spawn() {
233 Err(ref e) if self.should_retry_with_argfile(e) => {}
234 Err(e) => return Err(e),
235 Ok(mut child) => return child.wait(),
236 }
237 }
238 let (mut cmd, argfile) = self.build_command_with_argfile()?;
239 let status = cmd.spawn()?.wait();
240 close_tempfile_and_log_error(argfile);
241 status
242 }
243
244 pub fn exec(&self) -> Result<()> {
246 let exit = self.status()?;
247 if exit.success() {
248 Ok(())
249 } else {
250 Err(ProcessError::new(
251 &format!("process didn't exit successfully: {}", self),
252 Some(exit),
253 None,
254 )
255 .into())
256 }
257 }
258
259 pub fn exec_replace(&self) -> Result<()> {
275 imp::exec_replace(self)
276 }
277
278 pub fn output(&self) -> Result<Output> {
280 self._output()
281 .with_context(|| ProcessError::could_not_execute(self))
282 }
283
284 fn _output(&self) -> io::Result<Output> {
285 if !debug_force_argfile(self.retry_with_argfile) {
286 let mut cmd = self.build_command();
287 match piped(&mut cmd, self.stdin.is_some()).spawn() {
288 Err(ref e) if self.should_retry_with_argfile(e) => {}
289 Err(e) => return Err(e),
290 Ok(mut child) => {
291 if let Some(stdin) = &self.stdin {
292 child.stdin.take().unwrap().write_all(stdin)?;
293 }
294 return child.wait_with_output();
295 }
296 }
297 }
298 let (mut cmd, argfile) = self.build_command_with_argfile()?;
299 let mut child = piped(&mut cmd, self.stdin.is_some()).spawn()?;
300 if let Some(stdin) = &self.stdin {
301 child.stdin.take().unwrap().write_all(stdin)?;
302 }
303 let output = child.wait_with_output();
304 close_tempfile_and_log_error(argfile);
305 output
306 }
307
308 pub fn exec_with_output(&self) -> Result<Output> {
310 let output = self.output()?;
311 if output.status.success() {
312 Ok(output)
313 } else {
314 Err(ProcessError::new(
315 &format!("process didn't exit successfully: {}", self),
316 Some(output.status),
317 Some(&output),
318 )
319 .into())
320 }
321 }
322
323 pub fn exec_with_streaming(
333 &self,
334 on_stdout_line: &mut dyn FnMut(&str) -> Result<()>,
335 on_stderr_line: &mut dyn FnMut(&str) -> Result<()>,
336 capture_output: bool,
337 ) -> Result<Output> {
338 let mut stdout = Vec::new();
339 let mut stderr = Vec::new();
340
341 let mut callback_error = None;
342 let mut stdout_pos = 0;
343 let mut stderr_pos = 0;
344
345 let spawn = |mut cmd| {
346 if !debug_force_argfile(self.retry_with_argfile) {
347 match piped(&mut cmd, false).spawn() {
348 Err(ref e) if self.should_retry_with_argfile(e) => {}
349 Err(e) => return Err(e),
350 Ok(child) => return Ok((child, None)),
351 }
352 }
353 let (mut cmd, argfile) = self.build_command_with_argfile()?;
354 Ok((piped(&mut cmd, false).spawn()?, Some(argfile)))
355 };
356
357 let status = (|| {
358 let cmd = self.build_command();
359 let (mut child, argfile) = spawn(cmd)?;
360 let out = child.stdout.take().unwrap();
361 let err = child.stderr.take().unwrap();
362 read2(out, err, &mut |is_out, data, eof| {
363 let pos = if is_out {
364 &mut stdout_pos
365 } else {
366 &mut stderr_pos
367 };
368 let idx = if eof {
369 data.len()
370 } else {
371 match data[*pos..].iter().rposition(|b| *b == b'\n') {
372 Some(i) => *pos + i + 1,
373 None => {
374 *pos = data.len();
375 return;
376 }
377 }
378 };
379
380 let new_lines = &data[..idx];
381
382 for line in String::from_utf8_lossy(new_lines).lines() {
383 if callback_error.is_some() {
384 break;
385 }
386 let callback_result = if is_out {
387 on_stdout_line(line)
388 } else {
389 on_stderr_line(line)
390 };
391 if let Err(e) = callback_result {
392 callback_error = Some(e);
393 break;
394 }
395 }
396
397 if capture_output {
398 let dst = if is_out { &mut stdout } else { &mut stderr };
399 dst.extend(new_lines);
400 }
401
402 data.drain(..idx);
403 *pos = 0;
404 })?;
405 let status = child.wait();
406 if let Some(argfile) = argfile {
407 close_tempfile_and_log_error(argfile);
408 }
409 status
410 })()
411 .with_context(|| ProcessError::could_not_execute(self))?;
412 let output = Output {
413 status,
414 stdout,
415 stderr,
416 };
417
418 {
419 let to_print = if capture_output { Some(&output) } else { None };
420 if let Some(e) = callback_error {
421 let cx = ProcessError::new(
422 &format!("failed to parse process output: {}", self),
423 Some(output.status),
424 to_print,
425 );
426 bail!(anyhow::Error::new(cx).context(e));
427 } else if !output.status.success() {
428 bail!(ProcessError::new(
429 &format!("process didn't exit successfully: {}", self),
430 Some(output.status),
431 to_print,
432 ));
433 }
434 }
435
436 Ok(output)
437 }
438
439 fn build_command_with_argfile(&self) -> io::Result<(Command, NamedTempFile)> {
442 use std::io::Write as _;
443
444 let mut tmp = tempfile::Builder::new()
445 .prefix("cargo-argfile.")
446 .tempfile()?;
447
448 let mut arg = OsString::from("@");
449 arg.push(tmp.path());
450 let mut cmd = self.build_command_without_args();
451 cmd.arg(arg);
452 tracing::debug!("created argfile at {} for {self}", tmp.path().display());
453
454 let cap = self.get_args().map(|arg| arg.len() + 1).sum::<usize>();
455 let mut buf = Vec::with_capacity(cap);
456 for arg in &self.args {
457 let arg = arg.to_str().ok_or_else(|| {
458 io::Error::new(
459 io::ErrorKind::Other,
460 format!(
461 "argument for argfile contains invalid UTF-8 characters: `{}`",
462 arg.to_string_lossy()
463 ),
464 )
465 })?;
466 if arg.contains('\n') {
467 return Err(io::Error::new(
468 io::ErrorKind::Other,
469 format!("argument for argfile contains newlines: `{arg}`"),
470 ));
471 }
472 writeln!(buf, "{arg}")?;
473 }
474 tmp.write_all(&mut buf)?;
475 Ok((cmd, tmp))
476 }
477
478 fn build_command_without_args(&self) -> Command {
480 let mut command = {
481 let mut iter = self.wrappers.iter().rev().chain(once(&self.program));
482 let mut cmd = Command::new(iter.next().expect("at least one `program` exists"));
483 cmd.args(iter);
484 cmd
485 };
486 if let Some(cwd) = self.get_cwd() {
487 command.current_dir(cwd);
488 }
489 for (k, v) in &self.env {
490 match *v {
491 Some(ref v) => {
492 command.env(k, v);
493 }
494 None => {
495 command.env_remove(k);
496 }
497 }
498 }
499 if let Some(ref c) = self.jobserver {
500 c.configure(&mut command);
501 }
502 command
503 }
504
505 pub fn build_command(&self) -> Command {
511 let mut command = self.build_command_without_args();
512 for arg in &self.args {
513 command.arg(arg);
514 }
515 command
516 }
517
518 pub fn wrapped(mut self, wrapper: Option<impl AsRef<OsStr>>) -> Self {
531 if let Some(wrapper) = wrapper.as_ref() {
532 let wrapper = wrapper.as_ref();
533 if !wrapper.is_empty() {
534 self.wrappers.push(wrapper.to_os_string());
535 }
536 }
537 self
538 }
539}
540
541fn debug_force_argfile(retry_enabled: bool) -> bool {
545 cfg!(debug_assertions) && env::var("__CARGO_TEST_FORCE_ARGFILE").is_ok() && retry_enabled
546}
547
548fn piped(cmd: &mut Command, pipe_stdin: bool) -> &mut Command {
550 cmd.stdout(Stdio::piped())
551 .stderr(Stdio::piped())
552 .stdin(if pipe_stdin {
553 Stdio::piped()
554 } else {
555 Stdio::null()
556 })
557}
558
559fn close_tempfile_and_log_error(file: NamedTempFile) {
560 file.close().unwrap_or_else(|e| {
561 tracing::warn!("failed to close temporary file: {e}");
562 });
563}
564
565#[cfg(unix)]
566mod imp {
567 use super::{close_tempfile_and_log_error, debug_force_argfile, ProcessBuilder, ProcessError};
568 use anyhow::Result;
569 use std::io;
570 use std::os::unix::process::CommandExt;
571
572 pub fn exec_replace(process_builder: &ProcessBuilder) -> Result<()> {
573 let mut error;
574 let mut file = None;
575 if debug_force_argfile(process_builder.retry_with_argfile) {
576 let (mut command, argfile) = process_builder.build_command_with_argfile()?;
577 file = Some(argfile);
578 error = command.exec()
579 } else {
580 let mut command = process_builder.build_command();
581 error = command.exec();
582 if process_builder.should_retry_with_argfile(&error) {
583 let (mut command, argfile) = process_builder.build_command_with_argfile()?;
584 file = Some(argfile);
585 error = command.exec()
586 }
587 }
588 if let Some(file) = file {
589 close_tempfile_and_log_error(file);
590 }
591
592 Err(anyhow::Error::from(error).context(ProcessError::new(
593 &format!("could not execute process {}", process_builder),
594 None,
595 None,
596 )))
597 }
598
599 pub fn command_line_too_big(err: &io::Error) -> bool {
600 err.raw_os_error() == Some(libc::E2BIG)
601 }
602}
603
604#[cfg(windows)]
605mod imp {
606 use super::{ProcessBuilder, ProcessError};
607 use anyhow::Result;
608 use std::io;
609 use windows_sys::Win32::Foundation::{BOOL, FALSE, TRUE};
610 use windows_sys::Win32::System::Console::SetConsoleCtrlHandler;
611
612 unsafe extern "system" fn ctrlc_handler(_: u32) -> BOOL {
613 TRUE
615 }
616
617 pub fn exec_replace(process_builder: &ProcessBuilder) -> Result<()> {
618 unsafe {
619 if SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) == FALSE {
620 return Err(ProcessError::new("Could not set Ctrl-C handler.", None, None).into());
621 }
622 }
623
624 process_builder.exec()
626 }
627
628 pub fn command_line_too_big(err: &io::Error) -> bool {
629 use windows_sys::Win32::Foundation::ERROR_FILENAME_EXCED_RANGE;
630 err.raw_os_error() == Some(ERROR_FILENAME_EXCED_RANGE as i32)
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use super::ProcessBuilder;
637 use std::fs;
638
639 #[test]
640 fn argfile_build_succeeds() {
641 let mut cmd = ProcessBuilder::new("echo");
642 cmd.args(["foo", "bar"].as_slice());
643 let (cmd, argfile) = cmd.build_command_with_argfile().unwrap();
644
645 assert_eq!(cmd.get_program(), "echo");
646 let cmd_args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
647 assert_eq!(cmd_args.len(), 1);
648 assert!(cmd_args[0].starts_with("@"));
649 assert!(cmd_args[0].contains("cargo-argfile."));
650
651 let buf = fs::read_to_string(argfile.path()).unwrap();
652 assert_eq!(buf, "foo\nbar\n");
653 }
654
655 #[test]
656 fn argfile_build_fails_if_arg_contains_newline() {
657 let mut cmd = ProcessBuilder::new("echo");
658 cmd.arg("foo\n");
659 let err = cmd.build_command_with_argfile().unwrap_err();
660 assert_eq!(
661 err.to_string(),
662 "argument for argfile contains newlines: `foo\n`"
663 );
664 }
665
666 #[test]
667 fn argfile_build_fails_if_arg_contains_invalid_utf8() {
668 let mut cmd = ProcessBuilder::new("echo");
669
670 #[cfg(windows)]
671 let invalid_arg = {
672 use std::os::windows::prelude::*;
673 std::ffi::OsString::from_wide(&[0x0066, 0x006f, 0xD800, 0x006f])
674 };
675
676 #[cfg(unix)]
677 let invalid_arg = {
678 use std::os::unix::ffi::OsStrExt;
679 std::ffi::OsStr::from_bytes(&[0x66, 0x6f, 0x80, 0x6f]).to_os_string()
680 };
681
682 cmd.arg(invalid_arg);
683 let err = cmd.build_command_with_argfile().unwrap_err();
684 assert_eq!(
685 err.to_string(),
686 "argument for argfile contains invalid UTF-8 characters: `fo�o`"
687 );
688 }
689}