cargo / cc / audit
cargo : cc @ 1.2.62
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

src/command_helpers.rs

483 lines · rust · 1 line annotation

//! Miscellaneous helpers for running commandsuse std::{    borrow::Cow,    collections::hash_map,    ffi::OsString,    fmt::Display,    fs,    hash::Hasher,    io::{self, Read, Write},    path::Path,    process::{Child, ChildStderr, Command, Output, Stdio},    sync::{        atomic::{AtomicBool, Ordering},        Arc,    },};use crate::{utilities::cargo_env_var_os, Error, ErrorKind, Object};#[derive(Clone, Debug)]pub(crate) struct CargoOutput {    pub(crate) metadata: bool,    pub(crate) warnings: bool,    pub(crate) debug: bool,    pub(crate) output: OutputKind,    checked_dbg_var: Arc<AtomicBool>,}/// Different strategies for handling compiler output (to stdout)#[derive(Clone, Debug)]pub(crate) enum OutputKind {    /// Forward the output to this process' stdout ([`Stdio::inherit()`])    Forward,    /// Discard the output ([`Stdio::null()`])    Discard,    /// Capture the result ([`Stdio::piped()`])    Capture,}impl CargoOutput {    pub(crate) fn new() -> Self {        #[allow(clippy::disallowed_methods)]        Self {            metadata: true,            warnings: true,            output: OutputKind::Forward,            debug: match std::env::var_os("CC_ENABLE_DEBUG_OUTPUT") {                Some(v) => v != "0" && v != "false" && !v.is_empty(),                None => false,            },            checked_dbg_var: Arc::new(AtomicBool::new(false)),        }    }    pub(crate) fn print_metadata(&self, s: &dyn Display) {        if self.metadata {            println!("{s}");        }    }    pub(crate) fn print_warning(&self, arg: &dyn Display) {        if self.warnings {            println!("cargo:warning={arg}");        }    }    pub(crate) fn print_debug(&self, arg: &dyn Display) {        if self.metadata            && self                .checked_dbg_var                .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)                .is_ok()        {            println!("cargo:rerun-if-env-changed=CC_ENABLE_DEBUG_OUTPUT");        }        if self.debug {            println!("{arg}");        }    }    fn stdio_for_warnings(&self) -> Stdio {        if self.warnings {            Stdio::piped()        } else {            Stdio::null()        }    }    fn stdio_for_output(&self) -> Stdio {        match self.output {            OutputKind::Capture => Stdio::piped(),            OutputKind::Forward => Stdio::inherit(),            OutputKind::Discard => Stdio::null(),        }    }}pub(crate) struct StderrForwarder {    inner: Option<(ChildStderr, Vec<u8>)>,    #[cfg(feature = "parallel")]    is_non_blocking: bool,    #[cfg(feature = "parallel")]    bytes_available_failed: bool,    /// number of bytes buffered in inner    bytes_buffered: usize,}const MIN_BUFFER_CAPACITY: usize = 100;impl StderrForwarder {    pub(crate) fn new(child: &mut Child) -> Self {        Self {            inner: child                .stderr                .take()                .map(|stderr| (stderr, Vec::with_capacity(MIN_BUFFER_CAPACITY))),            bytes_buffered: 0,            #[cfg(feature = "parallel")]            is_non_blocking: false,            #[cfg(feature = "parallel")]            bytes_available_failed: false,        }    }    pub(crate) fn forward_available(&mut self) -> bool {        if let Some((stderr, buffer)) = self.inner.as_mut() {            loop {                // For non-blocking we check to see if there is data available, so we should try to                // read at least that much. For blocking, always read at least the minimum amount.                #[cfg(not(feature = "parallel"))]                let to_reserve = MIN_BUFFER_CAPACITY;                #[cfg(feature = "parallel")]                let to_reserve = if self.is_non_blocking && !self.bytes_available_failed {                    match crate::parallel::stderr::bytes_available(stderr) {                        #[cfg(windows)]                        Ok(0) => break false,                        #[cfg(unix)]                        Ok(0) => {                            // On Unix, depending on the implementation, we may sometimes get 0 in a                            // loop (either there is data available or the pipe is broken), so                            // continue with the non-blocking read anyway.                            MIN_BUFFER_CAPACITY                        }                        #[cfg(windows)]                        Err(_) => {                            // On Windows, if we get an error then the pipe is broken, so flush                            // the buffer and bail.                            if !buffer.is_empty() {                                write_warning(&buffer[..]);                            }                            self.inner = None;                            break true;                        }                        #[cfg(unix)]                        Err(_) => {                            // On Unix, depending on the implementation, we may get spurious                            // errors so make a note not to use bytes_available again and try                            // the non-blocking read anyway.                            self.bytes_available_failed = true;                            MIN_BUFFER_CAPACITY                        }                        #[cfg(target_family = "wasm")]                        Err(_) => panic!("bytes_available should always succeed on wasm"),                        Ok(bytes_available) => MIN_BUFFER_CAPACITY.max(bytes_available),                    }                } else {                    MIN_BUFFER_CAPACITY                };                if self.bytes_buffered + to_reserve > buffer.len() {                    buffer.resize(self.bytes_buffered + to_reserve, 0);                }                match stderr.read(&mut buffer[self.bytes_buffered..]) {                    Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {                        // No data currently, yield back.                        break false;                    }                    Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {                        // Interrupted, try again.                        continue;                    }                    Ok(bytes_read) if bytes_read != 0 => {                        self.bytes_buffered += bytes_read;                        let mut consumed = 0;                        for line in buffer[..self.bytes_buffered].split_inclusive(|&b| b == b'\n') {                            // Only forward complete lines, leave the rest in the buffer.                            if let Some((b'\n', line)) = line.split_last() {                                consumed += line.len() + 1;                                write_warning(line);                            }                        }                        if consumed > 0 && consumed < self.bytes_buffered {                            // Remove the consumed bytes from buffer                            buffer.copy_within(consumed.., 0);                        }                        self.bytes_buffered -= consumed;                    }                    res => {                        // End of stream: flush remaining data and bail.                        if self.bytes_buffered > 0 {                            write_warning(&buffer[..self.bytes_buffered]);                        }                        if let Err(err) = res {                            write_warning(                                format!("Failed to read from child stderr: {err}").as_bytes(),                            );                        }                        self.inner.take();                        break true;                    }                }            }        } else {            true        }    }    #[cfg(feature = "parallel")]    pub(crate) fn set_non_blocking(&mut self) -> Result<(), Error> {        assert!(!self.is_non_blocking);        #[cfg(unix)]        if let Some((stderr, _)) = self.inner.as_ref() {            crate::parallel::stderr::set_non_blocking(stderr)?;        }        self.is_non_blocking = true;        Ok(())    }    #[cfg(feature = "parallel")]    pub(crate) fn forward_all(&mut self) {        while !self.forward_available() {}    }    #[cfg(not(feature = "parallel"))]    fn forward_all(&mut self) {        let forward_result = self.forward_available();        assert!(forward_result, "Should have consumed all data");    }}fn write_warning(line: &[u8]) {    let stdout = io::stdout();    let mut stdout = stdout.lock();    stdout.write_all(b"cargo:warning=").unwrap();    stdout.write_all(line).unwrap();    stdout.write_all(b"\n").unwrap();}fn wait_on_child(    cmd: &Command,    child: &mut Child,    cargo_output: &CargoOutput,) -> Result<(), Error> {    StderrForwarder::new(child).forward_all();    let status = match child.wait() {        Ok(s) => s,        Err(e) => {            return Err(Error::new(                ErrorKind::ToolExecError,                format!("failed to wait on spawned child process `{cmd:?}`: {e}"),            ));        }    };    cargo_output.print_debug(&status);    if status.success() {        Ok(())    } else {        Err(Error::new(            ErrorKind::ToolExecError,            format!("command did not execute successfully (status code {status}): {cmd:?}"),        ))    }}/// Find the destination object path for each file in the input source files,/// and store them in the output Object.pub(crate) fn objects_from_files(files: &[Arc<Path>], dst: &Path) -> Result<Vec<Object>, Error> {    let mut objects = Vec::with_capacity(files.len());    for file in files {        let basename = file            .file_name()            .ok_or_else(|| {                Error::new(                    ErrorKind::InvalidArgument,                    "No file_name for object file path!",                )            })?            .to_string_lossy();        let dirname = file            .parent()            .ok_or_else(|| {                Error::new(                    ErrorKind::InvalidArgument,                    "No parent for object file path!",                )            })?            .to_string_lossy();        // Hash the dirname. This should prevent conflicts if we have multiple        // object files with the same filename in different subfolders.        let mut hasher = hash_map::DefaultHasher::new();        // Make the dirname relative (if possible) to avoid full system paths influencing the sha        // and making the output system-dependent        let dirname = if let Some(root) = cargo_env_var_os("CARGO_MANIFEST_DIR") {            let root = root.to_string_lossy();            Cow::Borrowed(dirname.strip_prefix(&*root).unwrap_or(&dirname))        } else {            dirname        };        hasher.write(dirname.as_bytes());        if let Some(extension) = file.extension() {            hasher.write(extension.to_string_lossy().as_bytes());        }        let obj = dst            .join(format!("{:016x}-{}", hasher.finish(), basename))            .with_extension("o");        match obj.parent() {            Some(s) => fs::create_dir_all(s)?,            None => {                return Err(Error::new(                    ErrorKind::InvalidArgument,                    "dst is an invalid path with no parent",                ));            }        };        objects.push(Object::new(file.to_path_buf(), obj));    }    Ok(objects)}pub(crate) fn run(cmd: &mut Command, cargo_output: &CargoOutput) -> Result<(), Error> {    let mut child = spawn(cmd, cargo_output)?;    wait_on_child(cmd, &mut child, cargo_output)}/// Like [`run`], but stderr is only forwarded as `cargo:warning=` when the/// command succeeds. On failure, stderr is silently discarded.////// Useful for probe commands where failure is expected and the error/// message is not actionable.pub(crate) fn run_silent_on_error(    cmd: &mut Command,    cargo_output: &CargoOutput,) -> Result<(), Error> {    let Output {        status,        stdout: _,        stderr,    } = spawn_and_wait_for_output(cmd, cargo_output)?;    cargo_output.print_debug(&status);    if status.success() {        if cargo_output.warnings {            stderr                .split(|&b| b == b'\n')                .map(|line| line.strip_suffix(b"\r").unwrap_or(line))                .filter(|line| !line.is_empty())                .for_each(write_warning);        }        Ok(())    } else {        Err(Error::new(            ErrorKind::ToolExecError,            format!("command did not execute successfully (status code {status}): {cmd:?}"),        ))    }}pub(crate) fn spawn_and_wait_for_output(    cmd: &mut Command,    cargo_output: &CargoOutput,) -> Result<Output, Error> {    // We specifically need the output to be captured, so override default    let mut captured_cargo_output = cargo_output.clone();    captured_cargo_output.output = OutputKind::Capture;    spawn(cmd, &captured_cargo_output)?        .wait_with_output()        .map_err(|e| {            Error::new(                ErrorKind::ToolExecError,                format!("failed to wait on spawned child process `{cmd:?}`: {e}"),            )        })}pub(crate) fn run_output(cmd: &mut Command, cargo_output: &CargoOutput) -> Result<Vec<u8>, Error> {    let Output {        status,        stdout,        stderr,    } = spawn_and_wait_for_output(cmd, cargo_output)?;    stderr        .split(|&b| b == b'\n')        .filter(|part| !part.is_empty())        .for_each(write_warning);    cargo_output.print_debug(&status);    if status.success() {        Ok(stdout)    } else {        Err(Error::new(            ErrorKind::ToolExecError,            format!("command did not execute successfully (status code {status}): {cmd:?}"),        ))    }}
pub(crate) fn spawn(cmd: &mut Command, cargo_output: &CargoOutput) -> Result<Child, Error> {    struct ResetStderr<'cmd>(&'cmd mut Command);    impl Drop for ResetStderr<'_> {        fn drop(&mut self) {            // Reset stderr to default to release pipe_writer so that print thread will            // not block forever.            self.0.stderr(Stdio::inherit());        }    }    cargo_output.print_debug(&format_args!("running: {cmd:?}"));    let cmd = ResetStderr(cmd);    let child = cmd        .0        .stderr(cargo_output.stdio_for_warnings())        .stdout(cargo_output.stdio_for_output())        .spawn();    match child {        Ok(child) => Ok(child),        Err(ref e) if e.kind() == io::ErrorKind::NotFound => {            let extra = if cfg!(windows) {                " (see https://docs.rs/cc/latest/cc/#compile-time-requirements for help)"            } else {                ""            };            Err(Error::new(                ErrorKind::ToolNotFound,                format!("failed to find tool {:?}: {e}{extra}", cmd.0.get_program()),            ))        }        Err(e) => Err(Error::new(            ErrorKind::ToolExecError,            format!("command `{:?}` failed to start: {e}", cmd.0),        )),    }}
Line 423–460

All subprocess invocations use std::process::Command::new(path) with arguments added via cmd.arg(...), never via shell string interpolation. The compiler path is resolved from the CC/CXX/AR environment variables or built-in defaults; arguments are passed as typed OsString or &str values, never through a shell. This avoids shell injection. The shlex crate is used to split CFLAGS-style env vars when CC_SHELL_ESCAPED_FLAGS is set; it is also used without that flag to split the space-separated compiler path from CC='sccache cc'.

Justifies uses-exec and exec-safe.

pub(crate) struct CmdAddOutputFileArgs {    pub(crate) cuda: bool,    pub(crate) is_assembler_msvc: bool,    pub(crate) msvc: bool,    pub(crate) clang: bool,    pub(crate) gnu: bool,    pub(crate) is_asm: bool,    pub(crate) is_arm: bool,}pub(crate) fn command_add_output_file(cmd: &mut Command, dst: &Path, args: CmdAddOutputFileArgs) {    if args.is_assembler_msvc        || !(!args.msvc || args.clang || args.gnu || args.cuda || (args.is_asm && args.is_arm))    {        let mut s = OsString::from("-Fo");        s.push(dst);        cmd.arg(s);    } else {        cmd.arg("-o").arg(dst);    }}