491 lines
15 KiB
Rust
491 lines
15 KiB
Rust
//! Miscellaneous helpers for running commands
|
|
|
|
use std::{
|
|
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>,
|
|
}
|
|
|
|
/// 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 != "",
|
|
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,
|
|
/// 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,
|
|
}
|
|
}
|
|
|
|
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")]
|
|
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
|
|
),
|
|
))
|
|
}
|
|
}
|
|
|
|
/// 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();
|
|
hasher.write(dirname.to_string().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();
|
|
|
|
// 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;
|
|
let mut child = spawn(cmd, program, &captured_cargo_output)?;
|
|
|
|
let mut stdout = vec![];
|
|
child
|
|
.stdout
|
|
.take()
|
|
.unwrap()
|
|
.read_to_end(&mut stdout)
|
|
.unwrap();
|
|
|
|
// Don't care about this output, use the normal settings
|
|
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) {
|
|
// 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. 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
|
|
),
|
|
))
|
|
}
|
|
}
|
|
}
|