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
70            && self
71                .checked_dbg_var
72                .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
73                .is_ok()
74        {
75            println!("cargo:rerun-if-env-changed=CC_ENABLE_DEBUG_OUTPUT");
76        }
77        if self.debug {
78            println!("{arg}");
79        }
80    }
81
82    fn stdio_for_warnings(&self) -> Stdio {
83        if self.warnings {
84            Stdio::piped()
85        } else {
86            Stdio::null()
87        }
88    }
89
90    fn stdio_for_output(&self) -> Stdio {
91        match self.output {
92            OutputKind::Capture => Stdio::piped(),
93            OutputKind::Forward => Stdio::inherit(),
94            OutputKind::Discard => Stdio::null(),
95        }
96    }
97}
98
99pub(crate) struct StderrForwarder {
100    inner: Option<(ChildStderr, Vec<u8>)>,
101    #[cfg(feature = "parallel")]
102    is_non_blocking: bool,
103    #[cfg(feature = "parallel")]
104    bytes_available_failed: bool,
105    /// number of bytes buffered in inner
106    bytes_buffered: usize,
107}
108
109const MIN_BUFFER_CAPACITY: usize = 100;
110
111impl StderrForwarder {
112    pub(crate) fn new(child: &mut Child) -> Self {
113        Self {
114            inner: child
115                .stderr
116                .take()
117                .map(|stderr| (stderr, Vec::with_capacity(MIN_BUFFER_CAPACITY))),
118            bytes_buffered: 0,
119            #[cfg(feature = "parallel")]
120            is_non_blocking: false,
121            #[cfg(feature = "parallel")]
122            bytes_available_failed: false,
123        }
124    }
125
126    pub(crate) fn forward_available(&mut self) -> bool {
127        if let Some((stderr, buffer)) = self.inner.as_mut() {
128            loop {
129                // For non-blocking we check to see if there is data available, so we should try to
130                // read at least that much. For blocking, always read at least the minimum amount.
131                #[cfg(not(feature = "parallel"))]
132                let to_reserve = MIN_BUFFER_CAPACITY;
133                #[cfg(feature = "parallel")]
134                let to_reserve = if self.is_non_blocking && !self.bytes_available_failed {
135                    match crate::parallel::stderr::bytes_available(stderr) {
136                        #[cfg(windows)]
137                        Ok(0) => break false,
138                        #[cfg(unix)]
139                        Ok(0) => {
140                            // On Unix, depending on the implementation, we may sometimes get 0 in a
141                            // loop (either there is data available or the pipe is broken), so
142                            // continue with the non-blocking read anyway.
143                            MIN_BUFFER_CAPACITY
144                        }
145                        #[cfg(windows)]
146                        Err(_) => {
147                            // On Windows, if we get an error then the pipe is broken, so flush
148                            // the buffer and bail.
149                            if !buffer.is_empty() {
150                                write_warning(&buffer[..]);
151                            }
152                            self.inner = None;
153                            break true;
154                        }
155                        #[cfg(unix)]
156                        Err(_) => {
157                            // On Unix, depending on the implementation, we may get spurious
158                            // errors so make a note not to use bytes_available again and try
159                            // the non-blocking read anyway.
160                            self.bytes_available_failed = true;
161                            MIN_BUFFER_CAPACITY
162                        }
163                        #[cfg(target_family = "wasm")]
164                        Err(_) => panic!("bytes_available should always succeed on wasm"),
165                        Ok(bytes_available) => MIN_BUFFER_CAPACITY.max(bytes_available),
166                    }
167                } else {
168                    MIN_BUFFER_CAPACITY
169                };
170                if self.bytes_buffered + to_reserve > buffer.len() {
171                    buffer.resize(self.bytes_buffered + to_reserve, 0);
172                }
173
174                match stderr.read(&mut buffer[self.bytes_buffered..]) {
175                    Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
176                        // No data currently, yield back.
177                        break false;
178                    }
179                    Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {
180                        // Interrupted, try again.
181                        continue;
182                    }
183                    Ok(bytes_read) if bytes_read != 0 => {
184                        self.bytes_buffered += bytes_read;
185                        let mut consumed = 0;
186                        for line in buffer[..self.bytes_buffered].split_inclusive(|&b| b == b'\n') {
187                            // Only forward complete lines, leave the rest in the buffer.
188                            if let Some((b'\n', line)) = line.split_last() {
189                                consumed += line.len() + 1;
190                                write_warning(line);
191                            }
192                        }
193                        if consumed > 0 && consumed < self.bytes_buffered {
194                            // Remove the consumed bytes from buffer
195                            buffer.copy_within(consumed.., 0);
196                        }
197                        self.bytes_buffered -= consumed;
198                    }
199                    res => {
200                        // End of stream: flush remaining data and bail.
201                        if self.bytes_buffered > 0 {
202                            write_warning(&buffer[..self.bytes_buffered]);
203                        }
204                        if let Err(err) = res {
205                            write_warning(
206                                format!("Failed to read from child stderr: {err}").as_bytes(),
207                            );
208                        }
209                        self.inner.take();
210                        break true;
211                    }
212                }
213            }
214        } else {
215            true
216        }
217    }
218
219    #[cfg(feature = "parallel")]
220    pub(crate) fn set_non_blocking(&mut self) -> Result<(), Error> {
221        assert!(!self.is_non_blocking);
222
223        #[cfg(unix)]
224        if let Some((stderr, _)) = self.inner.as_ref() {
225            crate::parallel::stderr::set_non_blocking(stderr)?;
226        }
227
228        self.is_non_blocking = true;
229        Ok(())
230    }
231
232    #[cfg(feature = "parallel")]
233    pub(crate) fn forward_all(&mut self) {
234        while !self.forward_available() {}
235    }
236
237    #[cfg(not(feature = "parallel"))]
238    fn forward_all(&mut self) {
239        let forward_result = self.forward_available();
240        assert!(forward_result, "Should have consumed all data");
241    }
242}
243
244fn write_warning(line: &[u8]) {
245    let stdout = io::stdout();
246    let mut stdout = stdout.lock();
247    stdout.write_all(b"cargo:warning=").unwrap();
248    stdout.write_all(line).unwrap();
249    stdout.write_all(b"\n").unwrap();
250}
251
252fn wait_on_child(
253    cmd: &Command,
254    child: &mut Child,
255    cargo_output: &CargoOutput,
256) -> Result<(), Error> {
257    StderrForwarder::new(child).forward_all();
258
259    let status = match child.wait() {
260        Ok(s) => s,
261        Err(e) => {
262            return Err(Error::new(
263                ErrorKind::ToolExecError,
264                format!("failed to wait on spawned child process `{cmd:?}`: {e}"),
265            ));
266        }
267    };
268
269    cargo_output.print_debug(&status);
270
271    if status.success() {
272        Ok(())
273    } else {
274        Err(Error::new(
275            ErrorKind::ToolExecError,
276            format!("command did not execute successfully (status code {status}): {cmd:?}"),
277        ))
278    }
279}
280
281/// Find the destination object path for each file in the input source files,
282/// and store them in the output Object.
283pub(crate) fn objects_from_files(files: &[Arc<Path>], dst: &Path) -> Result<Vec<Object>, Error> {
284    let mut objects = Vec::with_capacity(files.len());
285    for file in files {
286        let basename = file
287            .file_name()
288            .ok_or_else(|| {
289                Error::new(
290                    ErrorKind::InvalidArgument,
291                    "No file_name for object file path!",
292                )
293            })?
294            .to_string_lossy();
295        let dirname = file
296            .parent()
297            .ok_or_else(|| {
298                Error::new(
299                    ErrorKind::InvalidArgument,
300                    "No parent for object file path!",
301                )
302            })?
303            .to_string_lossy();
304
305        // Hash the dirname. This should prevent conflicts if we have multiple
306        // object files with the same filename in different subfolders.
307        let mut hasher = hash_map::DefaultHasher::new();
308
309        // Make the dirname relative (if possible) to avoid full system paths influencing the sha
310        // and making the output system-dependent
311        //
312        // NOTE: Here we allow using std::env::var (instead of Build::getenv) because
313        // CARGO_* variables always trigger a rebuild when changed
314        #[allow(clippy::disallowed_methods)]
315        let dirname = if let Some(root) = std::env::var_os("CARGO_MANIFEST_DIR") {
316            let root = root.to_string_lossy();
317            Cow::Borrowed(dirname.strip_prefix(&*root).unwrap_or(&dirname))
318        } else {
319            dirname
320        };
321
322        hasher.write(dirname.as_bytes());
323        if let Some(extension) = file.extension() {
324            hasher.write(extension.to_string_lossy().as_bytes());
325        }
326        let obj = dst
327            .join(format!("{:016x}-{}", hasher.finish(), basename))
328            .with_extension("o");
329
330        match obj.parent() {
331            Some(s) => fs::create_dir_all(s)?,
332            None => {
333                return Err(Error::new(
334                    ErrorKind::InvalidArgument,
335                    "dst is an invalid path with no parent",
336                ));
337            }
338        };
339
340        objects.push(Object::new(file.to_path_buf(), obj));
341    }
342
343    Ok(objects)
344}
345
346pub(crate) fn run(cmd: &mut Command, cargo_output: &CargoOutput) -> Result<(), Error> {
347    let mut child = spawn(cmd, cargo_output)?;
348    wait_on_child(cmd, &mut child, cargo_output)
349}
350
351pub(crate) fn run_output(cmd: &mut Command, cargo_output: &CargoOutput) -> Result<Vec<u8>, Error> {
352    // We specifically need the output to be captured, so override default
353    let mut captured_cargo_output = cargo_output.clone();
354    captured_cargo_output.output = OutputKind::Capture;
355    let mut child = spawn(cmd, &captured_cargo_output)?;
356
357    let mut stdout = vec![];
358    child
359        .stdout
360        .take()
361        .unwrap()
362        .read_to_end(&mut stdout)
363        .unwrap();
364
365    // Don't care about this output, use the normal settings
366    wait_on_child(cmd, &mut child, cargo_output)?;
367
368    Ok(stdout)
369}
370
371pub(crate) fn spawn(cmd: &mut Command, cargo_output: &CargoOutput) -> Result<Child, Error> {
372    struct ResetStderr<'cmd>(&'cmd mut Command);
373
374    impl Drop for ResetStderr<'_> {
375        fn drop(&mut self) {
376            // Reset stderr to default to release pipe_writer so that print thread will
377            // not block forever.
378            self.0.stderr(Stdio::inherit());
379        }
380    }
381
382    cargo_output.print_debug(&format_args!("running: {cmd:?}"));
383
384    let cmd = ResetStderr(cmd);
385    let child = cmd
386        .0
387        .stderr(cargo_output.stdio_for_warnings())
388        .stdout(cargo_output.stdio_for_output())
389        .spawn();
390    match child {
391        Ok(child) => Ok(child),
392        Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
393            let extra = if cfg!(windows) {
394                " (see https://docs.rs/cc/latest/cc/#compile-time-requirements for help)"
395            } else {
396                ""
397            };
398            Err(Error::new(
399                ErrorKind::ToolNotFound,
400                format!("failed to find tool {:?}: {e}{extra}", cmd.0.get_program()),
401            ))
402        }
403        Err(e) => Err(Error::new(
404            ErrorKind::ToolExecError,
405            format!("command `{:?}` failed to start: {e}", cmd.0),
406        )),
407    }
408}
409
410pub(crate) struct CmdAddOutputFileArgs {
411    pub(crate) cuda: bool,
412    pub(crate) is_assembler_msvc: bool,
413    pub(crate) msvc: bool,
414    pub(crate) clang: bool,
415    pub(crate) gnu: bool,
416    pub(crate) is_asm: bool,
417    pub(crate) is_arm: bool,
418}
419
420pub(crate) fn command_add_output_file(cmd: &mut Command, dst: &Path, args: CmdAddOutputFileArgs) {
421    if args.is_assembler_msvc
422        || !(!args.msvc || args.clang || args.gnu || args.cuda || (args.is_asm && args.is_arm))
423    {
424        let mut s = OsString::from("-Fo");
425        s.push(dst);
426        cmd.arg(s);
427    } else {
428        cmd.arg("-o").arg(dst);
429    }
430}