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.is_empty(),
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    child: &mut Child,
251    cargo_output: &CargoOutput,
252) -> Result<(), Error> {
253    StderrForwarder::new(child).forward_all();
254
255    let status = match child.wait() {
256        Ok(s) => s,
257        Err(e) => {
258            return Err(Error::new(
259                ErrorKind::ToolExecError,
260                format!("failed to wait on spawned child process `{cmd:?}`: {e}"),
261            ));
262        }
263    };
264
265    cargo_output.print_debug(&status);
266
267    if status.success() {
268        Ok(())
269    } else {
270        Err(Error::new(
271            ErrorKind::ToolExecError,
272            format!("command did not execute successfully (status code {status}): {cmd:?}"),
273        ))
274    }
275}
276
277/// Find the destination object path for each file in the input source files,
278/// and store them in the output Object.
279pub(crate) fn objects_from_files(files: &[Arc<Path>], dst: &Path) -> Result<Vec<Object>, Error> {
280    let mut objects = Vec::with_capacity(files.len());
281    for file in files {
282        let basename = file
283            .file_name()
284            .ok_or_else(|| {
285                Error::new(
286                    ErrorKind::InvalidArgument,
287                    "No file_name for object file path!",
288                )
289            })?
290            .to_string_lossy();
291        let dirname = file
292            .parent()
293            .ok_or_else(|| {
294                Error::new(
295                    ErrorKind::InvalidArgument,
296                    "No parent for object file path!",
297                )
298            })?
299            .to_string_lossy();
300
301        // Hash the dirname. This should prevent conflicts if we have multiple
302        // object files with the same filename in different subfolders.
303        let mut hasher = hash_map::DefaultHasher::new();
304
305        // Make the dirname relative (if possible) to avoid full system paths influencing the sha
306        // and making the output system-dependent
307        //
308        // NOTE: Here we allow using std::env::var (instead of Build::getenv) because
309        // CARGO_* variables always trigger a rebuild when changed
310        #[allow(clippy::disallowed_methods)]
311        let dirname = if let Some(root) = std::env::var_os("CARGO_MANIFEST_DIR") {
312            let root = root.to_string_lossy();
313            Cow::Borrowed(dirname.strip_prefix(&*root).unwrap_or(&dirname))
314        } else {
315            dirname
316        };
317
318        hasher.write(dirname.as_bytes());
319        if let Some(extension) = file.extension() {
320            hasher.write(extension.to_string_lossy().as_bytes());
321        }
322        let obj = dst
323            .join(format!("{:016x}-{}", hasher.finish(), basename))
324            .with_extension("o");
325
326        match obj.parent() {
327            Some(s) => fs::create_dir_all(s)?,
328            None => {
329                return Err(Error::new(
330                    ErrorKind::InvalidArgument,
331                    "dst is an invalid path with no parent",
332                ));
333            }
334        };
335
336        objects.push(Object::new(file.to_path_buf(), obj));
337    }
338
339    Ok(objects)
340}
341
342pub(crate) fn run(cmd: &mut Command, cargo_output: &CargoOutput) -> Result<(), Error> {
343    let mut child = spawn(cmd, cargo_output)?;
344    wait_on_child(cmd, &mut child, cargo_output)
345}
346
347pub(crate) fn run_output(cmd: &mut Command, cargo_output: &CargoOutput) -> Result<Vec<u8>, Error> {
348    // We specifically need the output to be captured, so override default
349    let mut captured_cargo_output = cargo_output.clone();
350    captured_cargo_output.output = OutputKind::Capture;
351    let mut child = spawn(cmd, &captured_cargo_output)?;
352
353    let mut stdout = vec![];
354    child
355        .stdout
356        .take()
357        .unwrap()
358        .read_to_end(&mut stdout)
359        .unwrap();
360
361    // Don't care about this output, use the normal settings
362    wait_on_child(cmd, &mut child, cargo_output)?;
363
364    Ok(stdout)
365}
366
367pub(crate) fn spawn(cmd: &mut Command, cargo_output: &CargoOutput) -> Result<Child, Error> {
368    struct ResetStderr<'cmd>(&'cmd mut Command);
369
370    impl Drop for ResetStderr<'_> {
371        fn drop(&mut self) {
372            // Reset stderr to default to release pipe_writer so that print thread will
373            // not block forever.
374            self.0.stderr(Stdio::inherit());
375        }
376    }
377
378    cargo_output.print_debug(&format_args!("running: {:?}", cmd));
379
380    let cmd = ResetStderr(cmd);
381    let child = cmd
382        .0
383        .stderr(cargo_output.stdio_for_warnings())
384        .stdout(cargo_output.stdio_for_output())
385        .spawn();
386    match child {
387        Ok(child) => Ok(child),
388        Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
389            let extra = if cfg!(windows) {
390                " (see https://docs.rs/cc/latest/cc/#compile-time-requirements for help)"
391            } else {
392                ""
393            };
394            Err(Error::new(
395                ErrorKind::ToolNotFound,
396                format!("failed to find tool {:?}: {e}{extra}", cmd.0.get_program()),
397            ))
398        }
399        Err(e) => Err(Error::new(
400            ErrorKind::ToolExecError,
401            format!("command `{:?}` failed to start: {e}", cmd.0),
402        )),
403    }
404}
405
406pub(crate) struct CmdAddOutputFileArgs {
407    pub(crate) cuda: bool,
408    pub(crate) is_assembler_msvc: bool,
409    pub(crate) msvc: bool,
410    pub(crate) clang: bool,
411    pub(crate) gnu: bool,
412    pub(crate) is_asm: bool,
413    pub(crate) is_arm: bool,
414}
415
416pub(crate) fn command_add_output_file(cmd: &mut Command, dst: &Path, args: CmdAddOutputFileArgs) {
417    if args.is_assembler_msvc
418        || !(!args.msvc || args.clang || args.gnu || args.cuda || (args.is_asm && args.is_arm))
419    {
420        let mut s = OsString::from("-Fo");
421        s.push(dst);
422        cmd.arg(s);
423    } else {
424        cmd.arg("-o").arg(dst);
425    }
426}
427
428#[cfg(feature = "parallel")]
429pub(crate) fn try_wait_on_child(
430    cmd: &Command,
431    child: &mut Child,
432    stdout: &mut dyn io::Write,
433    stderr_forwarder: &mut StderrForwarder,
434) -> Result<Option<()>, Error> {
435    stderr_forwarder.forward_available();
436
437    match child.try_wait() {
438        Ok(Some(status)) => {
439            stderr_forwarder.forward_all();
440
441            let _ = writeln!(stdout, "{}", status);
442
443            if status.success() {
444                Ok(Some(()))
445            } else {
446                Err(Error::new(
447                    ErrorKind::ToolExecError,
448                    format!("command did not execute successfully (status code {status}): {cmd:?}"),
449                ))
450            }
451        }
452        Ok(None) => Ok(None),
453        Err(e) => {
454            stderr_forwarder.forward_all();
455            Err(Error::new(
456                ErrorKind::ToolExecError,
457                format!("failed to wait on spawned child process `{cmd:?}`: {e}"),
458            ))
459        }
460    }
461}