cargo_util/
process_builder.rs

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/// A builder object for an external process, similar to [`std::process::Command`].
19#[derive(Clone, Debug)]
20pub struct ProcessBuilder {
21    /// The program to execute.
22    program: OsString,
23    /// A list of arguments to pass to the program.
24    args: Vec<OsString>,
25    /// Any environment variables that should be set for the program.
26    env: BTreeMap<String, Option<OsString>>,
27    /// The directory to run the program from.
28    cwd: Option<OsString>,
29    /// A list of wrappers that wrap the original program when calling
30    /// [`ProcessBuilder::wrapped`]. The last one is the outermost one.
31    wrappers: Vec<OsString>,
32    /// The `make` jobserver. See the [jobserver crate] for
33    /// more information.
34    ///
35    /// [jobserver crate]: https://docs.rs/jobserver/
36    jobserver: Option<Client>,
37    /// `true` to include environment variable in display.
38    display_env_vars: bool,
39    /// `true` to retry with an argfile if hitting "command line too big" error.
40    /// See [`ProcessBuilder::retry_with_argfile`] for more information.
41    retry_with_argfile: bool,
42    /// Data to write to stdin.
43    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    /// Creates a new [`ProcessBuilder`] with the given executable path.
75    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    /// (chainable) Sets the executable for the process.
90    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    /// (chainable) Adds `arg` to the args list.
96    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    /// (chainable) Adds multiple `args` to the args list.
102    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    /// (chainable) Replaces the args list with the given `args`.
109    pub fn args_replace<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
110        if let Some(program) = self.wrappers.pop() {
111            // User intend to replace all args, so we
112            // - use the outermost wrapper as the main program, and
113            // - cleanup other inner wrappers.
114            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    /// (chainable) Sets the current working directory of the process.
122    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    /// (chainable) Sets an environment variable for the process.
128    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    /// (chainable) Unsets an environment variable for the process.
135    pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder {
136        self.env.insert(key.to_string(), None);
137        self
138    }
139
140    /// Gets the executable name.
141    pub fn get_program(&self) -> &OsString {
142        self.wrappers.last().unwrap_or(&self.program)
143    }
144
145    /// Gets the program arguments.
146    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) // Skip the main `program
153    }
154
155    /// Gets the current working directory for the process.
156    pub fn get_cwd(&self) -> Option<&Path> {
157        self.cwd.as_ref().map(Path::new)
158    }
159
160    /// Gets an environment variable as the process will see it (will inherit from environment
161    /// unless explicitally unset).
162    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    /// Gets all environment variables explicitly set or unset for the process (not inherited
171    /// vars).
172    pub fn get_envs(&self) -> &BTreeMap<String, Option<OsString>> {
173        &self.env
174    }
175
176    /// Sets the `make` jobserver. See the [jobserver crate][jobserver_docs] for
177    /// more information.
178    ///
179    /// [jobserver_docs]: https://docs.rs/jobserver/latest/jobserver/
180    pub fn inherit_jobserver(&mut self, jobserver: &Client) -> &mut Self {
181        self.jobserver = Some(jobserver.clone());
182        self
183    }
184
185    /// Enables environment variable display.
186    pub fn display_env_vars(&mut self) -> &mut Self {
187        self.display_env_vars = true;
188        self
189    }
190
191    /// Enables retrying with an argfile if hitting "command line too big" error
192    ///
193    /// This is primarily for the `@path` arg of rustc and rustdoc, which treat
194    /// each line as an command-line argument, so `LF` and `CRLF` bytes are not
195    /// valid as an argument for argfile at this moment.
196    /// For example, `RUSTDOCFLAGS="--crate-version foo\nbar" cargo doc` is
197    /// valid when invoking from command-line but not from argfile.
198    ///
199    /// To sum up, the limitations of the argfile are:
200    ///
201    /// - Must be valid UTF-8 encoded.
202    /// - Must not contain any newlines in each argument.
203    ///
204    /// Ref:
205    ///
206    /// - <https://doc.rust-lang.org/rustdoc/command-line-arguments.html#path-load-command-line-flags-from-a-path>
207    /// - <https://doc.rust-lang.org/rustc/command-line-arguments.html#path-load-command-line-flags-from-a-path>
208    pub fn retry_with_argfile(&mut self, enabled: bool) -> &mut Self {
209        self.retry_with_argfile = enabled;
210        self
211    }
212
213    /// Sets a value that will be written to stdin of the process on launch.
214    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    /// Like [`Command::status`] but with a better error message.
224    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    /// Runs the process, waiting for completion, and mapping non-success exit codes to an error.
245    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    /// Replaces the current process with the target process.
260    ///
261    /// On Unix, this executes the process using the Unix syscall `execvp`, which will block
262    /// this process, and will only return if there is an error.
263    ///
264    /// On Windows this isn't technically possible. Instead we emulate it to the best of our
265    /// ability. One aspect we fix here is that we specify a handler for the Ctrl-C handler.
266    /// In doing so (and by effectively ignoring it) we should emulate proxying Ctrl-C
267    /// handling to the application at hand, which will either terminate or handle it itself.
268    /// According to Microsoft's documentation at
269    /// <https://docs.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals>.
270    /// the Ctrl-C signal is sent to all processes attached to a terminal, which should
271    /// include our child process. If the child terminates then we'll reap them in Cargo
272    /// pretty quickly, and if the child handles the signal then we won't terminate
273    /// (and we shouldn't!) until the process itself later exits.
274    pub fn exec_replace(&self) -> Result<()> {
275        imp::exec_replace(self)
276    }
277
278    /// Like [`Command::output`] but with a better error message.
279    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    /// Executes the process, returning the stdio output, or an error if non-zero exit status.
309    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    /// Executes a command, passing each line of stdout and stderr to the supplied callbacks, which
324    /// can mutate the string data.
325    ///
326    /// If any invocations of these function return an error, it will be propagated.
327    ///
328    /// If `capture_output` is true, then all the output will also be buffered
329    /// and stored in the returned `Output` object. If it is false, no caching
330    /// is done, and the callbacks are solely responsible for handling the
331    /// output.
332    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    /// Builds the command with an `@<path>` argfile that contains all the
440    /// arguments. This is primarily served for rustc/rustdoc command family.
441    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    /// Builds a command from `ProcessBuilder` for everything but not `args`.
479    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    /// Converts `ProcessBuilder` into a `std::process::Command`, and handles
506    /// the jobserver, if present.
507    ///
508    /// Note that this method doesn't take argfile fallback into account. The
509    /// caller should handle it by themselves.
510    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    /// Wraps an existing command with the provided wrapper, if it is present and valid.
519    ///
520    /// # Examples
521    ///
522    /// ```rust
523    /// use cargo_util::ProcessBuilder;
524    /// // Running this would execute `rustc`
525    /// let cmd = ProcessBuilder::new("rustc");
526    ///
527    /// // Running this will execute `sccache rustc`
528    /// let cmd = cmd.wrapped(Some("sccache"));
529    /// ```
530    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
541/// Forces the command to use `@path` argfile.
542///
543/// You should set `__CARGO_TEST_FORCE_ARGFILE` to enable this.
544fn debug_force_argfile(retry_enabled: bool) -> bool {
545    cfg!(debug_assertions) && env::var("__CARGO_TEST_FORCE_ARGFILE").is_ok() && retry_enabled
546}
547
548/// Creates new pipes for stderr, stdout, and optionally stdin.
549fn 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        // Do nothing; let the child process handle it.
614        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        // Just execute the process as normal.
625        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}