Skip to main content

cargo_util/
process_builder.rs

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/// 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    /// Best-effort replacement for arg0
24    arg0: Option<OsString>,
25    /// A list of arguments to pass to the program.
26    args: Vec<OsString>,
27    /// Any environment variables that should be set for the program.
28    env: BTreeMap<String, Option<OsString>>,
29    /// The directory to run the program from.
30    cwd: Option<OsString>,
31    /// A list of wrappers that wrap the original program when calling
32    /// [`ProcessBuilder::wrapped`]. The last one is the outermost one.
33    wrappers: Vec<OsString>,
34    /// The `make` jobserver. See the [jobserver crate] for
35    /// more information.
36    ///
37    /// [jobserver crate]: https://docs.rs/jobserver/
38    jobserver: Option<Client>,
39    /// `true` to include environment variable in display.
40    display_env_vars: bool,
41    /// `true` to retry with an argfile if hitting "command line too big" error.
42    /// See [`ProcessBuilder::retry_with_argfile`] for more information.
43    retry_with_argfile: bool,
44    /// Data to write to stdin.
45    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    /// Creates a new [`ProcessBuilder`] with the given executable path.
79    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    /// (chainable) Sets the executable for the process.
97    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    /// (chainable) Overrides `arg0` for this program.
103    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    /// (chainable) Adds `arg` to the args list.
109    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    /// (chainable) Adds multiple `args` to the args list.
115    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    /// (chainable) Replaces the args list with the given `args`.
122    pub fn args_replace<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
123        if let Some(program) = self.wrappers.pop() {
124            // User intend to replace all args, so we
125            // - use the outermost wrapper as the main program, and
126            // - cleanup other inner wrappers.
127            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    /// (chainable) Sets the current working directory of the process.
135    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    /// (chainable) Sets an environment variable for the process.
141    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    /// (chainable) Unsets an environment variable for the process.
148    pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder {
149        self.env.insert(key.to_string(), None);
150        self
151    }
152
153    /// (chainable) Configure the process's stdout handle
154    ///
155    /// Only applies when used with [`Self::status`] and [`Self::exec`]
156    pub fn stdout<T: Into<Stdio>>(&mut self, cfg: T) -> &mut ProcessBuilder {
157        self.stdout = Some(cfg.into());
158        self
159    }
160
161    /// (chainable) Configure the process's stderr handle
162    ///
163    /// Only applies when used with [`Self::status`] and [`Self::exec`]
164    pub fn stderr<T: Into<Stdio>>(&mut self, cfg: T) -> &mut ProcessBuilder {
165        self.stderr = Some(cfg.into());
166        self
167    }
168
169    /// Gets the executable name.
170    pub fn get_program(&self) -> &OsString {
171        self.wrappers.last().unwrap_or(&self.program)
172    }
173
174    /// Gets the program arg0.
175    pub fn get_arg0(&self) -> Option<&OsStr> {
176        self.arg0.as_deref()
177    }
178
179    /// Gets the program arguments.
180    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) // Skip the main `program
187    }
188
189    /// Gets the current working directory for the process.
190    pub fn get_cwd(&self) -> Option<&Path> {
191        self.cwd.as_ref().map(Path::new)
192    }
193
194    /// Gets an environment variable as the process will see it (will inherit from environment
195    /// unless explicitly unset).
196    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    /// Gets all environment variables explicitly set or unset for the process (not inherited
205    /// vars).
206    pub fn get_envs(&self) -> &BTreeMap<String, Option<OsString>> {
207        &self.env
208    }
209
210    /// Sets the `make` jobserver. See the [jobserver crate][jobserver_docs] for
211    /// more information.
212    ///
213    /// [jobserver_docs]: https://docs.rs/jobserver/latest/jobserver/
214    pub fn inherit_jobserver(&mut self, jobserver: &Client) -> &mut Self {
215        self.jobserver = Some(jobserver.clone());
216        self
217    }
218
219    /// Enables environment variable display.
220    pub fn display_env_vars(&mut self) -> &mut Self {
221        self.display_env_vars = true;
222        self
223    }
224
225    /// Enables retrying with an argfile if hitting "command line too big" error
226    ///
227    /// This is primarily for the `@path` arg of rustc and rustdoc, which treat
228    /// each line as an command-line argument, so `LF` and `CRLF` bytes are not
229    /// valid as an argument for argfile at this moment.
230    /// For example, `RUSTDOCFLAGS="--crate-version foo\nbar" cargo doc` is
231    /// valid when invoking from command-line but not from argfile.
232    ///
233    /// To sum up, the limitations of the argfile are:
234    ///
235    /// - Must be valid UTF-8 encoded.
236    /// - Must not contain any newlines in each argument.
237    ///
238    /// Ref:
239    ///
240    /// - <https://doc.rust-lang.org/rustdoc/command-line-arguments.html#path-load-command-line-flags-from-a-path>
241    /// - <https://doc.rust-lang.org/rustc/command-line-arguments.html#path-load-command-line-flags-from-a-path>
242    pub fn retry_with_argfile(&mut self, enabled: bool) -> &mut Self {
243        self.retry_with_argfile = enabled;
244        self
245    }
246
247    /// Sets a value that will be written to stdin of the process on launch.
248    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    /// Like [`Command::status`] but with a better error message.
258    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    /// Runs the process, waiting for completion, and mapping non-success exit codes to an error.
291    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    /// Replaces the current process with the target process.
306    ///
307    /// On Unix, this executes the process using the Unix syscall `execvp`, which will block
308    /// this process, and will only return if there is an error.
309    ///
310    /// On Windows this isn't technically possible. Instead we emulate it to the best of our
311    /// ability. One aspect we fix here is that we specify a handler for the Ctrl-C handler.
312    /// In doing so (and by effectively ignoring it) we should emulate proxying Ctrl-C
313    /// handling to the application at hand, which will either terminate or handle it itself.
314    /// According to Microsoft's documentation at
315    /// <https://docs.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals>.
316    /// the Ctrl-C signal is sent to all processes attached to a terminal, which should
317    /// include our child process. If the child terminates then we'll reap them in Cargo
318    /// pretty quickly, and if the child handles the signal then we won't terminate
319    /// (and we shouldn't!) until the process itself later exits.
320    pub fn exec_replace(&self) -> Result<()> {
321        imp::exec_replace(self)
322    }
323
324    /// Like [`Command::output`] but with a better error message.
325    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    /// Executes the process, returning the stdio output, or an error if non-zero exit status.
355    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    /// Executes a command, passing each line of stdout and stderr to the supplied callbacks, which
370    /// can mutate the string data.
371    ///
372    /// If any invocations of these function return an error, it will be propagated.
373    ///
374    /// If `capture_output` is true, then all the output will also be buffered
375    /// and stored in the returned `Output` object. If it is false, no caching
376    /// is done, and the callbacks are solely responsible for handling the
377    /// output.
378    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    /// Builds the command with an `@<path>` argfile that contains all the
486    /// arguments. This is primarily served for rustc/rustdoc command family.
487    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    /// Builds a command from `ProcessBuilder` for everything but not `args`.
525    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    /// Converts `ProcessBuilder` into a `std::process::Command`, and handles
557    /// the jobserver, if present.
558    ///
559    /// Note that this method doesn't take argfile fallback into account. The
560    /// caller should handle it by themselves.
561    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    /// Wraps an existing command with the provided wrapper, if it is present and valid.
570    ///
571    /// # Examples
572    ///
573    /// ```rust
574    /// use cargo_util::ProcessBuilder;
575    /// // Running this would execute `rustc`
576    /// let cmd = ProcessBuilder::new("rustc");
577    ///
578    /// // Running this will execute `sccache rustc`
579    /// let cmd = cmd.wrapped(Some("sccache"));
580    /// ```
581    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
609/// Forces the command to use `@path` argfile.
610///
611/// You should set `__CARGO_TEST_FORCE_ARGFILE` to enable this.
612fn debug_force_argfile(retry_enabled: bool) -> bool {
613    retry_enabled && env::var("__CARGO_TEST_FORCE_ARGFILE").is_ok()
614}
615
616/// Creates new pipes for stderr, stdout, and optionally stdin.
617fn 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        // Do nothing; let the child process handle it.
685        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        // Just execute the process as normal.
696        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}