use std::{
borrow::Cow,
collections::hash_map,
ffi::{OsStr, OsString},
fmt::{self, Display},
fs,
hash::Hasher,
io::{self, Read, Write},
path::Path,
process::{Child, ChildStderr, Command, ExitStatus, Output, Stdio},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use crate::{
build_env::{BuildEnv, EnvVars},
logger::Logger,
utilities::cargo_env_var_os,
BuildMessageKind, 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,
pub(crate) logger: Option<Logger>,
is_detection_cmd: bool,
checked_dbg_var: Arc<AtomicBool>,
}
#[derive(Clone, Debug)]
pub(crate) enum OutputKind {
Forward,
Discard,
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,
},
logger: None,
is_detection_cmd: false,
checked_dbg_var: Arc::new(AtomicBool::new(false)),
}
}
pub(crate) fn for_detection_cmd(&self) -> Self {
let mut detection = self.clone();
detection.is_detection_cmd = true;
detection
}
pub(crate) fn quiet_unless_debug(&self) -> Self {
let mut quiet = self.clone();
quiet.warnings = quiet.debug;
if !quiet.debug {
quiet.logger = None;
}
quiet
}
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}");
}
if let Some(logger) = &self.logger {
logger.log(BuildMessageKind::GeneralWarning, &arg.to_string(), &());
}
}
fn forward_stderr_line(&self, line: &[u8], cmd: &Command) {
if self.warnings {
write_warning(line);
}
if let Some(logger) = &self.logger {
let line = line.strip_suffix(b"\r").unwrap_or(line);
logger.log(
BuildMessageKind::StderrForwarding,
&String::from_utf8_lossy(line),
cmd,
);
}
}
pub(crate) fn command_failed(&self, cmd: &Command, status: ExitStatus) -> Error {
let message = format!(
"command did not execute successfully (status code {status}): {}",
CommandLine(cmd)
);
if let Some(logger) = &self.logger {
logger.log(
BuildMessageKind::CommandFailed {
is_detection_cmd: self.is_detection_cmd,
exit_status: status,
},
&message,
cmd,
);
}
Error::new(ErrorKind::ToolExecError, message)
}
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 || self.logger.is_some() {
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>)>,
cargo_output: CargoOutput,
#[cfg(feature = "parallel")]
is_non_blocking: bool,
#[cfg(feature = "parallel")]
bytes_available_failed: bool,
bytes_buffered: usize,
}
const MIN_BUFFER_CAPACITY: usize = 100;
impl StderrForwarder {
pub(crate) fn new(child: &mut Child, cargo_output: &CargoOutput) -> Self {
Self {
inner: child
.stderr
.take()
.map(|stderr| (stderr, Vec::with_capacity(MIN_BUFFER_CAPACITY))),
cargo_output: cargo_output.clone(),
bytes_buffered: 0,
#[cfg(feature = "parallel")]
is_non_blocking: false,
#[cfg(feature = "parallel")]
bytes_available_failed: false,
}
}
#[cfg(feature = "parallel")]
pub(crate) fn stderr(&self) -> Option<&ChildStderr> {
self.inner.as_ref().map(|inner| &inner.0)
}
pub(crate) fn forward_available(&mut self, cmd: &Command) -> bool {
let Some((stderr, buffer)) = self.inner.as_mut() else {
return true;
};
loop {
#[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) => {
MIN_BUFFER_CAPACITY
}
#[cfg(windows)]
Err(_) => {
if self.bytes_buffered > 0 {
self.cargo_output
.forward_stderr_line(&buffer[..self.bytes_buffered], cmd);
}
self.inner = None;
break true;
}
#[cfg(unix)]
Err(_) => {
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 => {
break false;
}
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {
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') {
if let Some((b'\n', line)) = line.split_last() {
consumed += line.len() + 1;
self.cargo_output.forward_stderr_line(line, cmd);
}
}
if consumed > 0 && consumed < self.bytes_buffered {
buffer.copy_within(consumed.., 0);
}
self.bytes_buffered -= consumed;
}
res => {
if self.bytes_buffered > 0 {
self.cargo_output
.forward_stderr_line(&buffer[..self.bytes_buffered], cmd);
}
if let Err(err) = res {
self.cargo_output.print_warning(&format_args!(
"Failed to read from child stderr: {err}"
));
}
self.inner.take();
break 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, cmd: &Command) {
while !self.forward_available(cmd) {}
}
#[cfg(not(feature = "parallel"))]
fn forward_all(&mut self, cmd: &Command) {
let forward_result = self.forward_available(cmd);
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, cargo_output).forward_all(cmd);
let status = match child.wait() {
Ok(s) => s,
Err(e) => {
return Err(Error::new(
ErrorKind::ToolExecError,
format!(
"failed to wait on spawned child process `{}`: {e}",
CommandLine(cmd)
),
));
}
};
cargo_output.print_debug(&status);
if status.success() {
Ok(())
} else {
Err(cargo_output.command_failed(cmd, status))
}
}
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();
let mut hasher = hash_map::DefaultHasher::new();
let strip_root = |var| {
cargo_env_var_os(var).and_then(|root| {
dirname
.strip_prefix(&*root.to_string_lossy())
.map(|rel| Cow::Owned(rel.to_owned()))
})
};
let dirname = strip_root("OUT_DIR")
.or_else(|| strip_root("CARGO_MANIFEST_DIR"))
.unwrap_or(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)
}
#[derive(Clone, Copy, Debug)]
enum ProbeKind {
FamilyDetection,
FlagSupportCheck,
ArDetection,
CppStdlibDetection,
}
impl ProbeKind {
const fn out_files_var(self) -> &'static str {
match self {
Self::FamilyDetection => "CC_SHIM_OUT_FILES_FOR_FAMILY_DETECTION",
Self::FlagSupportCheck => "CC_SHIM_OUT_FILES_FOR_FLAG_SUPPORT_CHECK",
Self::ArDetection => "CC_SHIM_OUT_FILES_FOR_AR_DETECTION",
Self::CppStdlibDetection => "CC_SHIM_OUT_FILES_FOR_CPP_STDLIB_DETECTION",
}
}
const fn stdout_var(self) -> &'static str {
match self {
Self::FamilyDetection => "CC_SHIM_STDOUT_FOR_FAMILY_DETECTION",
Self::FlagSupportCheck => "CC_SHIM_STDOUT_FOR_FLAG_SUPPORT_CHECK",
Self::ArDetection => "CC_SHIM_STDOUT_FOR_AR_DETECTION",
Self::CppStdlibDetection => "CC_SHIM_STDOUT_FOR_CPP_STDLIB_DETECTION",
}
}
}
fn set_probe_env(cmd: &mut Command, env: &EnvVars, kind: ProbeKind) {
for (key, value) in env {
cmd.env(key, value);
}
cmd.env_remove("CC_SHIM_OUT_DIR");
for (var, shim_var) in [
(kind.out_files_var(), "CC_SHIM_OUT_FILES"),
(kind.stdout_var(), "CC_SHIM_STDOUT"),
] {
match env.iter().find(|(key, _)| &**key == OsStr::new(var)) {
Some((_, value)) => cmd.env(shim_var, value),
None => cmd.env_remove(shim_var),
};
}
}
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)
}
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() {
stderr_warnings(&stderr, None).for_each(|line| cargo_output.forward_stderr_line(line, cmd));
Ok(())
} else {
Err(Error::new(
ErrorKind::ToolExecError,
format!(
"command did not execute successfully (status code {status}): {}",
CommandLine(cmd)
),
))
}
}
pub(crate) fn spawn_and_wait_for_output(
cmd: &mut Command,
cargo_output: &CargoOutput,
) -> Result<Output, Error> {
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 `{}`: {e}",
CommandLine(cmd)
),
)
})
}
pub(crate) fn run_output(cmd: &mut Command, cargo_output: &CargoOutput) -> Result<Vec<u8>, Error> {
run_output_ignoring_line(cmd, cargo_output, None)
}
pub(crate) fn run_output_ignoring_line(
cmd: &mut Command,
cargo_output: &CargoOutput,
ignored_line: Option<&[u8]>,
) -> Result<Vec<u8>, Error> {
let Output {
status,
stdout,
stderr,
} = spawn_and_wait_for_output(cmd, cargo_output)?;
stderr_warnings(&stderr, ignored_line)
.for_each(|line| cargo_output.forward_stderr_line(line, cmd));
cargo_output.print_debug(&status);
if status.success() {
Ok(stdout)
} else {
Err(cargo_output.command_failed(cmd, status))
}
}
fn stderr_warnings<'a>(
stderr: &'a [u8],
ignored_line: Option<&'a [u8]>,
) -> impl Iterator<Item = &'a [u8]> {
stderr
.split(|&b| b == b'\n')
.map(|line| line.strip_suffix(b"\r").unwrap_or(line))
.filter(move |line| !line.is_empty() && Some(*line) != ignored_line)
}
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) {
self.0.stderr(Stdio::inherit());
}
}
cargo_output.print_debug(&format_args!("running: {}", CommandLine(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}", CommandLine(cmd.0)),
)),
}
}
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);
}
}
pub(crate) struct CommandLine<'a>(pub(crate) &'a Command);
impl fmt::Display for CommandLine<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0.get_program())?;
for arg in self.0.get_args() {
write!(f, " {arg:?}")?;
}
Ok(())
}
}
pub(crate) trait CommandExt {
fn set_family_detection_env(&mut self, env: &BuildEnv) -> &mut Self;
fn set_flag_supported_env(&mut self, env: &EnvVars) -> &mut Self;
fn set_ar_detection_env(&mut self, env: &BuildEnv) -> &mut Self;
fn set_cpp_stdlib_detection_env(&mut self, env: &EnvVars) -> &mut Self;
}
impl CommandExt for Command {
fn set_family_detection_env(&mut self, env: &BuildEnv) -> &mut Self {
env.inherited().apply(self);
set_probe_env(self, &env.explicit, ProbeKind::FamilyDetection);
self
}
fn set_flag_supported_env(&mut self, env: &EnvVars) -> &mut Self {
set_probe_env(self, env, ProbeKind::FlagSupportCheck);
self
}
fn set_ar_detection_env(&mut self, env: &BuildEnv) -> &mut Self {
env.inherited().apply(self);
set_probe_env(self, &env.explicit, ProbeKind::ArDetection);
self
}
fn set_cpp_stdlib_detection_env(&mut self, env: &EnvVars) -> &mut Self {
set_probe_env(self, env, ProbeKind::CppStdlibDetection);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stderr_warnings_skips_ignored_line() {
let stderr = b"expando.c\r\nexpando.c(2): warning C4005: 'X': macro redefinition\r\n";
let warnings: Vec<_> = stderr_warnings(stderr, Some(b"expando.c")).collect();
assert_eq!(
warnings,
[&b"expando.c(2): warning C4005: 'X': macro redefinition"[..]]
);
}
#[test]
fn stderr_warnings_keeps_every_line_without_ignored_line() {
let stderr = b"expando.c\n\nsecond\n";
let warnings: Vec<_> = stderr_warnings(stderr, None).collect();
assert_eq!(warnings, [&b"expando.c"[..], &b"second"[..]]);
}
}