#
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 explicitly 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}