cc/
command_helpers.rs

1//! Miscellaneous helpers for running commands
2
3use std::{
4    borrow::Cow,
5    collections::hash_map,
6    ffi::OsString,
7    fmt::Display,
8    fs,
9    hash::Hasher,
10    io::{self, Read, Write},
11    path::Path,
12    process::{Child, ChildStderr, Command, Stdio},
13    sync::{
14        atomic::{AtomicBool, Ordering},
15        Arc,
16    },
17};
18
19use crate::{Error, ErrorKind, Object};
20
21#[derive(Clone, Debug)]
22pub(crate) struct CargoOutput {
23    pub(crate) metadata: bool,
24    pub(crate) warnings: bool,
25    pub(crate) debug: bool,
26    pub(crate) output: OutputKind,
27    checked_dbg_var: Arc<AtomicBool>,
28}
29
30/// Different strategies for handling compiler output (to stdout)
31#[derive(Clone, Debug)]
32pub(crate) enum OutputKind {
33    /// Forward the output to this process' stdout ([`Stdio::inherit()`])
34    Forward,
35    /// Discard the output ([`Stdio::null()`])
36    Discard,
37    /// Capture the result (`[Stdio::piped()`])
38    Capture,
39}
40
41impl CargoOutput {
42    pub(crate) fn new() -> Self {
43        #[allow(clippy::disallowed_methods)]
44        Self {
45            metadata: true,
46            warnings: true,
47            output: OutputKind::Forward,
48            debug: match std::env::var_os("CC_ENABLE_DEBUG_OUTPUT") {
49                Some(v) => v != "0" && v != "false" && v != "",
50                None => false,
51            },
52            checked_dbg_var: Arc::new(AtomicBool::new(false)),
53        }
54    }
55
56    pub(crate) fn print_metadata(&self, s: &dyn Display) {
57        if self.metadata {
58            println!("{}", s);
59        }
60    }
61
62    pub(crate) fn print_warning(&self, arg: &dyn Display) {
63        if self.warnings {
64            println!("cargo:warning={}", arg);
65        }
66    }
67
68    pub(crate) fn print_debug(&self, arg: &dyn Display) {
69        if self.metadata && !self.checked_dbg_var.load(Ordering::Relaxed) {
70            self.checked_dbg_var.store(true, Ordering::Relaxed);
71            println!("cargo:rerun-if-env-changed=CC_ENABLE_DEBUG_OUTPUT");
72        }
73        if self.debug {
74            println!("{}", arg);
75        }
76    }
77
78    fn stdio_for_warnings(&self) -> Stdio {
79        if self.warnings {
80            Stdio::piped()
81        } else {
82            Stdio::null()
83        }
84    }
85
86    fn stdio_for_output(&self) -> Stdio {
87        match self.output {
88            OutputKind::Capture => Stdio::piped(),
89            OutputKind::Forward => Stdio::inherit(),
90            OutputKind::Discard => Stdio::null(),
91        }
92    }
93}
94
95pub(crate) struct StderrForwarder {
96    inner: Option<(ChildStderr, Vec<u8>)>,
97    #[cfg(feature = "parallel")]
98    is_non_blocking: bool,
99    #[cfg(feature = "parallel")]
100    bytes_available_failed: bool,
101    /// number of bytes buffered in inner
102    bytes_buffered: usize,
103}
104
105const MIN_BUFFER_CAPACITY: usize = 100;
106
107impl StderrForwarder {
108    pub(crate) fn new(child: &mut Child) -> Self {
109        Self {
110            inner: child
111                .stderr
112                .take()
113                .map(|stderr| (stderr, Vec::with_capacity(MIN_BUFFER_CAPACITY))),
114            bytes_buffered: 0,
115            #[cfg(feature = "parallel")]
116            is_non_blocking: false,
117            #[cfg(feature = "parallel")]
118            bytes_available_failed: false,
119        }
120    }
121
122    fn forward_available(&mut self) -> bool {
123        if let Some((stderr, buffer)) = self.inner.as_mut() {
124            loop {
125                // For non-blocking we check to see if there is data available, so we should try to
126                // read at least that much. For blocking, always read at least the minimum amount.
127                #[cfg(not(feature = "parallel"))]
128                let to_reserve = MIN_BUFFER_CAPACITY;
129                #[cfg(feature = "parallel")]
130                let to_reserve = if self.is_non_blocking && !self.bytes_available_failed {
131                    match crate::parallel::stderr::bytes_available(stderr) {
132                        #[cfg(windows)]
133                        Ok(0) => break false,
134                        #[cfg(unix)]
135                        Ok(0) => {
136                            // On Unix, depending on the implementation, we may sometimes get 0 in a
137                            // loop (either there is data available or the pipe is broken), so
138                            // continue with the non-blocking read anyway.
139                            MIN_BUFFER_CAPACITY
140                        }
141                        #[cfg(windows)]
142                        Err(_) => {
143                            // On Windows, if we get an error then the pipe is broken, so flush
144                            // the buffer and bail.
145                            if !buffer.is_empty() {
146                                write_warning(&buffer[..]);
147                            }
148                            self.inner = None;
149                            break true;
150                        }
151                        #[cfg(unix)]
152                        Err(_) => {
153                            // On Unix, depending on the implementation, we may get spurious
154                            // errors so make a note not to use bytes_available again and try
155                            // the non-blocking read anyway.
156                            self.bytes_available_failed = true;
157                            MIN_BUFFER_CAPACITY
158                        }
159                        #[cfg(target_family = "wasm")]
160                        Err(_) => panic!("bytes_available should always succeed on wasm"),
161                        Ok(bytes_available) => MIN_BUFFER_CAPACITY.max(bytes_available),
162                    }
163                } else {
164                    MIN_BUFFER_CAPACITY
165                };
166                if self.bytes_buffered + to_reserve > buffer.len() {
167                    buffer.resize(self.bytes_buffered + to_reserve, 0);
168                }
169
170                match stderr.read(&mut buffer[self.bytes_buffered..]) {
171                    Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
172                        // No data currently, yield back.
173                        break false;
174                    }
175                    Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {
176                        // Interrupted, try again.
177                        continue;
178                    }
179                    Ok(bytes_read) if bytes_read != 0 => {
180                        self.bytes_buffered += bytes_read;
181                        let mut consumed = 0;
182                        for line in buffer[..self.bytes_buffered].split_inclusive(|&b| b == b'\n') {
183                            // Only forward complete lines, leave the rest in the buffer.
184                            if let Some((b'\n', line)) = line.split_last() {
185                                consumed += line.len() + 1;
186                                write_warning(line);
187                            }
188                        }
189                        if consumed > 0 && consumed < self.bytes_buffered {
190                            // Remove the consumed bytes from buffer
191                            buffer.copy_within(consumed.., 0);
192                        }
193                        self.bytes_buffered -= consumed;
194                    }
195                    res => {
196                        // End of stream: flush remaining data and bail.
197                        if self.bytes_buffered > 0 {
198                            write_warning(&buffer[..self.bytes_buffered]);
199                        }
200                        if let Err(err) = res {
201                            write_warning(
202                                format!("Failed to read from child stderr: {err}").as_bytes(),
203                            );
204                        }
205                        self.inner.take();
206                        break true;
207                    }
208                }
209            }
210        } else {
211            true
212        }
213    }
214
215    #[cfg(feature = "parallel")]
216    pub(crate) fn set_non_blocking(&mut self) -> Result<(), Error> {
217        assert!(!self.is_non_blocking);
218
219        #[cfg(unix)]
220        if let Some((stderr, _)) = self.inner.as_ref() {
221            crate::parallel::stderr::set_non_blocking(stderr)?;
222        }
223
224        self.is_non_blocking = true;
225        Ok(())
226    }
227
228    #[cfg(feature = "parallel")]
229    fn forward_all(&mut self) {
230        while !self.forward_available() {}
231    }
232
233    #[cfg(not(feature = "parallel"))]
234    fn forward_all(&mut self) {
235        let forward_result = self.forward_available();
236        assert!(forward_result, "Should have consumed all data");
237    }
238}
239
240fn write_warning(line: &[u8]) {
241    let stdout = io::stdout();
242    let mut stdout = stdout.lock();
243    stdout.write_all(b"cargo:warning=").unwrap();
244    stdout.write_all(line).unwrap();
245    stdout.write_all(b"\n").unwrap();
246}
247
248fn wait_on_child(
249    cmd: &Command,
250    program: &Path,
251    child: &mut Child,
252    cargo_output: &CargoOutput,
253) -> Result<(), Error> {
254    StderrForwarder::new(child).forward_all();
255
256    let status = match child.wait() {
257        Ok(s) => s,
258        Err(e) => {
259            return Err(Error::new(
260                ErrorKind::ToolExecError,
261                format!(
262                    "Failed to wait on spawned child process, command {:?} with args {}: {}.",
263                    cmd,
264                    program.display(),
265                    e
266                ),
267            ));
268        }
269    };
270
271    cargo_output.print_debug(&status);
272
273    if status.success() {
274        Ok(())
275    } else {
276        Err(Error::new(
277            ErrorKind::ToolExecError,
278            format!(
279                "Command {:?} with args {} did not execute successfully (status code {}).",
280                cmd,
281                program.display(),
282                status
283            ),
284        ))
285    }
286}
287
288/// Find the destination object path for each file in the input source files,
289/// and store them in the output Object.
290pub(crate) fn objects_from_files(files: &[Arc<Path>], dst: &Path) -> Result<Vec<Object>, Error> {
291    let mut objects = Vec::with_capacity(files.len());
292    for file in files {
293        let basename = file
294            .file_name()
295            .ok_or_else(|| {
296                Error::new(
297                    ErrorKind::InvalidArgument,
298                    "No file_name for object file path!",
299                )
300            })?
301            .to_string_lossy();
302        let dirname = file
303            .parent()
304            .ok_or_else(|| {
305                Error::new(
306                    ErrorKind::InvalidArgument,
307                    "No parent for object file path!",
308                )
309            })?
310            .to_string_lossy();
311
312        // Hash the dirname. This should prevent conflicts if we have multiple
313        // object files with the same filename in different subfolders.
314        let mut hasher = hash_map::DefaultHasher::new();
315
316        // Make the dirname relative (if possible) to avoid full system paths influencing the sha
317        // and making the output system-dependent
318        //
319        // NOTE: Here we allow using std::env::var (instead of Build::getenv) because
320        // CARGO_* variables always trigger a rebuild when changed
321        #[allow(clippy::disallowed_methods)]
322        let dirname = if let Some(root) = std::env::var_os("CARGO_MANIFEST_DIR") {
323            let root = root.to_string_lossy();
324            Cow::Borrowed(dirname.strip_prefix(&*root).unwrap_or(&dirname))
325        } else {
326            dirname
327        };
328
329        hasher.write(dirname.as_bytes());
330        let obj = dst
331            .join(format!("{:016x}-{}", hasher.finish(), basename))
332            .with_extension("o");
333
334        match obj.parent() {
335            Some(s) => fs::create_dir_all(s)?,
336            None => {
337                return Err(Error::new(
338                    ErrorKind::InvalidArgument,
339                    "dst is an invalid path with no parent",
340                ));
341            }
342        };
343
344        objects.push(Object::new(file.to_path_buf(), obj));
345    }
346
347    Ok(objects)
348}
349
350pub(crate) fn run(
351    cmd: &mut Command,
352    program: impl AsRef<Path>,
353    cargo_output: &CargoOutput,
354) -> Result<(), Error> {
355    let program = program.as_ref();
356
357    let mut child = spawn(cmd, program, cargo_output)?;
358    wait_on_child(cmd, program, &mut child, cargo_output)
359}
360
361pub(crate) fn run_output(
362    cmd: &mut Command,
363    program: impl AsRef<Path>,
364    cargo_output: &CargoOutput,
365) -> Result<Vec<u8>, Error> {
366    let program = program.as_ref();
367
368    // We specifically need the output to be captured, so override default
369    let mut captured_cargo_output = cargo_output.clone();
370    captured_cargo_output.output = OutputKind::Capture;
371    let mut child = spawn(cmd, program, &captured_cargo_output)?;
372
373    let mut stdout = vec![];
374    child
375        .stdout
376        .take()
377        .unwrap()
378        .read_to_end(&mut stdout)
379        .unwrap();
380
381    // Don't care about this output, use the normal settings
382    wait_on_child(cmd, program, &mut child, cargo_output)?;
383
384    Ok(stdout)
385}
386
387pub(crate) fn spawn(
388    cmd: &mut Command,
389    program: &Path,
390    cargo_output: &CargoOutput,
391) -> Result<Child, Error> {
392    struct ResetStderr<'cmd>(&'cmd mut Command);
393
394    impl Drop for ResetStderr<'_> {
395        fn drop(&mut self) {
396            // Reset stderr to default to release pipe_writer so that print thread will
397            // not block forever.
398            self.0.stderr(Stdio::inherit());
399        }
400    }
401
402    cargo_output.print_debug(&format_args!("running: {:?}", cmd));
403
404    let cmd = ResetStderr(cmd);
405    let child = cmd
406        .0
407        .stderr(cargo_output.stdio_for_warnings())
408        .stdout(cargo_output.stdio_for_output())
409        .spawn();
410    match child {
411        Ok(child) => Ok(child),
412        Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
413            let extra = if cfg!(windows) {
414                " (see https://docs.rs/cc/latest/cc/#compile-time-requirements \
415for help)"
416            } else {
417                ""
418            };
419            Err(Error::new(
420                ErrorKind::ToolNotFound,
421                format!(
422                    "Failed to find tool. Is `{}` installed?{}",
423                    program.display(),
424                    extra
425                ),
426            ))
427        }
428        Err(e) => Err(Error::new(
429            ErrorKind::ToolExecError,
430            format!(
431                "Command {:?} with args {} failed to start: {:?}",
432                cmd.0,
433                program.display(),
434                e
435            ),
436        )),
437    }
438}
439
440pub(crate) struct CmdAddOutputFileArgs {
441    pub(crate) cuda: bool,
442    pub(crate) is_assembler_msvc: bool,
443    pub(crate) msvc: bool,
444    pub(crate) clang: bool,
445    pub(crate) gnu: bool,
446    pub(crate) is_asm: bool,
447    pub(crate) is_arm: bool,
448}
449
450pub(crate) fn command_add_output_file(cmd: &mut Command, dst: &Path, args: CmdAddOutputFileArgs) {
451    if args.is_assembler_msvc
452        || !(!args.msvc || args.clang || args.gnu || args.cuda || (args.is_asm && args.is_arm))
453    {
454        let mut s = OsString::from("-Fo");
455        s.push(dst);
456        cmd.arg(s);
457    } else {
458        cmd.arg("-o").arg(dst);
459    }
460}
461
462#[cfg(feature = "parallel")]
463pub(crate) fn try_wait_on_child(
464    cmd: &Command,
465    program: &Path,
466    child: &mut Child,
467    stdout: &mut dyn io::Write,
468    stderr_forwarder: &mut StderrForwarder,
469) -> Result<Option<()>, Error> {
470    stderr_forwarder.forward_available();
471
472    match child.try_wait() {
473        Ok(Some(status)) => {
474            stderr_forwarder.forward_all();
475
476            let _ = writeln!(stdout, "{}", status);
477
478            if status.success() {
479                Ok(Some(()))
480            } else {
481                Err(Error::new(
482                    ErrorKind::ToolExecError,
483                    format!(
484                        "Command {:?} with args {} did not execute successfully (status code {}).",
485                        cmd,
486                        program.display(),
487                        status
488                    ),
489                ))
490            }
491        }
492        Ok(None) => Ok(None),
493        Err(e) => {
494            stderr_forwarder.forward_all();
495            Err(Error::new(
496                ErrorKind::ToolExecError,
497                format!(
498                    "Failed to wait on spawned child process, command {:?} with args {}: {}.",
499                    cmd,
500                    program.display(),
501                    e
502                ),
503            ))
504        }
505    }
506}