use std::{
borrow::Cow,
collections::hash_map,
ffi::OsString,
fmt::Display,
fs,
hash::Hasher,
io::{self, Read, Write},
path::Path,
process::{Child, ChildStderr, Command, Stdio},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use crate::{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>,
}
#[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 != "",
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.load(Ordering::Relaxed) {
self.checked_dbg_var.store(true, Ordering::Relaxed);
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,
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,
}
}
fn forward_available(&mut self) -> bool {
if let Some((stderr, buffer)) = self.inner.as_mut() {
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 !buffer.is_empty() {
write_warning(&buffer[..]);
}
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;
write_warning(line);
}
}
if consumed > 0 && consumed < self.bytes_buffered {
buffer.copy_within(consumed.., 0);
}
self.bytes_buffered -= consumed;
}
res => {
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")]
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,
program: &Path,
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, command {:?} with args {}: {}.",
cmd,
program.display(),
e
),
));
}
};
cargo_output.print_debug(&status);
if status.success() {
Ok(())
} else {
Err(Error::new(
ErrorKind::ToolExecError,
format!(
"Command {:?} with args {} did not execute successfully (status code {}).",
cmd,
program.display(),
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();
#[allow(clippy::disallowed_methods)]
let dirname = if let Some(root) = std::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,
program: impl AsRef<Path>,
cargo_output: &CargoOutput,
) -> Result<(), Error> {
let program = program.as_ref();
let mut child = spawn(cmd, program, cargo_output)?;
wait_on_child(cmd, program, &mut child, cargo_output)
}
pub(crate) fn run_output(
cmd: &mut Command,
program: impl AsRef<Path>,
cargo_output: &CargoOutput,
) -> Result<Vec<u8>, Error> {
let program = program.as_ref();
let mut captured_cargo_output = cargo_output.clone();
captured_cargo_output.output = OutputKind::Capture;
let mut child = spawn(cmd, program, &captured_cargo_output)?;
let mut stdout = vec![];
child
.stdout
.take()
.unwrap()
.read_to_end(&mut stdout)
.unwrap();
wait_on_child(cmd, program, &mut child, cargo_output)?;
Ok(stdout)
}
pub(crate) fn spawn(
cmd: &mut Command,
program: &Path,
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: {:?}", 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. Is `{}` installed?{}",
program.display(),
extra
),
))
}
Err(e) => Err(Error::new(
ErrorKind::ToolExecError,
format!(
"Command {:?} with args {} failed to start: {:?}",
cmd.0,
program.display(),
e
),
)),
}
}
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);
}
}
#[cfg(feature = "parallel")]
pub(crate) fn try_wait_on_child(
cmd: &Command,
program: &Path,
child: &mut Child,
stdout: &mut dyn io::Write,
stderr_forwarder: &mut StderrForwarder,
) -> Result<Option<()>, Error> {
stderr_forwarder.forward_available();
match child.try_wait() {
Ok(Some(status)) => {
stderr_forwarder.forward_all();
let _ = writeln!(stdout, "{}", status);
if status.success() {
Ok(Some(()))
} else {
Err(Error::new(
ErrorKind::ToolExecError,
format!(
"Command {:?} with args {} did not execute successfully (status code {}).",
cmd,
program.display(),
status
),
))
}
}
Ok(None) => Ok(None),
Err(e) => {
stderr_forwarder.forward_all();
Err(Error::new(
ErrorKind::ToolExecError,
format!(
"Failed to wait on spawned child process, command {:?} with args {}: {}.",
cmd,
program.display(),
e
),
))
}
}
}