use std::ffi::OsStr;
use std::{
io,
path::{Path, PathBuf},
process::Output,
process::Stdio,
sync::atomic::{AtomicBool, Ordering},
};
use color_eyre::eyre;
use smol::{process::Command, unblock};
pub(crate) async fn which(name: &'static str) -> Result<PathBuf, which::Error> {
unblock(move || which::which(name)).await
}
static STD_OUTPUT: AtomicBool = AtomicBool::new(false);
pub fn set_std_output(enabled: bool) {
STD_OUTPUT.store(enabled, std::sync::atomic::Ordering::SeqCst);
}
#[must_use]
pub const fn sccache_install_hint() -> &'static str {
if cfg!(target_os = "macos") {
"brew install sccache"
} else if cfg!(target_os = "linux") {
"your distro package manager (e.g. apt/dnf/pacman) or cargo install sccache"
} else if cfg!(target_os = "windows") {
"winget install Mozilla.sccache or cargo install sccache"
} else {
"cargo install sccache"
}
}
pub(crate) fn command(command: &mut Command) -> &mut Command {
command
.kill_on_drop(true)
.stdout(if STD_OUTPUT.load(Ordering::SeqCst) {
Stdio::inherit()
} else {
Stdio::piped()
})
.stderr(if STD_OUTPUT.load(Ordering::SeqCst) {
Stdio::inherit()
} else {
Stdio::piped()
})
}
pub(crate) async fn run_command_output_os<N, A, S>(name: N, args: A) -> eyre::Result<Output>
where
N: AsRef<OsStr>,
A: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let name = name.as_ref();
let result = Command::new(name)
.args(args)
.kill_on_drop(true)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;
if STD_OUTPUT.load(Ordering::SeqCst) {
use std::io::Write;
let _ = std::io::stdout().write_all(&result.stdout);
let _ = std::io::stderr().write_all(&result.stderr);
}
Ok(result)
}
pub(crate) async fn run_command(
name: &str,
args: impl IntoIterator<Item = &str>,
) -> eyre::Result<String> {
run_command_os(name, args).await
}
pub(crate) async fn run_command_os<N, A, S>(name: N, args: A) -> eyre::Result<String>
where
N: AsRef<OsStr>,
A: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let name_ref = name.as_ref();
let result = run_command_output_os(name_ref, args).await?;
if result.status.success() {
Ok(String::from_utf8_lossy(&result.stdout).to_string())
} else {
let name_display = name_ref.to_string_lossy();
Err(eyre::eyre!(
"Command {name_display} failed with status {}{}{}",
result.status,
format_failure_stream("stderr", &result.stderr),
format_failure_stream("stdout", &result.stdout),
))
}
}
const MAX_REPORTED_OUTPUT_LINES: usize = 200;
fn format_failure_stream(label: &str, bytes: &[u8]) -> String {
let text = String::from_utf8_lossy(bytes);
let trimmed = text.trim_end();
if trimmed.is_empty() {
return String::new();
}
let lines: Vec<&str> = trimmed.lines().collect();
let elided = lines.len().saturating_sub(MAX_REPORTED_OUTPUT_LINES);
let body = lines[elided..].join("\n");
if elided == 0 {
format!("\n{label}:\n{body}")
} else {
format!(
"\n{label} (last {MAX_REPORTED_OUTPUT_LINES} of {} lines):\n{body}",
lines.len()
)
}
}
pub(crate) fn parse_whitespace_separated_u32s(input: &str) -> Vec<u32> {
input
.split_whitespace()
.filter_map(|part| part.parse::<u32>().ok())
.collect()
}
pub async fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
let from = from.as_ref().to_path_buf();
let to = to.as_ref().to_path_buf();
unblock(move || reflink::reflink_or_copy(from, to).map(|_| ())).await
}
#[cfg(test)]
mod tests {
use super::parse_whitespace_separated_u32s;
#[test]
fn parses_pidof_output_with_multiple_pids() {
let parsed = parse_whitespace_separated_u32s("123 456\n");
assert_eq!(parsed, vec![123, 456]);
}
#[test]
fn ignores_non_numeric_tokens() {
let parsed = parse_whitespace_separated_u32s("foo 42 bar\n");
assert_eq!(parsed, vec![42]);
}
}